Merge pull request #33 from mitchellh/padding-cfg

Explicit Padding Configuration
This commit is contained in:
Mitchell Hashimoto
2022-11-14 17:44:14 -08:00
committed by GitHub
11 changed files with 178 additions and 42 deletions

View File

@@ -82,6 +82,9 @@ io_thr: std.Thread,
/// The dimensions of the grid in rows and columns. /// The dimensions of the grid in rows and columns.
grid_size: renderer.GridSize, grid_size: renderer.GridSize,
/// Explicit padding due to configuration
padding: renderer.Padding,
/// The app configuration /// The app configuration
config: *const Config, config: *const Config,
@@ -259,8 +262,24 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
}); });
errdefer font_group.deinit(alloc); errdefer font_group.deinit(alloc);
// Convert our padding from points to pixels
const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72;
const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72;
const padding: renderer.Padding = .{
.top = padding_y,
.bottom = padding_y,
.right = padding_x,
.left = padding_x,
};
// Create our terminal grid with the initial window size // Create our terminal grid with the initial window size
var renderer_impl = try Renderer.init(alloc, font_group); var renderer_impl = try Renderer.init(alloc, .{
.font_group = font_group,
.padding = .{
.explicit = padding,
.balance = config.@"window-padding-balance",
},
});
errdefer renderer_impl.deinit(); errdefer renderer_impl.deinit();
renderer_impl.background = .{ renderer_impl.background = .{
.r = config.background.r, .r = config.background.r,
@@ -279,7 +298,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
.width = window_size.width, .width = window_size.width,
.height = window_size.height, .height = window_size.height,
}; };
const grid_size = renderer.GridSize.init(screen_size, renderer_impl.cell_size); const grid_size = renderer_impl.gridSize(screen_size);
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
// but is otherwise somewhat arbitrary. // but is otherwise somewhat arbitrary.
@@ -353,6 +372,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
.io_thread = io_thread, .io_thread = io_thread,
.io_thr = undefined, .io_thr = undefined,
.grid_size = grid_size, .grid_size = grid_size,
.padding = padding,
.config = config, .config = config,
.imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(),
@@ -560,13 +580,22 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
// overhead of inter-thread communication // overhead of inter-thread communication
// Recalculate our grid size // Recalculate our grid size
win.grid_size.update(screen_size, win.renderer.cell_size); win.grid_size = win.renderer.gridSize(screen_size);
if (win.grid_size.columns < 5 and (win.padding.left > 0 or win.padding.right > 0)) {
log.warn("WARNING: very small terminal grid detected with padding " ++
"set. Is your padding reasonable?", .{});
}
if (win.grid_size.rows < 2 and (win.padding.top > 0 or win.padding.bottom > 0)) {
log.warn("WARNING: very small terminal grid detected with padding " ++
"set. Is your padding reasonable?", .{});
}
// Mail the IO thread // Mail the IO thread
_ = win.io_thread.mailbox.push(.{ _ = win.io_thread.mailbox.push(.{
.resize = .{ .resize = .{
.grid_size = win.grid_size, .grid_size = win.grid_size,
.screen_size = screen_size, .screen_size = screen_size,
.padding = win.padding,
}, },
}, .{ .forever = {} }); }, .{ .forever = {} });
win.io_thread.wakeup.send() catch {}; win.io_thread.wakeup.send() catch {};

View File

@@ -138,6 +138,12 @@ fn parseIntoField(
0, 0,
), ),
u32 => try std.fmt.parseInt(
u32,
value orelse return error.ValueRequired,
0,
),
else => unreachable, else => unreachable,
}; };

View File

