diff --git a/src/Surface.zig b/src/Surface.zig index cd23a81da..82837033a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -113,13 +113,8 @@ io_thr: std.Thread, /// Terminal inspector inspector: ?*inspector.Inspector = null, -/// All the cached sizes since we need them at various times. -screen_size: renderer.ScreenSize, -grid_size: renderer.GridSize, -cell_size: renderer.CellSize, - -/// Explicit padding due to configuration -padding: renderer.Padding, +/// All our sizing information. +size: renderer.Size, /// The configuration derived from the main config. We "derive" it so that /// we don't have a shared pointer hanging around that we need to worry about @@ -329,6 +324,32 @@ const DerivedConfig = struct { for (self.links) |*link| link.regex.deinit(); self.arena.deinit(); } + + fn scaledPadding(self: *const DerivedConfig, x_dpi: f32, y_dpi: f32) renderer.Padding { + const padding_top: u32 = padding_top: { + const padding_top: f32 = @floatFromInt(self.window_padding_top); + break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72)); + }; + const padding_bottom: u32 = padding_bottom: { + const padding_bottom: f32 = @floatFromInt(self.window_padding_bottom); + break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72)); + }; + const padding_left: u32 = padding_left: { + const padding_left: f32 = @floatFromInt(self.window_padding_left); + break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72)); + }; + const padding_right: u32 = padding_right: { + const padding_right: f32 = @floatFromInt(self.window_padding_right); + break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72)); + }; + + return .{ + .top = padding_top, + .bottom = padding_bottom, + .left = padding_left, + .right = padding_right, + }; + } }; /// Create a new surface. This must be called from the main thread. The @@ -378,28 +399,26 @@ pub fn init( // Pre-calculate our initial cell size ourselves. const cell_size = font_grid.cellSize(); - // Convert our padding from points to pixels - const padding_top: u32 = padding_top: { - const padding_top: f32 = @floatFromInt(derived_config.window_padding_top); - break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72)); - }; - const padding_bottom: u32 = padding_bottom: { - const padding_bottom: f32 = @floatFromInt(derived_config.window_padding_bottom); - break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72)); - }; - const padding_left: u32 = padding_left: { - const padding_left: f32 = @floatFromInt(derived_config.window_padding_left); - break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72)); - }; - const padding_right: u32 = padding_right: { - const padding_right: f32 = @floatFromInt(derived_config.window_padding_right); - break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72)); - }; - const padding: renderer.Padding = .{ - .top = padding_top, - .bottom = padding_bottom, - .left = padding_left, - .right = padding_right, + // Build our size struct which has all the sizes we need. + const size: renderer.Size = size: { + var size: renderer.Size = .{ + .screen = screen: { + const surface_size = try rt_surface.getSize(); + break :screen .{ + .width = surface_size.width, + .height = surface_size.height, + }; + }, + + .cell = font_grid.cellSize(), + .padding = derived_config.scaledPadding(x_dpi, y_dpi), + }; + + if (derived_config.window_padding_balance) { + size.balancePadding(); + } + + break :size size; }; // Create our terminal grid with the initial size @@ -407,26 +426,12 @@ pub fn init( var renderer_impl = try Renderer.init(alloc, .{ .config = try Renderer.DerivedConfig.init(alloc, config), .font_grid = font_grid, - .padding = .{ - .explicit = padding, - .balance = config.@"window-padding-balance", - }, + .size = size, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, .rt_surface = rt_surface, }); errdefer renderer_impl.deinit(); - // Calculate our grid size based on known dimensions. - const surface_size = try rt_surface.getSize(); - const screen_size: renderer.ScreenSize = .{ - .width = surface_size.width, - .height = surface_size.height, - }; - const grid_size = renderer.GridSize.init( - screen_size.subPadding(padding), - cell_size, - ); - // The mutex used to protect our renderer state. const mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -467,10 +472,7 @@ pub fn init( .io = undefined, .io_thread = io_thread, .io_thr = undefined, - .screen_size = .{ .width = 0, .height = 0 }, - .grid_size = .{}, - .cell_size = cell_size, - .padding = padding, + .size = size, .config = derived_config, }; @@ -510,10 +512,7 @@ pub fn init( errdefer io_mailbox.deinit(alloc); try termio.Termio.init(&self.io, alloc, .{ - .grid_size = grid_size, - .cell_size = cell_size, - .screen_size = screen_size, - .padding = padding, + .size = size, .full_config = config, .config = try termio.Termio.DerivedConfig.init(alloc, config), .backend = .{ .exec = io_exec }, @@ -532,7 +531,7 @@ pub fn init( try rt_app.performAction( .{ .surface = self }, .cell_size, - .{ .width = cell_size.width, .height = cell_size.height }, + .{ .width = size.cell.width, .height = size.cell.height }, ); // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app @@ -541,8 +540,8 @@ pub fn init( .{ .surface = self }, .size_limit, .{ - .min_width = cell_size.width * 10, - .min_height = cell_size.height * 4, + .min_width = size.cell.width * 10, + .min_height = size.cell.height * 4, // No max: .max_width = 0, .max_height = 0, @@ -554,7 +553,7 @@ pub fn init( // init stuff we should get rid of this. But this is required because // sizeCallback does retina-aware stuff we don't do here and don't want // to duplicate. - try self.sizeCallback(surface_size); + try self.resize(self.size.screen); // Give the renderer one more opportunity to finalize any surface // setup on the main thread prior to spinning up the rendering thread. @@ -594,12 +593,12 @@ pub fn init( // account for the padding so we get the exact correct grid size. const final_width: u32 = @as(u32, @intFromFloat(@ceil(width_f32 / scale.x))) + - padding.left + - padding.right; + size.padding.left + + size.padding.right; const final_height: u32 = @as(u32, @intFromFloat(@ceil(height_f32 / scale.y))) + - padding.top + - padding.bottom; + size.padding.top + + size.padding.bottom; rt_app.performAction( .{ .surface = self }, @@ -1152,18 +1151,12 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection { // Our sizes are all scaled so we need to send the unscaled values back. const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; - // We need to account for padding as well. - const pad = if (self.config.window_padding_balance) - renderer.Padding.balanced(self.screen_size, self.grid_size, self.cell_size) - else - self.padding; - const x: f64 = x: { // Simple x * cell width gives the left - var x: f64 = @floatFromInt(tl_coord.x * self.cell_size.width); + var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width); // Add padding - x += @floatFromInt(pad.left); + x += @floatFromInt(self.size.padding.left); // Scale x /= content_scale.x; @@ -1173,14 +1166,14 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection { const y: f64 = y: { // Simple y * cell height gives the top - var y: f64 = @floatFromInt(tl_coord.y * self.cell_size.height); + var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height); // We want the text baseline - y += @floatFromInt(self.cell_size.height); + y += @floatFromInt(self.size.cell.height); y -= @floatFromInt(self.font_metrics.cell_baseline); // Add padding - y += @floatFromInt(pad.top); + y += @floatFromInt(self.size.padding.top); // Scale y /= content_scale.y; @@ -1221,10 +1214,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos { const x: f64 = x: { // Simple x * cell width gives the top-left corner - var x: f64 = @floatFromInt(cursor.x * self.cell_size.width); + var x: f64 = @floatFromInt(cursor.x * self.size.cell.width); // We want the midpoint - x += @as(f64, @floatFromInt(self.cell_size.width)) / 2; + x += @as(f64, @floatFromInt(self.size.cell.width)) / 2; // And scale it x /= content_scale.x; @@ -1234,10 +1227,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos { const y: f64 = y: { // Simple x * cell width gives the top-left corner - var y: f64 = @floatFromInt(cursor.y * self.cell_size.height); + var y: f64 = @floatFromInt(cursor.y * self.size.cell.height); // We want the bottom - y += @floatFromInt(self.cell_size.height); + y += @floatFromInt(self.size.cell.height); // And scale it y /= content_scale.y; @@ -1363,24 +1356,12 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void { /// Change the cell size for the terminal grid. This can happen as /// a result of changing the font size at runtime. fn setCellSize(self: *Surface, size: renderer.CellSize) !void { - // Update our new cell size for future calcs - self.cell_size = size; - - // Update our grid_size - self.grid_size = renderer.GridSize.init( - self.screen_size.subPadding(self.padding), - self.cell_size, - ); + // Update our cell size within our size struct + self.size.cell = size; + if (self.config.window_padding_balance) self.size.balancePadding(); // Notify the terminal - self.io.queueMessage(.{ - .resize = .{ - .grid_size = self.grid_size, - .cell_size = self.cell_size, - .screen_size = self.screen_size, - .padding = self.padding, - }, - }, .unlocked); + self.io.queueMessage(.{ .resize = self.size }, .unlocked); // Notify the window try self.rt_app.performAction( @@ -1451,41 +1432,32 @@ pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void { // Update our screen size, but only if it actually changed. And if // the screen size didn't change, then our grid size could not have // changed, so we just return. - if (self.screen_size.equals(new_screen_size)) return; + if (self.size.screen.equals(new_screen_size)) return; try self.resize(new_screen_size); } fn resize(self: *Surface, size: renderer.ScreenSize) !void { // Save our screen size - self.screen_size = size; + self.size.screen = size; + if (self.config.window_padding_balance) self.size.balancePadding(); // Recalculate our grid size. Because Ghostty supports fluid resizing, // its possible the grid doesn't change at all even if the screen size changes. // We have to update the IO thread no matter what because we send // pixel-level sizing to the subprocess. - self.grid_size = renderer.GridSize.init( - self.screen_size.subPadding(self.padding), - self.cell_size, - ); - if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) { + const grid_size = self.size.grid(); + if (grid_size.columns < 5 and (self.size.padding.left > 0 or self.size.padding.right > 0)) { log.warn("WARNING: very small terminal grid detected with padding " ++ "set. Is your padding reasonable?", .{}); } - if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { + if (grid_size.rows < 2 and (self.size.padding.top > 0 or self.size.padding.bottom > 0)) { log.warn("WARNING: very small terminal grid detected with padding " ++ "set. Is your padding reasonable?", .{}); } // Mail the IO thread - self.io.queueMessage(.{ - .resize = .{ - .grid_size = self.grid_size, - .cell_size = self.cell_size, - .screen_size = self.screen_size, - .padding = self.padding, - }, - }, .unlocked); + self.io.queueMessage(.{ .resize = self.size }, .unlocked); } /// Called to set the preedit state for character input. Preedit is used @@ -2144,7 +2116,8 @@ pub fn scrollCallback( if (!scroll_mods.precision) { // Calculate our magnitude of scroll. This is constant (not // dependent on yoff). - const grid_rows_f64: f64 = @floatFromInt(self.grid_size.rows); + const grid_size = self.size.grid(); + const grid_rows_f64: f64 = @floatFromInt(grid_size.rows); const y_delta_f64: f64 = @round((grid_rows_f64 * self.config.mouse_scroll_multiplier) / 15.0); const y_delta_usize: usize = @max(1, @as(usize, @intFromFloat(y_delta_f64))); @@ -2171,7 +2144,7 @@ pub fn scrollCallback( // If the new offset is less than a single unit of scroll, we save // the new pending value and do not scroll yet. - const cell_size: f64 = @floatFromInt(self.cell_size.height); + const cell_size: f64 = @floatFromInt(self.size.cell.height); if (@abs(poff) < cell_size) { self.mouse.pending_scroll_y = poff; break :y .{}; @@ -2201,7 +2174,7 @@ pub fn scrollCallback( const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier; const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted; - const cell_size: f64 = @floatFromInt(self.cell_size.width); + const cell_size: f64 = @floatFromInt(self.size.cell.width); if (@abs(poff) < cell_size) { self.mouse.pending_scroll_x = poff; break :x .{}; @@ -2326,36 +2299,15 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! try self.setFontSize(size); - // Update our padding which is dependent on DPI. - self.padding = padding: { - const padding_top: u32 = padding_top: { - const padding_top: f32 = @floatFromInt(self.config.window_padding_top); - break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72)); - }; - const padding_bottom: u32 = padding_bottom: { - const padding_bottom: f32 = @floatFromInt(self.config.window_padding_bottom); - break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72)); - }; - const padding_left: u32 = padding_left: { - const padding_left: f32 = @floatFromInt(self.config.window_padding_left); - break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72)); - }; - const padding_right: u32 = padding_right: { - const padding_right: f32 = @floatFromInt(self.config.window_padding_right); - break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72)); - }; - - break :padding .{ - .top = padding_top, - .bottom = padding_bottom, - .left = padding_left, - .right = padding_right, - }; - }; + // Update our padding which is dependent on DPI. We only do this for + // unbalanced padding since balanced padding is not dependent on DPI. + if (!self.config.window_padding_balance) { + self.size.padding = self.config.scaledPadding(x_dpi, y_dpi); + } // Force a resize event because the change in padding will affect // pixel-level changes to the renderer and viewport. - try self.resize(self.screen_size); + try self.resize(self.size.screen); } /// The type of action to report for a mouse event. @@ -2394,8 +2346,8 @@ fn mouseReport( // We always report release events no matter where they happen. if (action != .release) { const pos_out_viewport = pos_out_viewport: { - const max_x: f32 = @floatFromInt(self.screen_size.width); - const max_y: f32 = @floatFromInt(self.screen_size.height); + const max_x: f32 = @floatFromInt(self.size.screen.width); + const max_y: f32 = @floatFromInt(self.size.screen.height); break :pos_out_viewport pos.x < 0 or pos.y < 0 or pos.x > max_x or pos.y > max_y; }; @@ -2554,15 +2506,22 @@ fn mouseReport( .sgr_pixels => { // Final character to send in the CSI const final: u8 = if (action == .release) 'm' else 'M'; - const adjusted = self.posAdjusted(pos.x, pos.y); + + // The position has to be adjusted to the terminal space. + const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{ + .surface = .{ + .x = pos.x, + .y = pos.y, + }, + }).convert(.terminal, self.size).terminal; // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, - @as(i32, @intFromFloat(@round(adjusted.x))), - @as(i32, @intFromFloat(@round(adjusted.y))), + @as(i32, @intFromFloat(@round(coord.x))), + @as(i32, @intFromFloat(@round(coord.y))), final, }); @@ -2817,7 +2776,7 @@ pub fn mouseButtonCallback( // If we move our cursor too much between clicks then we reset // the multi-click state. if (self.mouse.left_click_count > 0) { - const max_distance: f64 = @floatFromInt(self.cell_size.width); + const max_distance: f64 = @floatFromInt(self.size.cell.width); const distance = @sqrt( std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), @@ -3317,8 +3276,8 @@ pub fn cursorPosCallback( // We allow for a 1 pixel buffer at the top and bottom to detect // scroll even in full screen windows. // Note: one day, we can change this from distance to time based if we want. - //log.warn("CURSOR POS: {} {}", .{ pos, self.screen_size }); - const max_y: f32 = @floatFromInt(self.screen_size.height); + //log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen }); + const max_y: f32 = @floatFromInt(self.size.screen.height); if (pos.y <= 1 or pos.y > max_y - 1) { const delta: isize = if (pos.y < 0) -1 else 1; try self.io.terminal.scrollViewport(.{ .delta = delta }); @@ -3477,11 +3436,11 @@ fn dragLeftClickSingle( const click_pin = self.mouse.left_click_pin.?.*; // the boundary point at which we consider selection or non-selection - const cell_width_f64: f64 = @floatFromInt(self.cell_size.width); + const cell_width_f64: f64 = @floatFromInt(self.size.cell.width); const cell_xboundary = cell_width_f64 * 0.6; // first xpos of the clicked cell adjusted for padding - const left_padding_f64: f64 = @as(f64, @floatFromInt(self.padding.left)); + const left_padding_f64: f64 = @as(f64, @floatFromInt(self.size.padding.left)); const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64; const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64; @@ -3631,43 +3590,11 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { if (report) try self.reportColorScheme(); } -pub fn posAdjusted(self: Surface, xpos: f64, ypos: f64) struct { x: f64, y: f64 } { - const pad = if (self.config.window_padding_balance) - renderer.Padding.balanced(self.screen_size, self.grid_size, self.cell_size) - else - self.padding; - - return .{ - .x = xpos - @as(f64, @floatFromInt(pad.left)), - .y = ypos - @as(f64, @floatFromInt(pad.top)), - }; -} - pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { - // xpos/ypos need to be adjusted for window padding - // (i.e. "window-padding-*" settings. - const adjusted = self.posAdjusted(xpos, ypos); - - // adjusted.x and adjusted.y can be negative if while dragging, the user moves the - // mouse off the surface. Likewise, they can be larger than our surface - // width if the user drags out of the surface positively. - return .{ - .x = if (adjusted.x < 0) 0 else x: { - // Our cell is the mouse divided by cell width - const cell_width: f64 = @floatFromInt(self.cell_size.width); - const x: usize = @intFromFloat(adjusted.x / cell_width); - - // Can be off the screen if the user drags it out, so max - // it out on our available columns - break :x @min(x, self.grid_size.columns - 1); - }, - - .y = if (adjusted.y < 0) 0 else y: { - const cell_height: f64 = @floatFromInt(self.cell_size.height); - const y: usize = @intFromFloat(adjusted.y / cell_height); - break :y @min(y, self.grid_size.rows - 1); - }, - }; + // Get our grid cell + const coord: renderer.Coordinate = .{ .surface = .{ .x = xpos, .y = ypos } }; + const grid = coord.convert(.grid, self.size).grid; + return .{ .x = grid.x, .y = grid.y }; } /// Scroll to the bottom of the viewport. @@ -3905,21 +3832,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, .scroll_page_up => { - const rows: isize = @intCast(self.grid_size.rows); + const rows: isize = @intCast(self.size.grid().rows); self.io.queueMessage(.{ .scroll_viewport = .{ .delta = -1 * rows }, }, .unlocked); }, .scroll_page_down => { - const rows: isize = @intCast(self.grid_size.rows); + const rows: isize = @intCast(self.size.grid().rows); self.io.queueMessage(.{ .scroll_viewport = .{ .delta = rows }, }, .unlocked); }, .scroll_page_fractional => |fraction| { - const rows: f32 = @floatFromInt(self.grid_size.rows); + const rows: f32 = @floatFromInt(self.size.grid().rows); const delta: isize = @intFromFloat(@trunc(fraction * rows)); self.io.queueMessage(.{ .scroll_viewport = .{ .delta = delta }, @@ -3989,7 +3916,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .left => .left, .down => .down, .up => .up, - .auto => if (self.screen_size.width > self.screen_size.height) + .auto => if (self.size.screen.width > self.size.screen.height) .right else .down, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 9f0b2a2a4..877bddf0d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1447,13 +1447,14 @@ pub const CAPI = struct { /// Return the size information a surface has. export fn ghostty_surface_size(surface: *Surface) SurfaceSize { + const grid_size = surface.core_surface.size.grid(); return .{ - .columns = surface.core_surface.grid_size.columns, - .rows = surface.core_surface.grid_size.rows, - .width_px = surface.core_surface.screen_size.width, - .height_px = surface.core_surface.screen_size.height, - .cell_width_px = surface.core_surface.cell_size.width, - .cell_height_px = surface.core_surface.cell_size.height, + .columns = grid_size.columns, + .rows = grid_size.rows, + .width_px = surface.core_surface.size.screen.width, + .height_px = surface.core_surface.size.screen.height, + .cell_width_px = surface.core_surface.size.cell.width, + .cell_height_px = surface.core_surface.size.cell.height, }; } diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig index 73252ee62..108dcd676 100644 --- a/src/apprt/gtk/ResizeOverlay.zig +++ b/src/apprt/gtk/ResizeOverlay.zig @@ -94,13 +94,14 @@ fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean { return c.FALSE; }; + const grid_size = surface.core_surface.size.grid(); var buf: [32]u8 = undefined; const text = std.fmt.bufPrintZ( &buf, "{d}c тип {d}r", .{ - surface.core_surface.grid_size.columns, - surface.core_surface.grid_size.rows, + grid_size.columns, + grid_size.rows, }, ) catch |err| { log.err("unable to format text: {}", .{err}); diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index afff83f50..25e6e60b3 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -177,29 +177,30 @@ fn beforeSend( const obj = sentry.Value.initObject(); errdefer obj.decref(); const surface = thr_state.surface; + const grid_size = surface.size.grid(); obj.set( "screen-width", - sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.width) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.width) orelse -1), ); obj.set( "screen-height", - sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.height) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.height) orelse -1), ); obj.set( "grid-columns", - sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.columns) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, grid_size.columns) orelse -1), ); obj.set( "grid-rows", - sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.rows) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, grid_size.rows) orelse -1), ); obj.set( "cell-width", - sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.width) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.width) orelse -1), ); obj.set( "cell-height", - sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.height) orelse -1), + sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.height) orelse -1), ); contexts.set("Dimensions", obj); diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 14ebadf04..7dd61c8a1 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -11,6 +11,7 @@ const cimgui = @import("cimgui"); const Surface = @import("../Surface.zig"); const font = @import("../font/main.zig"); const input = @import("../input.zig"); +const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const inspector = @import("main.zig"); @@ -641,8 +642,8 @@ fn renderSizeWindow(self: *Inspector) void { _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText( "%dpx x %dpx", - self.surface.screen_size.width, - self.surface.screen_size.height, + self.surface.size.screen.width, + self.surface.size.screen.height, ); } } @@ -656,10 +657,11 @@ fn renderSizeWindow(self: *Inspector) void { } { _ = cimgui.c.igTableSetColumnIndex(1); + const grid_size = self.surface.size.grid(); cimgui.c.igText( "%dc x %dr", - self.surface.grid_size.columns, - self.surface.grid_size.rows, + grid_size.columns, + grid_size.rows, ); } } @@ -675,8 +677,8 @@ fn renderSizeWindow(self: *Inspector) void { _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText( "%dpx x %dpx", - self.surface.cell_size.width, - self.surface.cell_size.height, + self.surface.size.cell.width, + self.surface.size.cell.height, ); } } @@ -692,10 +694,10 @@ fn renderSizeWindow(self: *Inspector) void { _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText( "T=%d B=%d L=%d R=%d px", - self.surface.padding.top, - self.surface.padding.bottom, - self.surface.padding.left, - self.surface.padding.right, + self.surface.size.padding.top, + self.surface.size.padding.bottom, + self.surface.size.padding.left, + self.surface.size.padding.right, ); } } @@ -785,7 +787,13 @@ fn renderSizeWindow(self: *Inspector) void { } { - const adjusted = self.surface.posAdjusted(self.mouse.last_xpos, self.mouse.last_ypos); + const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{ + .surface = .{ + .x = self.mouse.last_xpos, + .y = self.mouse.last_ypos, + }, + }).convert(.terminal, self.surface.size).terminal; + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); { _ = cimgui.c.igTableSetColumnIndex(0); @@ -795,8 +803,8 @@ fn renderSizeWindow(self: *Inspector) void { _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText( "(%dpx, %dpx)", - @as(i64, @intFromFloat(adjusted.x)), - @as(i64, @intFromFloat(adjusted.y)), + @as(i64, @intFromFloat(coord.x)), + @as(i64, @intFromFloat(coord.y)), ); } } diff --git a/src/renderer.zig b/src/renderer.zig index 5cf316c70..d968ab4df 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -24,6 +24,8 @@ pub const Thread = @import("renderer/Thread.zig"); pub const State = @import("renderer/State.zig"); pub const CursorStyle = cursor.Style; pub const Message = message.Message; +pub const Size = size.Size; +pub const Coordinate = size.Coordinate; pub const CellSize = size.CellSize; pub const ScreenSize = size.ScreenSize; pub const GridSize = size.GridSize; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 742dfbcd4..7b1b83b8d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -70,12 +70,8 @@ surface_mailbox: apprt.surface.Mailbox, /// Current font metrics defining our grid. grid_metrics: font.face.Metrics, -/// Current screen size dimensions for this grid. This is set on the first -/// resize event, and is not immediately available. -screen_size: ?renderer.ScreenSize, - -/// Explicit padding. -padding: renderer.Options.Padding, +/// The size of everything. +size: renderer.Size, /// True if the window is focused focused: bool, @@ -626,13 +622,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; errdefer if (display_link) |v| v.release(); - return Metal{ + var result: Metal = .{ .alloc = alloc, .config = options.config, .surface_mailbox = options.surface_mailbox, .grid_metrics = font_critical.metrics, - .screen_size = null, - .padding = options.padding, + .size = options.size, .focused = true, .foreground_color = options.config.foreground, .background_color = options.config.background, @@ -668,6 +663,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .custom_shader_state = custom_shader_state, .gpu_state = gpu_state, }; + + // Do an initialize screen size setup to ensure our undefined values + // above are initialized. + try result.setScreenSize(result.size); + + return result; } pub fn deinit(self: *Metal) void { @@ -796,19 +797,6 @@ pub fn hasVsync(self: *const Metal) bool { return display_link.isRunning(); } -/// Returns the grid size for a given screen size. This is safe to call -/// on any thread. -fn gridSize(self: *Metal) ?renderer.GridSize { - const screen_size = self.screen_size orelse return null; - return renderer.GridSize.init( - screen_size.subPadding(self.padding.explicit), - .{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }, - ); -} - /// Callback when the focus changes for the terminal this is rendering. /// /// Must be called on the render thread. @@ -878,15 +866,13 @@ pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { // // If the screen size isn't set, it will be eventually so that'll call // the setScreenSize automatically. - if (self.screen_size) |size| { - self.setScreenSize(size, self.padding.explicit) catch |err| { - // The setFontGrid function can't fail but resizing our cell - // buffer definitely can fail. If it does, our renderer is probably - // screwed but let's just log it and continue until we can figure - // out a better way to handle this. - log.err("error resizing cells buffer err={}", .{err}); - }; - } + self.setScreenSize(self.size) catch |err| { + // The setFontGrid function can't fail but resizing our cell + // buffer definitely can fail. If it does, our renderer is probably + // screwed but let's just log it and continue until we can figure + // out a better way to handle this. + log.err("error resizing cells buffer err={}", .{err}); + }; } /// Update the frame data. @@ -898,9 +884,6 @@ pub fn updateFrame( ) !void { _ = surface; - // If we don't have a screen size yet then we can't render anything. - if (self.screen_size == null) return; - // Data we extract out of the critical area. const Critical = struct { bg: terminal.color.RGB, @@ -1116,9 +1099,6 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = surface; - // If we don't have a screen size yet then we can't render anything. - if (self.screen_size == null) return; - // If we have no cells rebuilt we can usually skip drawing since there // is no changed data. However, if we have active animations we still // need to draw so that we can update the time uniform and render the @@ -1972,38 +1952,19 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { /// Resize the screen. pub fn setScreenSize( self: *Metal, - dim: renderer.ScreenSize, - pad: renderer.Padding, + size: renderer.Size, ) !void { // Store our sizes - self.screen_size = dim; - self.padding.explicit = pad; - - // Recalculate the rows/columns. This can't fail since we just set - // the screen size above. - const grid_size = self.gridSize().?; - - // Determine if we need to pad the window. For "auto" padding, we take - // the leftover amounts on the right/bottom that don't fit a full grid cell - // and we split them equal across all boundaries. - const padding = if (self.padding.balance) - renderer.Padding.balanced( - dim, - grid_size, - .{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }, - ) - else - self.padding.explicit; - const padded_dim = dim.subPadding(padding); + self.size = size; + const grid_size = size.grid(); + const terminal_size = size.terminal(); // Blank space around the grid. - const blank: renderer.Padding = dim.blankPadding(padding, grid_size, .{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }).add(padding); + const blank: renderer.Padding = size.screen.blankPadding( + size.padding, + grid_size, + size.cell, + ).add(size.padding); var padding_extend = self.uniforms.padding_extend; switch (self.config.padding_color) { @@ -2030,18 +1991,18 @@ pub fn setScreenSize( // Set the size of the drawable surface to the bounds self.layer.setProperty("drawableSize", macos.graphics.Size{ - .width = @floatFromInt(dim.width), - .height = @floatFromInt(dim.height), + .width = @floatFromInt(size.screen.width), + .height = @floatFromInt(size.screen.height), }); // Setup our uniforms const old = self.uniforms; self.uniforms = .{ .projection_matrix = math.ortho2d( - -1 * @as(f32, @floatFromInt(padding.left)), - @floatFromInt(padded_dim.width + padding.right), - @floatFromInt(padded_dim.height + padding.bottom), - -1 * @as(f32, @floatFromInt(padding.top)), + -1 * @as(f32, @floatFromInt(size.padding.left)), + @floatFromInt(terminal_size.width + size.padding.right), + @floatFromInt(terminal_size.height + size.padding.bottom), + -1 * @as(f32, @floatFromInt(size.padding.top)), ), .cell_size = .{ @floatFromInt(self.grid_metrics.cell_width), @@ -2082,8 +2043,8 @@ pub fn setScreenSize( } state.uniforms.resolution = .{ - @floatFromInt(dim.width), - @floatFromInt(dim.height), + @floatFromInt(size.screen.width), + @floatFromInt(size.screen.height), 1, }; @@ -2097,8 +2058,8 @@ pub fn setScreenSize( break :init id_init; }; desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); - desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); - desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); + desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); + desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); desc.setProperty( "usage", @intFromEnum(mtl.MTLTextureUsage.render_target) | @@ -2127,8 +2088,8 @@ pub fn setScreenSize( break :init id_init; }; desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); - desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); - desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); + desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); + desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); desc.setProperty( "usage", @intFromEnum(mtl.MTLTextureUsage.render_target) | @@ -2148,7 +2109,7 @@ pub fn setScreenSize( }; } - log.debug("screen size screen={} grid={}, cell_width={} cell_height={}", .{ dim, grid_size, self.grid_metrics.cell_width, self.grid_metrics.cell_height }); + log.debug("screen size size={}", .{size}); } /// Convert the terminal state to GPU cells stored in CPU memory. These diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 5313315b1..6749645ef 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -51,10 +51,8 @@ config: DerivedConfig, /// Current font metrics defining our grid. grid_metrics: font.face.Metrics, -/// Current screen size dimensions for this grid. This is set on the first -/// resize event, and is not immediately available. -screen_size: ?renderer.ScreenSize, -grid_size: renderer.GridSize, +/// The size of everything. +size: renderer.Size, /// The current set of cells to render. Each set of cells goes into /// a separate shader call. @@ -108,9 +106,6 @@ cursor_color: ?terminal.color.RGB, /// foreground color as the cursor color. cursor_invert: bool, -/// Padding options -padding: renderer.Options.Padding, - /// The mailbox for communicating with the window. surface_mailbox: apprt.surface.Mailbox, @@ -141,7 +136,7 @@ image_virtual: bool = false, /// Defererred OpenGL operation to update the screen size. const SetScreenSize = struct { - size: renderer.ScreenSize, + size: renderer.Size, fn apply(self: SetScreenSize, r: *OpenGL) !void { const gl_state: *GLState = if (r.gl_state) |*v| @@ -150,19 +145,8 @@ const SetScreenSize = struct { return error.OpenGLUninitialized; // Apply our padding - const grid_size = r.gridSize(self.size); - const padding = if (r.padding.balance) - renderer.Padding.balanced( - self.size, - grid_size, - .{ - .width = r.grid_metrics.cell_width, - .height = r.grid_metrics.cell_height, - }, - ) - else - r.padding.explicit; - const padded_size = self.size.subPadding(padding); + const grid_size = self.size.grid(); + const terminal_size = self.size.terminal(); // Blank space around the grid. const blank: renderer.Padding = switch (r.config.padding_color) { @@ -170,30 +154,20 @@ const SetScreenSize = struct { // clear color. .background => .{}, - .extend, .@"extend-always" => self.size.blankPadding(padding, grid_size, .{ - .width = r.grid_metrics.cell_width, - .height = r.grid_metrics.cell_height, - }).add(padding), + .extend, .@"extend-always" => self.size.screen.blankPadding( + self.size.padding, + grid_size, + self.size.cell, + ).add(self.size.padding), }; - log.debug("GL api: screen size padded={} screen={} grid={} cell={} padding={}", .{ - padded_size, - self.size, - r.gridSize(self.size), - renderer.CellSize{ - .width = r.grid_metrics.cell_width, - .height = r.grid_metrics.cell_height, - }, - r.padding.explicit, - }); - // Update our viewport for this context to be the entire window. // OpenGL works in pixels, so we have to use the pixel size. try gl.viewport( 0, 0, - @intCast(self.size.width), - @intCast(self.size.height), + @intCast(self.size.screen.width), + @intCast(self.size.screen.height), ); // Update the projection uniform within our shader @@ -206,10 +180,10 @@ const SetScreenSize = struct { // 2D orthographic projection with the full w/h math.ortho2d( - -1 * @as(f32, @floatFromInt(padding.left)), - @floatFromInt(padded_size.width + padding.right), - @floatFromInt(padded_size.height + padding.bottom), - -1 * @as(f32, @floatFromInt(padding.top)), + -1 * @as(f32, @floatFromInt(self.size.padding.left)), + @floatFromInt(terminal_size.width + self.size.padding.right), + @floatFromInt(terminal_size.height + self.size.padding.bottom), + -1 * @as(f32, @floatFromInt(self.size.padding.top)), ), ); } @@ -405,8 +379,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .cells_bg = .{}, .cells = .{}, .grid_metrics = grid.metrics, - .screen_size = null, - .grid_size = .{}, + .size = options.size, .gl_state = gl_state, .font_grid = grid, .font_shaper = shaper, @@ -417,7 +390,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .background_color = options.config.background, .cursor_color = options.config.cursor_color, .cursor_invert = options.config.cursor_invert, - .padding = options.padding, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = grid.metrics }, .deferred_config = .{}, @@ -558,9 +530,7 @@ pub fn displayRealize(self: *OpenGL) !void { self.texture_color_resized = 0; // We need to reset our uniforms - if (self.screen_size) |size| { - self.deferred_screen_size = .{ .size = size }; - } + self.deferred_screen_size = .{ .size = self.size }; self.deferred_font_size = .{ .metrics = self.grid_metrics }; self.deferred_config = .{}; } @@ -688,15 +658,9 @@ pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache; - if (self.screen_size) |size| { - // Update our grid size if we have a screen size. If we don't, its okay - // because this will get set when we get the screen size set. - self.grid_size = self.gridSize(size); - - // Update our screen size because the font grid can affect grid - // metrics which update uniforms. - self.deferred_screen_size = .{ .size = size }; - } + // Update our screen size because the font grid can affect grid + // metrics which update uniforms. + self.deferred_screen_size = .{ .size = self.size }; // Defer our GPU updates self.deferred_font_size = .{ .metrics = grid.metrics }; @@ -725,6 +689,8 @@ pub fn updateFrame( // Update all our data as tightly as possible within the mutex. var critical: Critical = critical: { + const grid_size = self.size.grid(); + state.mutex.lock(); defer state.mutex.unlock(); @@ -753,8 +719,8 @@ pub fn updateFrame( // // For some reason this doesn't seem to cause any significant issues // with flickering while resizing. '\_('-')_/' - if (self.grid_size.rows != state.terminal.rows or - self.grid_size.columns != state.terminal.cols) + if (grid_size.rows != state.terminal.rows or + grid_size.columns != state.terminal.cols) { return; } @@ -1314,7 +1280,7 @@ pub fn rebuildCells( color_palette, self.background_color, ); - } else if (y == self.grid_size.rows - 1) { + } else if (y == self.size.grid().rows - 1) { self.padding_extend_bottom = !row.neverExtendBg( color_palette, self.background_color, @@ -2115,18 +2081,6 @@ fn addGlyph( }); } -/// Returns the grid size for a given screen size. This is safe to call -/// on any thread. -fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize { - return renderer.GridSize.init( - screen_size.subPadding(self.padding.explicit), - .{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }, - ); -} - /// Update the configuration. pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { // We always redo the font shaper in case font features changed. We @@ -2164,8 +2118,7 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { /// used for the shader so that the scaling of the grid is correct. pub fn setScreenSize( self: *OpenGL, - dim: renderer.ScreenSize, - pad: renderer.Padding, + size: renderer.Size, ) !void { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); @@ -2177,22 +2130,12 @@ pub fn setScreenSize( self.cells_bg.clearAndFree(self.alloc); // Store our screen size - self.screen_size = dim; - self.padding.explicit = pad; - self.grid_size = self.gridSize(dim); - - log.debug("screen size screen={} grid={} cell={} padding={}", .{ - dim, - self.grid_size, - renderer.CellSize{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }, - self.padding.explicit, - }); + self.size = size; // Defer our OpenGL updates - self.deferred_screen_size = .{ .size = dim }; + self.deferred_screen_size = .{ .size = size }; + + log.debug("screen size size={}", .{size}); } /// Updates the font texture atlas if it is dirty. diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index 8c68affe8..e7d9b3a42 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -11,8 +11,8 @@ config: renderer.Renderer.DerivedConfig, /// The font grid that should be used along with the key for deref-ing. font_grid: *font.SharedGrid, -/// Padding options for the viewport. -padding: Padding, +/// The size of everything. +size: renderer.Size, /// The mailbox for sending the surface messages. This is only valid /// once the thread has started and should not be used outside of the thread. @@ -20,12 +20,3 @@ surface_mailbox: apprt.surface.Mailbox, /// The apprt surface. rt_surface: *apprt.Surface, - -pub const Padding = struct { - // Explicit padding options, in pixels. The surface thread is - // expected to convert points to pixels for a given DPI. - explicit: renderer.Padding, - - // Balance options - balance: bool = false, -}; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 94a1280d9..91e355480 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -371,9 +371,7 @@ fn drainMailbox(self: *Thread) !void { self.renderer.markDirty(); }, - .resize => |v| { - try self.renderer.setScreenSize(v.screen_size, v.padding); - }, + .resize => |v| try self.renderer.setScreenSize(v), .change_config => |config| { defer config.alloc.destroy(config.thread); diff --git a/src/renderer/message.zig b/src/renderer/message.zig index 18b4d916f..b420e554e 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -54,14 +54,8 @@ pub const Message = union(enum) { /// config file in response to an OSC 12 command. cursor_color: ?terminal.color.RGB, - /// Changes the screen size. - resize: struct { - /// The full screen (drawable) size. This does NOT include padding. - screen_size: renderer.ScreenSize, - - /// The explicit padding values. - padding: renderer.Padding, - }, + /// Changes the size. The screen size might change, padding, grid, etc. + resize: renderer.Size, /// The derived configuration to update the renderer with. change_config: struct { diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig index c988b62d0..2cab0940c 100644 --- a/src/renderer/opengl/custom.zig +++ b/src/renderer/opengl/custom.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const gl = @import("opengl"); -const ScreenSize = @import("../size.zig").ScreenSize; +const Size = @import("../size.zig").Size; const log = std.log.scoped(.opengl_custom); @@ -154,11 +154,11 @@ pub const State = struct { self.fbo.destroy(); } - pub fn setScreenSize(self: *State, size: ScreenSize) !void { + pub fn setScreenSize(self: *State, size: Size) !void { // Update our uniforms self.uniforms.resolution = .{ - @floatFromInt(size.width), - @floatFromInt(size.height), + @floatFromInt(size.screen.width), + @floatFromInt(size.screen.height), 1, }; try self.syncUniforms(); @@ -168,8 +168,8 @@ pub const State = struct { try texbind.image2D( 0, .rgb, - @intCast(size.width), - @intCast(size.height), + @intCast(size.screen.width), + @intCast(size.screen.height), 0, .rgb, .UnsignedByte, diff --git a/src/renderer/size.zig b/src/renderer/size.zig index add3134ba..9d7a13b54 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -5,6 +5,139 @@ const terminal = @import("../terminal/main.zig"); const log = std.log.scoped(.renderer_size); +/// All relevant sizes for a rendered terminal. These are all the sizes that +/// any functionality should need to know about the terminal in order to +/// convert between any coordinate systems. +/// +/// See the individual field type documentation for more information on each +/// field. One important note is that any pixel values should already be scaled +/// to the current DPI of the screen. If the DPI changes, the sizes should be +/// recalculated and we expect this to be done by the caller. +pub const Size = struct { + screen: ScreenSize, + cell: CellSize, + padding: Padding, + + /// Return the grid size for this size. The grid size is calculated by + /// taking the screen size, removing padding, and dividing by the cell + /// dimensions. + pub fn grid(self: Size) GridSize { + return GridSize.init(self.screen.subPadding(self.padding), self.cell); + } + + /// The size of the terminal. This is the same as the screen without + /// padding. + pub fn terminal(self: Size) ScreenSize { + return self.screen.subPadding(self.padding); + } + + /// Set the padding to be balanced around the grid. Overwrites the current + /// padding. + pub fn balancePadding(self: *Size) void { + self.padding = Padding.balanced( + self.screen, + self.grid(), + self.cell, + ); + } +}; + +/// A coordinate. This is defined as a tagged union to allow for different +/// coordinate systems to be represented. +/// +/// A coordinate is only valid within the context of a stable Size value. +/// If any of the sizes in the Size struct change, the coordinate is no +/// longer valid and must be recalculated. A conversion function is provided +/// to migrate to a new Size (which may result in failure). +/// +/// The coordinate systems are: +/// +/// * surface: (0, 0) is the top-left of the surface (with padding). Negative +/// values are allowed and are off the surface. Likewise, values greater +/// than the surface size are off the surface. Units are pixels. +/// +/// * terminal: (0, 0) is the top-left of the terminal grid. This is the +/// same as the surface but with the padding removed. Negative values and +/// values greater than the grid size are allowed and are off the terminal. +/// Units are pixels. +/// +/// * grid: (0, 0) is the top-left of the grid. Units are in cells. Negative +/// values are not allowed but values greater than the grid size are +/// possible and are off the grid. +/// +pub const Coordinate = union(enum) { + surface: Surface, + terminal: Terminal, + grid: Grid, + + pub const Tag = @typeInfo(Coordinate).Union.tag_type.?; + pub const Surface = struct { x: f64, y: f64 }; + pub const Terminal = struct { x: f64, y: f64 }; + pub const Grid = struct { x: GridSize.Unit, y: GridSize.Unit }; + + /// Convert a coordinate to a different space within the same Size. + pub fn convert(self: Coordinate, to: Tag, size: Size) Coordinate { + // Unlikely fast-path but avoid work. + if (@as(Tag, self) == to) return self; + + // To avoid the combinatorial explosion of conversion functions, we + // convert to the surface system first and then reconvert from there. + const surface = self.convertToSurface(size); + + return switch (to) { + .surface => .{ .surface = surface }, + .terminal => .{ .terminal = .{ + .x = surface.x - @as(f64, @floatFromInt(size.padding.left)), + .y = surface.y - @as(f64, @floatFromInt(size.padding.top)), + } }, + .grid => grid: { + // Get rid of the padding. + const term = (Coordinate{ .surface = surface }).convert( + .terminal, + size, + ).terminal; + + // We need our grid to clamp + const grid = size.grid(); + + // Calculate the grid position. + const cell_width: f64 = @as(f64, @floatFromInt(size.cell.width)); + const cell_height: f64 = @as(f64, @floatFromInt(size.cell.height)); + const clamped_x: f64 = @max(0, term.x); + const clamped_y: f64 = @max(0, term.y); + const col: GridSize.Unit = @intFromFloat(clamped_x / cell_width); + const row: GridSize.Unit = @intFromFloat(clamped_y / cell_height); + const clamped_col: GridSize.Unit = @min(col, grid.columns - 1); + const clamped_row: GridSize.Unit = @min(row, grid.rows - 1); + break :grid .{ .grid = .{ .x = clamped_col, .y = clamped_row } }; + }, + }; + } + + /// Convert a coordinate to the surface coordinate system. + fn convertToSurface(self: Coordinate, size: Size) Surface { + return switch (self) { + .surface => |v| v, + .terminal => |v| .{ + .x = v.x + @as(f64, @floatFromInt(size.padding.left)), + .y = v.y + @as(f64, @floatFromInt(size.padding.top)), + }, + .grid => |v| grid: { + const col: f64 = @floatFromInt(v.x); + const row: f64 = @floatFromInt(v.y); + const cell_width: f64 = @floatFromInt(size.cell.width); + const cell_height: f64 = @floatFromInt(size.cell.height); + const padding_left: f64 = @floatFromInt(size.padding.left); + const padding_top: f64 = @floatFromInt(size.padding.top); + break :grid .{ + .x = col * cell_width + padding_left, + .y = row * cell_height + padding_top, + }; + }, + }; + } +}; + /// The dimensions of a single "cell" in the terminal grid. /// /// The dimensions are dependent on the current loaded set of font glyphs. @@ -67,7 +200,7 @@ pub const ScreenSize = struct { /// The dimensions of the grid itself, in rows/columns units. pub const GridSize = struct { - const Unit = terminal.size.CellCountInt; + pub const Unit = terminal.size.CellCountInt; columns: Unit = 0, rows: Unit = 0, @@ -201,3 +334,54 @@ test "GridSize update rounding" { try testing.expectEqual(@as(GridSize.Unit, 3), grid.columns); try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows); } + +test "coordinate conversion" { + const testing = std.testing; + + // A size for testing purposes. Purposely easy to calculate numbers. + const test_size: Size = .{ + .screen = .{ + .width = 100, + .height = 100, + }, + + .cell = .{ + .width = 5, + .height = 10, + }, + + .padding = .{}, + }; + + // Each pair is a test case of [expected, actual]. We only test + // one-way conversion because conversion can be lossy due to clamping + // and so on. + const table: []const [2]Coordinate = &.{ + .{ + .{ .grid = .{ .x = 0, .y = 0 } }, + .{ .surface = .{ .x = 0, .y = 0 } }, + }, + .{ + .{ .grid = .{ .x = 1, .y = 0 } }, + .{ .surface = .{ .x = 6, .y = 0 } }, + }, + .{ + .{ .grid = .{ .x = 1, .y = 1 } }, + .{ .surface = .{ .x = 6, .y = 10 } }, + }, + .{ + .{ .grid = .{ .x = 0, .y = 0 } }, + .{ .surface = .{ .x = -10, .y = -10 } }, + }, + .{ + .{ .grid = .{ .x = test_size.grid().columns - 1, .y = test_size.grid().rows - 1 } }, + .{ .surface = .{ .x = 100_000, .y = 100_000 } }, + }, + }; + + for (table) |pair| { + const expected = pair[0]; + const actual = pair[1].convert(@as(Coordinate.Tag, expected), test_size); + try testing.expectEqual(expected, actual); + } +} diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 8014ed403..023423c95 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -8,17 +8,8 @@ const Command = @import("../Command.zig"); const Config = @import("../config.zig").Config; const termio = @import("../termio.zig"); -/// The size of the terminal grid. -grid_size: renderer.GridSize, - -/// The size of a single cell, in pixels. -cell_size: renderer.CellSize, - -/// The size of the viewport in pixels. -screen_size: renderer.ScreenSize, - -/// The padding of the viewport. -padding: renderer.Padding, +/// All size metrics for the terminal. +size: renderer.Size, /// The full app configuration. This is only available during initialization. /// The memory it points to is NOT stable after the init call so any values diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index f2cdfc770..d24a86d76 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -56,11 +56,8 @@ renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for communicating with the surface. surface_mailbox: apprt.surface.Mailbox, -/// The cached grid size whenever a resize is called. -grid_size: renderer.GridSize, - -/// The size of a single cell. Used for size reports. -cell_size: renderer.CellSize, +/// The cached size info +size: renderer.Size, /// The mailbox implementation to use. mailbox: termio.Mailbox, @@ -131,10 +128,13 @@ pub const DerivedConfig = struct { /// to run a child process. pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { // Create our terminal - var term = try terminal.Terminal.init(alloc, .{ - .cols = opts.grid_size.columns, - .rows = opts.grid_size.rows, - .max_scrollback = opts.full_config.@"scrollback-limit", + var term = try terminal.Terminal.init(alloc, opts: { + const grid_size = opts.size.grid(); + break :opts .{ + .cols = grid_size.columns, + .rows = grid_size.rows, + .max_scrollback = opts.full_config.@"scrollback-limit", + }; }); errdefer term.deinit(alloc); term.default_palette = opts.config.palette; @@ -168,14 +168,14 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { // Set our default cursor style term.screen.cursor.cursor_style = opts.config.cursor_style; + // Setup our terminal size in pixels for certain requests. + term.width_px = term.cols * opts.size.cell.width; + term.height_px = term.rows * opts.size.cell.height; + // Setup our backend. var backend = opts.backend; backend.initTerminal(&term); - // Setup our terminal size in pixels for certain requests. - term.width_px = opts.grid_size.columns * opts.cell_size.width; - term.height_px = opts.grid_size.rows * opts.cell_size.height; - // Create our stream handler. This points to memory in self so it // isn't safe to use until self.* is set. const handler: StreamHandler = handler: { @@ -191,7 +191,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, .renderer_mailbox = opts.renderer_mailbox, - .grid_size = &self.grid_size, + .size = &self.size, .terminal = &self.terminal, .osc_color_report_format = opts.config.osc_color_report_format, .enquiry_response = opts.config.enquiry_response, @@ -214,8 +214,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .renderer_wakeup = opts.renderer_wakeup, .renderer_mailbox = opts.renderer_mailbox, .surface_mailbox = opts.surface_mailbox, - .grid_size = opts.grid_size, - .cell_size = opts.cell_size, + .size = opts.size, .backend = opts.backend, .mailbox = opts.mailbox, .terminal_stream = .{ @@ -349,18 +348,12 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi pub fn resize( self: *Termio, td: *ThreadData, - grid_size: renderer.GridSize, - cell_size: renderer.CellSize, - screen_size: renderer.ScreenSize, - padding: renderer.Padding, + size: renderer.Size, ) !void { - // Update the size of our pty. - const padded_size = screen_size.subPadding(padding); - try self.backend.resize(grid_size, padded_size); + const grid_size = size.grid(); - // Update our cached grid size - self.grid_size = grid_size; - self.cell_size = cell_size; + // Update the size of our pty. + try self.backend.resize(grid_size, size.terminal()); // Enter the critical area that we want to keep small { @@ -375,8 +368,8 @@ pub fn resize( ); // Update our pixel sizes - self.terminal.width_px = self.grid_size.columns * self.cell_size.width; - self.terminal.height_px = self.grid_size.rows * self.cell_size.height; + self.terminal.width_px = grid_size.columns * self.size.cell.width; + self.terminal.height_px = grid_size.rows * self.size.cell.height; // Disable synchronized output mode so that we show changes // immediately for a resize. This is allowed by the spec. @@ -389,12 +382,7 @@ pub fn resize( } // Mail the renderer so that it can update the GPU and re-render - _ = self.renderer_mailbox.push(.{ - .resize = .{ - .screen_size = screen_size, - .padding = padding, - }, - }, .{ .forever = {} }); + _ = self.renderer_mailbox.push(.{ .resize = size }, .{ .forever = {} }); self.renderer_wakeup.notify() catch {}; } @@ -406,6 +394,8 @@ pub fn sizeReport(self: *Termio, td: *ThreadData, style: termio.Message.SizeRepo } fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !void { + const grid_size = self.size.grid(); + // 1024 bytes should be enough for size report since report // in columns and pixels. var buf: [1024]u8 = undefined; @@ -414,34 +404,34 @@ fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeRe &buf, "\x1B[48;{};{};{};{}t", .{ - self.grid_size.rows, - self.grid_size.columns, - self.grid_size.rows * self.cell_size.height, - self.grid_size.columns * self.cell_size.width, + grid_size.rows, + grid_size.columns, + grid_size.rows * self.size.cell.height, + grid_size.columns * self.size.cell.width, }, ), .csi_14_t => try std.fmt.bufPrint( &buf, "\x1b[4;{};{}t", .{ - self.grid_size.rows * self.cell_size.height, - self.grid_size.columns * self.cell_size.width, + grid_size.rows * self.size.cell.height, + grid_size.columns * self.size.cell.width, }, ), .csi_16_t => try std.fmt.bufPrint( &buf, "\x1b[6;{};{}t", .{ - self.cell_size.height, - self.cell_size.width, + self.size.cell.height, + self.size.cell.width, }, ), .csi_18_t => try std.fmt.bufPrint( &buf, "\x1b[8;{};{}t", .{ - self.grid_size.rows, - self.grid_size.columns, + grid_size.rows, + grid_size.columns, }, ), }; diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 3d316e399..d80046737 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -17,6 +17,7 @@ const builtin = @import("builtin"); const xev = @import("xev"); const crash = @import("../crash/main.zig"); const termio = @import("../termio.zig"); +const renderer = @import("../renderer.zig"); const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const Allocator = std.mem.Allocator; @@ -28,7 +29,7 @@ const Coalesce = struct { /// Not all message types are coalesced. const min_ms = 25; - resize: ?termio.Message.Resize = null, + resize: ?renderer.Size = null, }; /// The number of milliseconds before we reset the synchronized output flag @@ -324,7 +325,7 @@ fn startSynchronizedOutput(self: *Thread, cb: *CallbackData) void { ); } -fn handleResize(self: *Thread, cb: *CallbackData, resize: termio.Message.Resize) void { +fn handleResize(self: *Thread, cb: *CallbackData, resize: renderer.Size) void { self.coalesce_data.resize = resize; // If the timer is already active we just return. In the future we want @@ -380,13 +381,7 @@ fn coalesceCallback( if (cb.self.coalesce_data.resize) |v| { cb.self.coalesce_data.resize = null; - cb.io.resize( - &cb.data, - v.grid_size, - v.cell_size, - v.screen_size, - v.padding, - ) catch |err| { + cb.io.resize(&cb.data, v) catch |err| { log.warn("error during resize err={}", .{err}); }; } diff --git a/src/termio/message.zig b/src/termio/message.zig index c88a12f14..44381b228 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -16,22 +16,6 @@ pub const Message = union(enum) { /// in the future. pub const WriteReq = MessageData(u8, 38); - pub const Resize = struct { - /// The grid size for the given screen size with padding applied. - grid_size: renderer.GridSize, - - /// The updated cell size. - cell_size: renderer.CellSize, - - /// The full screen (drawable) size. This does NOT include padding. - /// This should be sent on to the renderer. - screen_size: renderer.ScreenSize, - - /// The padding, so that the terminal implementation can subtract - /// this to send to the pty. - padding: renderer.Padding, - }; - /// Purposely crash the renderer. This is used for testing and debugging. /// See the "crash" binding action. crash: void, @@ -47,7 +31,7 @@ pub const Message = union(enum) { inspector: bool, /// Resize the window. - resize: Resize, + resize: renderer.Size, /// Request a size report is sent to the pty ([in-band /// size report, mode 2048](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) and diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index dd7763334..37d176de3 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -26,7 +26,7 @@ const disable_kitty_keyboard_protocol = apprt.runtime == apprt.glfw; /// unless all of the member fields are copied. pub const StreamHandler = struct { alloc: Allocator, - grid_size: *renderer.GridSize, + size: *renderer.Size, terminal: *terminal.Terminal, /// Mailbox for data to the termio thread. @@ -611,12 +611,15 @@ pub const StreamHandler = struct { }, // Force resize back to the window size - .enable_mode_3 => self.terminal.resize( - self.alloc, - self.grid_size.columns, - self.grid_size.rows, - ) catch |err| { - log.err("error updating terminal size: {}", .{err}); + .enable_mode_3 => { + const grid_size = self.size.grid(); + self.terminal.resize( + self.alloc, + grid_size.columns, + grid_size.rows, + ) catch |err| { + log.err("error updating terminal size: {}", .{err}); + }; }, .@"132_column" => try self.terminal.deccolm(