win32 gui: decide on aligning cells to pixel boundary

This commit is contained in:
Jonathan Marler 2025-01-05 16:45:39 -07:00 committed by CJ van den Berg
parent 456ec8535b
commit 67613c5bc4

View file

@ -53,43 +53,6 @@ const window_style_ex = win32.WINDOW_EX_STYLE{
}; };
const window_style = win32.WS_OVERLAPPEDWINDOW; const window_style = win32.WS_OVERLAPPEDWINDOW;
const CellAlignment = enum {
// doesn't not align the cell size on the pixel boundary, this can
// cause more anti-aliasing than pixel mode in certain situations
none,
// aligns the cell size up to the next pixel size, means there
// will likely be gaps between cells
pixel,
};
//const cell_alignment = CellAlignment.none;
const cell_alignment = CellAlignment.pixel;
const CellUnit = switch (cell_alignment) {
.none => f32,
.pixel => i32,
};
fn cellUnitFrom(value: anytype) CellUnit {
return switch (@typeInfo(@TypeOf(value))) {
.Int => switch (cell_alignment) {
.none => @floatFromInt(value),
.pixel => @intCast(value),
},
else => @compileError("cellUnitFrom " ++ @typeName(@TypeOf(value)) ++ " is not supported"),
};
}
fn fromCellUnit(comptime T: type, unit: CellUnit) T {
return switch (@typeInfo(T)) {
.Float => switch (cell_alignment) {
.none => @floatCast(unit),
.pixel => @floatFromInt(unit),
},
.Int => switch (cell_alignment) {
.none => @intFromFloat(unit),
.pixel => @intCast(unit),
},
else => @compileError("fromCellUnit " ++ @typeName(T) ++ " is not supported"),
};
}
pub fn init() void { pub fn init() void {
std.debug.assert(!global.init_called); std.debug.assert(!global.init_called);
global.init_called = true; global.init_called = true;
@ -179,14 +142,14 @@ fn createTextFormatEditor(dpi: Dpi) *win32.IDWriteTextFormat {
return ddui.createTextFormat(global.dwrite_factory, &err, .{ return ddui.createTextFormat(global.dwrite_factory, &err, .{
.size = win32.scaleDpi(f32, 14, dpi.value), .size = win32.scaleDpi(f32, 14, dpi.value),
.family_name = win32.L("Cascadia Code"), .family_name = win32.L("Cascadia Code"),
.center_x = switch (cell_alignment) { // the cell size is rounded up to the nearest whole number so the character
.none => false, // will tend to be a bit offset towards the top/left corner unless we center it.
.pixel => true, // I don't think this will necessarily mess with antialiasing since a monospace
}, // font that is concerned with pixel alignment will probably provide text metrics
.center_y = switch (cell_alignment) { // that are integer aligned which would make the text/metric sizes the same
.none => false, // and therefore centering a no-op.
.pixel => true, .center_x = true,
}, .center_y = true,
}) catch std.debug.panic("{s} failed, hresult=0x{x}", .{ err.context, err.hr }); }) catch std.debug.panic("{s} failed, hresult=0x{x}", .{ err.context, err.hr });
} }
@ -262,7 +225,7 @@ const State = struct {
erase_bg_done: bool = false, erase_bg_done: bool = false,
text_format_editor: ddui.TextFormatCache(Dpi, createTextFormatEditor) = .{}, text_format_editor: ddui.TextFormatCache(Dpi, createTextFormatEditor) = .{},
scroll_delta: isize = 0, scroll_delta: isize = 0,
currently_rendered_cell_size: ?XY(CellUnit) = null, currently_rendered_cell_size: ?XY(i32) = null,
// these fields should only be accessed inside the global mutex // these fields should only be accessed inside the global mutex
shared_screen_arena: std.heap.ArenaAllocator, shared_screen_arena: std.heap.ArenaAllocator,
@ -290,7 +253,7 @@ fn paint(
d2d: *const D2d, d2d: *const D2d,
screen: *const vaxis.Screen, screen: *const vaxis.Screen,
text_format_editor: *win32.IDWriteTextFormat, text_format_editor: *win32.IDWriteTextFormat,
cell_size: XY(CellUnit), cell_size: XY(i32),
) void { ) void {
{ {
const color = ddui.rgb8(31, 31, 31); const color = ddui.rgb8(31, 31, 31);
@ -298,20 +261,21 @@ fn paint(
} }
for (0..screen.height) |y| { for (0..screen.height) |y| {
const row_y: CellUnit = cell_size.y * cellUnitFrom(y); const row_y: i32 = cell_size.y * @as(i32, @intCast(y));
for (0..screen.width) |x| { for (0..screen.width) |x| {
const column_x: CellUnit = cell_size.x * cellUnitFrom(x); const column_x: i32 = cell_size.x * @as(i32, @intCast(x));
const cell_index = screen.width * y + x; const cell_index = screen.width * y + x;
const cell = &screen.buf[cell_index]; const cell = &screen.buf[cell_index];
const cell_rect: win32.D2D_RECT_F = .{ const cell_rect: win32.RECT = .{
.left = fromCellUnit(f32, column_x), .left = column_x,
.top = fromCellUnit(f32, row_y), .top = row_y,
.right = fromCellUnit(f32, column_x + cell_size.x), .right = column_x + cell_size.x,
.bottom = fromCellUnit(f32, row_y + cell_size.y), .bottom = row_y + cell_size.y,
}; };
d2d.target.ID2D1RenderTarget.FillRectangle( ddui.FillRectangle(
&cell_rect, &d2d.target.ID2D1RenderTarget,
cell_rect,
d2d.solid(d2dColorFromVAxis(cell.style.bg)), d2d.solid(d2dColorFromVAxis(cell.style.bg)),
); );
@ -324,12 +288,11 @@ fn paint(
const grapheme = buf_wtf16[0..grapheme_len]; const grapheme = buf_wtf16[0..grapheme_len];
if (std.mem.eql(u16, grapheme, &[_]u16{' '})) if (std.mem.eql(u16, grapheme, &[_]u16{' '}))
continue; continue;
ddui.DrawText( ddui.DrawText(
&d2d.target.ID2D1RenderTarget, &d2d.target.ID2D1RenderTarget,
grapheme, grapheme,
text_format_editor, text_format_editor,
cell_rect, ddui.rectFloatFromInt(cell_rect),
d2d.solid(d2dColorFromVAxis(cell.style.fg)), d2d.solid(d2dColorFromVAxis(cell.style.fg)),
.{ .{
.CLIP = 1, .CLIP = 1,
@ -495,19 +458,10 @@ pub fn updateScreen(screen: *const vaxis.Screen) bool {
return true; return true;
} }
fn getCellSize( // NOTE: we round the text metric up to the nearest integer which
text_format_editor: *win32.IDWriteTextFormat, // means our background rectangles will be aligned. We accomodate
) XY(CellUnit) { // for any gap added by doing this by centering the text.
const box_size = getBoxSize(text_format_editor); fn getCellSize(text_format: *win32.IDWriteTextFormat) XY(i32) {
return switch (cell_alignment) {
.none => box_size,
.pixel => .{
.x = @intFromFloat(@ceil(box_size.x)),
.y = @intFromFloat(@ceil(box_size.y)),
},
};
}
fn getBoxSize(text_format: *win32.IDWriteTextFormat) XY(f32) {
var text_layout: *win32.IDWriteTextLayout = undefined; var text_layout: *win32.IDWriteTextLayout = undefined;
{ {
const hr = global.dwrite_factory.CreateTextLayout( const hr = global.dwrite_factory.CreateTextLayout(
@ -527,54 +481,26 @@ fn getBoxSize(text_format: *win32.IDWriteTextFormat) XY(f32) {
const hr = text_layout.GetMetrics(&metrics); const hr = text_layout.GetMetrics(&metrics);
if (hr < 0) fatalHr("GetMetrics", hr); if (hr < 0) fatalHr("GetMetrics", hr);
} }
switch (cell_alignment) {
.none => {
// these seem to only be true if centering is disabled
std.debug.assert(metrics.left == 0);
std.debug.assert(metrics.top == 0);
std.debug.assert(metrics.width == metrics.widthIncludingTrailingWhitespace);
},
.pixel => {},
}
return .{ return .{
.x = metrics.width, .x = @intFromFloat(@ceil(metrics.width)),
.y = metrics.height, .y = @intFromFloat(@ceil(metrics.height)),
}; };
} }
const CellPos = struct { const CellPos = struct {
cell: XY(i32), cell: XY(i32),
offset: XY(i32), offset: XY(i32),
pub fn init(cell_size: XY(CellUnit), x: i32, y: i32) CellPos { pub fn init(cell_size: XY(i32), x: i32, y: i32) CellPos {
switch (cell_alignment) { return .{
.none => { .cell = .{
const cell_pos: XY(CellUnit) = .{ .x = @divTrunc(x, cell_size.x),
.x = @as(f32, @floatFromInt(x)) / cell_size.x, .y = @divTrunc(y, cell_size.y),
.y = @as(f32, @floatFromInt(y)) / cell_size.y,
};
const cell: XY(i32) = .{
.x = @intFromFloat(@floor(cell_pos.x)),
.y = @intFromFloat(@floor(cell_pos.y)),
};
return .{
.cell = cell,
.offset = .{
.x = @intFromFloat(@round(cell_pos.x - @as(f32, @floatFromInt(cell.x)))),
.y = @intFromFloat(@round(cell_pos.y - @as(f32, @floatFromInt(cell.y)))),
},
};
}, },
.pixel => return .{ .offset = .{
.cell = .{ .x = @mod(x, cell_size.x),
.x = @divTrunc(x, cell_size.x), .y = @mod(y, cell_size.y),
.y = @divTrunc(y, cell_size.y),
},
.offset = .{
.x = @mod(x, cell_size.x),
.y = @mod(y, cell_size.y),
},
}, },
} };
} }
}; };
@ -1140,15 +1066,9 @@ fn sendResize(
state.text_format_editor.getOrCreate(Dpi{ .value = @intCast(dpi) }), state.text_format_editor.getOrCreate(Dpi{ .value = @intCast(dpi) }),
); );
const client_pixel_size = getClientSize(hwnd); const client_pixel_size = getClientSize(hwnd);
const client_cell_size: XY(u16) = switch (cell_alignment) { const client_cell_size: XY(u16) = .{
.none => .{ .x = @intCast(@divTrunc(client_pixel_size.x, single_cell_size.x)),
.x = @intFromFloat(@floor(@as(f32, @floatFromInt(client_pixel_size.x)) / single_cell_size.x)), .y = @intCast(@divTrunc(client_pixel_size.y, single_cell_size.y)),
.y = @intFromFloat(@floor(@as(f32, @floatFromInt(client_pixel_size.y)) / single_cell_size.y)),
},
.pixel => .{
.x = @intCast(@divTrunc(client_pixel_size.x, single_cell_size.x)),
.y = @intCast(@divTrunc(client_pixel_size.y, single_cell_size.y)),
},
}; };
std.log.info( std.log.info(
"Resize Px={}x{} Cells={}x{}", "Resize Px={}x{} Cells={}x{}",