@@ -89,6 +89,32 @@ pub const Config = struct {
/// ///
keybind: Keybinds = .{}, keybind: Keybinds = .{},
/// Window padding. This applies padding between the terminal cells and
/// the window border. The "x" option applies to the left and right
/// padding and the "y" option is top and bottom. The value is in points,
/// meaning that it will be scaled appropriately for screen DPI.
///
/// If this value is set too large, the screen will render nothing, because
/// the grid will be completely squished by the padding. It is up to you
/// as the user to pick a reasonable value. If you pick an unreasonable
/// value, a warning will appear in the logs.
@"window-padding-x": u32 = 0,
@"window-padding-y": u32 = 0,
/// The viewport dimensions are usually not perfectly divisible by
/// the cell size. In this case, some extra padding on the end of a
/// column and the bottom of the final row may exist. If this is true,
/// then this extra padding is automatically balanced between all four
/// edges to minimize imbalance on one side. If this is false, the top
/// left grid cell will always hug the edge with zero padding other than
/// what may be specified with the other "window-padding" options.
///
/// If other "window-padding" fields are set and this is true, this will
/// still apply. The other padding is applied first and may affect how
/// many grid cells actually exist, and this is applied last in order
/// to balance the padding given a certain viewport size and grid cell size.
@"window-padding-balance": bool = true,
/// Additional configuration files to read. /// Additional configuration files to read.
@"config-file": RepeatableString = .{}, @"config-file": RepeatableString = .{},

View File

@@ -14,6 +14,7 @@ pub usingnamespace @import("renderer/message.zig");
pub usingnamespace @import("renderer/size.zig"); pub usingnamespace @import("renderer/size.zig");
pub const Metal = @import("renderer/Metal.zig"); pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig"); pub const OpenGL = @import("renderer/OpenGL.zig");
pub const Options = @import("renderer/Options.zig");
pub const Thread = @import("renderer/Thread.zig"); pub const Thread = @import("renderer/Thread.zig");
pub const State = @import("renderer/State.zig"); pub const State = @import("renderer/State.zig");

View File

@@ -33,6 +33,9 @@ alloc: std.mem.Allocator,
/// Current cell dimensions for this grid. /// Current cell dimensions for this grid.
cell_size: renderer.CellSize, cell_size: renderer.CellSize,
/// Explicit padding.
padding: renderer.Options.Padding,
/// True if the window is focused /// True if the window is focused
focused: bool, focused: bool,
@@ -130,7 +133,7 @@ pub fn windowInit(window: glfw.Window) !void {
// else up during actual initialization. // else up during actual initialization.
} }
pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// Initialize our metal stuff // Initialize our metal stuff
const device = objc.Object.fromId(MTLCreateSystemDefaultDevice()); const device = objc.Object.fromId(MTLCreateSystemDefaultDevice());
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
@@ -150,8 +153,8 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
// Doesn't matter, any normal ASCII will do we're just trying to make // Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font. // sure we use the regular font.
const metrics = metrics: { const metrics = metrics: {
const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; const index = (try options.font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try font_group.group.faceFromIndex(index); const face = try options.font_group.group.faceFromIndex(index);
break :metrics face.metrics; break :metrics face.metrics;
}; };
log.debug("cell dimensions={}", .{metrics}); log.debug("cell dimensions={}", .{metrics});
@@ -200,12 +203,13 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
// Initialize our shader (MTLLibrary) // Initialize our shader (MTLLibrary)
const library = try initLibrary(device, @embedFile("../shaders/cell.metal")); const library = try initLibrary(device, @embedFile("../shaders/cell.metal"));
const pipeline_state = try initPipelineState(device, library); const pipeline_state = try initPipelineState(device, library);
const texture_greyscale = try initAtlasTexture(device, &font_group.atlas_greyscale); const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
const texture_color = try initAtlasTexture(device, &font_group.atlas_color); const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
return Metal{ return Metal{
.alloc = alloc, .alloc = alloc,
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.padding = options.padding,
.background = .{ .r = 0, .g = 0, .b = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 },
.foreground = .{ .r = 255, .g = 255, .b = 255 }, .foreground = .{ .r = 255, .g = 255, .b = 255 },
.focused = true, .focused = true,
@@ -224,7 +228,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
}, },
// Fonts // Fonts
.font_group = font_group, .font_group = options.font_group,
.font_shaper = font_shaper, .font_shaper = font_shaper,
// Metal stuff // Metal stuff
@@ -302,6 +306,15 @@ pub fn threadExit(self: *const Metal) void {
// Metal requires no per-thread state. // Metal requires no per-thread state.
} }
/// Returns the grid size for a given screen size. This is safe to call
/// on any thread.
pub fn gridSize(self: *Metal, screen_size: renderer.ScreenSize) renderer.GridSize {
return renderer.GridSize.init(
screen_size.subPadding(self.padding.explicit),
self.cell_size,
);
}
/// Callback when the focus changes for the terminal this is rendering. /// Callback when the focus changes for the terminal this is rendering.
pub fn setFocus(self: *Metal, focus: bool) !void { pub fn setFocus(self: *Metal, focus: bool) !void {
self.focused = focus; self.focused = focus;
@@ -562,12 +575,14 @@ fn setScreenSize(self: *Metal, bounds: macos.graphics.Size) !void {
}; };
// Recalculate the rows/columns. // Recalculate the rows/columns.
const grid_size = renderer.GridSize.init(dim, self.cell_size); const grid_size = self.gridSize(dim);
// Determine if we need to pad the window. For "auto" padding, we take // 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 // the leftover amounts on the right/bottom that don't fit a full grid cell
// and we split them equal across all boundaries. // and we split them equal across all boundaries.
const padding = renderer.Padding.balanced(dim, grid_size, self.cell_size); const padding = self.padding.explicit.add(if (self.padding.balance)
renderer.Padding.balanced(dim, grid_size, self.cell_size)
else .{});
const padded_dim = dim.subPadding(padding); const padded_dim = dim.subPadding(padding);
// Update our shaper // Update our shaper
@@ -585,8 +600,8 @@ fn setScreenSize(self: *Metal, bounds: macos.graphics.Size) !void {
self.uniforms = .{ self.uniforms = .{
.projection_matrix = math.ortho2d( .projection_matrix = math.ortho2d(
-1 * padding.left, -1 * padding.left,
@intToFloat(f32, padded_dim.width), @intToFloat(f32, padded_dim.width) + padding.right,
@intToFloat(f32, padded_dim.height), @intToFloat(f32, padded_dim.height) + padding.bottom,
-1 * padding.top, -1 * padding.top,
), ),
.cell_size = .{ self.cell_size.width, self.cell_size.height }, .cell_size = .{ self.cell_size.width, self.cell_size.height },

View File

@@ -77,6 +77,9 @@ background: terminal.color.RGB,
/// True if the window is focused /// True if the window is focused
focused: bool, focused: bool,
/// Padding options
padding: renderer.Options.Padding,
/// The raw structure that maps directly to the buffer sent to the vertex shader. /// The raw structure that maps directly to the buffer sent to the vertex shader.
/// This must be "extern" so that the field order is not reordered by the /// This must be "extern" so that the field order is not reordered by the
/// Zig compiler. /// Zig compiler.
@@ -146,7 +149,7 @@ const GPUCellMode = enum(u8) {
} }
}; };
pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL { pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
// Create the initial font shaper // Create the initial font shaper
var shape_buf = try alloc.alloc(font.Shaper.Cell, 1); var shape_buf = try alloc.alloc(font.Shaper.Cell, 1);
errdefer alloc.free(shape_buf); errdefer alloc.free(shape_buf);
@@ -157,8 +160,8 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
// Doesn't matter, any normal ASCII will do we're just trying to make // Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font. // sure we use the regular font.
const metrics = metrics: { const metrics = metrics: {
const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; const index = (try options.font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try font_group.group.faceFromIndex(index); const face = try options.font_group.group.faceFromIndex(index);
break :metrics face.metrics; break :metrics face.metrics;
}; };
log.debug("cell dimensions={}", .{metrics}); log.debug("cell dimensions={}", .{metrics});
@@ -248,12 +251,12 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
try texbind.image2D( try texbind.image2D(
0, 0,
.Red, .Red,
@intCast(c_int, font_group.atlas_greyscale.size), @intCast(c_int, options.font_group.atlas_greyscale.size),
@intCast(c_int, font_group.atlas_greyscale.size), @intCast(c_int, options.font_group.atlas_greyscale.size),
0, 0,
.Red, .Red,
.UnsignedByte, .UnsignedByte,
font_group.atlas_greyscale.data.ptr, options.font_group.atlas_greyscale.data.ptr,
); );
} }
@@ -269,12 +272,12 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
try texbind.image2D( try texbind.image2D(
0, 0,
.RGBA, .RGBA,
@intCast(c_int, font_group.atlas_color.size), @intCast(c_int, options.font_group.atlas_color.size),
@intCast(c_int, font_group.atlas_color.size), @intCast(c_int, options.font_group.atlas_color.size),
0, 0,
.BGRA, .BGRA,
.UnsignedByte, .UnsignedByte,
font_group.atlas_color.data.ptr, options.font_group.atlas_color.data.ptr,
); );
} }
@@ -289,13 +292,14 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
.vbo = vbo, .vbo = vbo,
.texture = tex, .texture = tex,
.texture_color = tex_color, .texture_color = tex_color,
.font_group = font_group, .font_group = options.font_group,
.font_shaper = shaper, .font_shaper = shaper,
.cursor_visible = true, .cursor_visible = true,
.cursor_style = .box, .cursor_style = .box,
.background = .{ .r = 0, .g = 0, .b = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 },
.foreground = .{ .r = 255, .g = 255, .b = 255 }, .foreground = .{ .r = 255, .g = 255, .b = 255 },
.focused = true, .focused = true,
.padding = options.padding,
}; };
} }
@@ -943,19 +947,34 @@ pub fn updateCell(
return true; return true;
} }
/// Returns the grid size for a given screen size. This is safe to call
/// on any thread.
pub fn gridSize(self: *OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize {
return renderer.GridSize.init(
screen_size.subPadding(self.padding.explicit),
self.cell_size,
);
}
/// Set the screen size for rendering. This will update the projection /// Set the screen size for rendering. This will update the projection
/// used for the shader so that the scaling of the grid is correct. /// used for the shader so that the scaling of the grid is correct.
fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void {
// Recalculate the rows/columns. // Recalculate the rows/columns.
const grid_size = renderer.GridSize.init(dim, self.cell_size); const grid_size = self.gridSize(dim);
// Determine if we need to pad the window. For "auto" padding, we take // Apply our padding
// the leftover amounts on the right/bottom that don't fit a full grid cell const padding = self.padding.explicit.add(if (self.padding.balance)
// and we split them equal across all boundaries. renderer.Padding.balanced(dim, grid_size, self.cell_size)
const padding = renderer.Padding.balanced(dim, grid_size, self.cell_size); else .{});
const padded_dim = dim.subPadding(padding); const padded_dim = dim.subPadding(padding);
log.debug("screen size padded={} screen={} grid={} cell={}", .{ padded_dim, dim, grid_size, self.cell_size }); log.debug("screen size padded={} screen={} grid={} cell={} padding={}", .{
padded_dim,
dim,
grid_size,
self.cell_size,
self.padding.explicit,
});
// Update our LRU. We arbitrarily support a certain number of pages here. // Update our LRU. We arbitrarily support a certain number of pages here.
// We also always support a minimum number of caching in case a user // We also always support a minimum number of caching in case a user
@@ -991,8 +1010,8 @@ fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void {
// 2D orthographic projection with the full w/h // 2D orthographic projection with the full w/h
math.ortho2d( math.ortho2d(
-1 * padding.left, -1 * padding.left,
@intToFloat(f32, padded_dim.width), @intToFloat(f32, padded_dim.width) + padding.right,
@intToFloat(f32, padded_dim.height), @intToFloat(f32, padded_dim.height) + padding.bottom,
-1 * padding.top, -1 * padding.top,
), ),
); );

19
src/renderer/Options.zig Normal file
View File

@@ -0,0 +1,19 @@
//! The options that are used to configure a renderer.
const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig");
/// The font group that should be used.
font_group: *font.GroupCache,
/// Padding options for the viewport.
padding: Padding,
pub const Padding = struct {
// Explicit padding options, in pixels. The windowing thread is
// expected to convert points to pixels for a given DPI.
explicit: renderer.Padding,
// Balance options
balance: bool = false,
};

View File

@@ -48,8 +48,8 @@ pub const ScreenSize = struct {
/// Subtract padding from the screen size. /// Subtract padding from the screen size.
pub fn subPadding(self: ScreenSize, padding: Padding) ScreenSize { pub fn subPadding(self: ScreenSize, padding: Padding) ScreenSize {
return .{ return .{
.width = self.width - @floatToInt(u32, padding.left + padding.right), .width = self.width -| @floatToInt(u32, padding.left + padding.right),
.height = self.height - @floatToInt(u32, padding.top + padding.bottom), .height = self.height -| @floatToInt(u32, padding.top + padding.bottom),
}; };
} }
}; };
@@ -71,17 +71,17 @@ pub const GridSize = struct {
/// Update the columns/rows for the grid based on the given screen and /// Update the columns/rows for the grid based on the given screen and
/// cell size. /// cell size.
pub fn update(self: *GridSize, screen: ScreenSize, cell: CellSize) void { pub fn update(self: *GridSize, screen: ScreenSize, cell: CellSize) void {
self.columns = @floatToInt(Unit, @intToFloat(f32, screen.width) / cell.width); self.columns = @max(1, @floatToInt(Unit, @intToFloat(f32, screen.width) / cell.width));
self.rows = @floatToInt(Unit, @intToFloat(f32, screen.height) / cell.height); self.rows = @max(1, @floatToInt(Unit, @intToFloat(f32, screen.height) / cell.height));
} }
}; };
/// The padding to add to a screen. /// The padding to add to a screen.
pub const Padding = struct { pub const Padding = struct {
top: f32, top: f32 = 0,
bottom: f32, bottom: f32 = 0,
right: f32, right: f32 = 0,
left: f32, left: f32 = 0,
/// Returns padding that balances the whitespace around the screen /// Returns padding that balances the whitespace around the screen
/// for the given grid and cell sizes. /// for the given grid and cell sizes.
@@ -112,6 +112,16 @@ pub const Padding = struct {
.left = padding_left, .left = padding_left,
}; };
} }
/// Add another padding to ths one
pub fn add(self: Padding, other: Padding) Padding {
return .{
.top = self.top + other.top,
.bottom = self.bottom + other.bottom,
.right = self.right + other.right,
.left = self.left + other.left,
};
}
}; };
test "GridSize update exact" { test "GridSize update exact" {

View File

@@ -231,13 +231,16 @@ pub fn resize(
self: *Exec, self: *Exec,
grid_size: renderer.GridSize, grid_size: renderer.GridSize,
screen_size: renderer.ScreenSize, screen_size: renderer.ScreenSize,
padding: renderer.Padding,
) !void { ) !void {
const padded_size = screen_size.subPadding(padding);
// Update the size of our pty // Update the size of our pty
try self.pty.setSize(.{ try self.pty.setSize(.{
.ws_row = @intCast(u16, grid_size.rows), .ws_row = @intCast(u16, grid_size.rows),
.ws_col = @intCast(u16, grid_size.columns), .ws_col = @intCast(u16, grid_size.columns),
.ws_xpixel = @intCast(u16, screen_size.width), .ws_xpixel = @intCast(u16, padded_size.width),
.ws_ypixel = @intCast(u16, screen_size.height), .ws_ypixel = @intCast(u16, padded_size.height),
}); });
// Update our cached grid size // Update our cached grid size

View File

@@ -172,7 +172,7 @@ fn drainMailbox(self: *Thread) !void {
log.debug("mailbox message={}", .{message}); log.debug("mailbox message={}", .{message});
switch (message) { switch (message) {
.resize => |v| try self.impl.resize(v.grid_size, v.screen_size), .resize => |v| try self.impl.resize(v.grid_size, v.screen_size, v.padding),
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]),
.write_stable => |v| try self.impl.queueWrite(v), .write_stable => |v| try self.impl.queueWrite(v),
.write_alloc => |v| { .write_alloc => |v| {

View File

@@ -12,8 +12,16 @@ const terminal = @import("../terminal/main.zig");
pub const Message = union(enum) { pub const Message = union(enum) {
/// Resize the window. /// Resize the window.
resize: struct { resize: struct {
/// The grid size for the given screen size with padding applied.
grid_size: renderer.GridSize, grid_size: renderer.GridSize,
/// The full screen (drawable) size. This does NOT include padding.
/// This should be sent on to the renderer.
screen_size: renderer.ScreenSize, screen_size: renderer.ScreenSize,
/// The padding, so that the terminal implementation can subtract
/// this to send to the pty.
padding: renderer.Padding,
}, },
/// Write where the data fits in the union. /// Write where the data fits in the union.