From d759c7fb254e6b71d3cd0ac8b1e3b952edc0c96c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 15:37:16 -0800 Subject: [PATCH 1/9] font: freetype face supports resize --- src/font/face/freetype.zig | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 1e02ce5b5..4c726b16f 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -71,6 +71,13 @@ pub const Face = struct { self.* = undefined; } + /// Resize the font in-place. If this succeeds, the caller is responsible + /// for clearing any glyph caches, font atlas data, etc. + pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { + try setSize_(self.face, size); + self.metrics = calcMetrics(self.face); + } + fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { // If we have fixed sizes, we just have to try to pick the one closest // to what the user requested. Otherwise, we can choose an arbitrary @@ -456,7 +463,6 @@ test { var ft_font = try Face.init(lib, testFont, .{ .points = 12 }); defer ft_font.deinit(); - log.warn("FT={}", .{ft_font.metrics}); try testing.expectEqual(Presentation.text, ft_font.presentation); @@ -465,6 +471,16 @@ test { while (i < 127) : (i += 1) { _ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, null); } + + // Test resizing + { + const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null); + try testing.expectEqual(@as(u32, 11), g1.height); + + try ft_font.setSize(.{ .points = 24 }); + const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null); + try testing.expectEqual(@as(u32, 21), g2.height); + } } test "color emoji" { @@ -491,6 +507,42 @@ test "color emoji" { } } +test "metrics" { + const testFont = @import("../test.zig").fontRegular; + const alloc = testing.allocator; + + var lib = try Library.init(); + defer lib.deinit(); + + var atlas = try Atlas.init(alloc, 512, .greyscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .points = 12 }); + defer ft_font.deinit(); + + try testing.expectEqual(font.face.Metrics{ + .cell_width = 8, + .cell_height = 1.8e1, + .cell_baseline = 4, + .underline_position = 18, + .underline_thickness = 1, + .strikethrough_position = 10, + .strikethrough_thickness = 1, + }, ft_font.metrics); + + // Resize should change metrics + try ft_font.setSize(.{ .points = 24 }); + try testing.expectEqual(font.face.Metrics{ + .cell_width = 16, + .cell_height = 35, + .cell_baseline = 7, + .underline_position = 36, + .underline_thickness = 2, + .strikethrough_position = 20, + .strikethrough_thickness = 2, + }, ft_font.metrics); +} + test "mono to rgba" { const alloc = testing.allocator; const testFont = @import("../test.zig").fontEmoji; From 24167d0d59554e3215a3f3a2c7b1f20766f47337 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 15:48:52 -0800 Subject: [PATCH 2/9] font: Group supports resize --- src/font/Group.zig | 71 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/font/Group.zig b/src/font/Group.zig index 41abd35eb..abacf385c 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -74,6 +74,25 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) try self.faces.getPtr(style).append(alloc, face); } +/// Resize the fonts to the desired size. +pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { + // Note: there are some issues here with partial failure. We don't + // currently handle it in any meaningful way if one face can resize + // but another can't. + + // Resize all our faces that are loaded + var it = self.faces.iterator(); + while (it.next()) |entry| { + for (entry.value.items) |*deferred| { + if (!deferred.loaded()) continue; + try deferred.face.?.setSize(size); + } + } + + // Set our size for future loads + self.size = size; +} + /// This represents a specific font in the group. pub const FontIndex = packed struct { /// The number of bits we use for the index. @@ -226,7 +245,57 @@ test { } } -test { +test "resize" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + var lib = try Library.init(); + defer lib.deinit(); + + var group = try init(alloc, lib, .{ .points = 12 }); + defer group.deinit(alloc); + + try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 }))); + + // Load a letter + { + const idx = group.indexForCodepoint('A', .regular, null).?; + const face = try group.faceFromIndex(idx); + const glyph_index = face.glyphIndex('A').?; + const glyph = try group.renderGlyph( + alloc, + &atlas_greyscale, + idx, + glyph_index, + null, + ); + + try testing.expectEqual(@as(u32, 11), glyph.height); + } + + // Resize + try group.setSize(.{ .points = 24 }); + { + const idx = group.indexForCodepoint('A', .regular, null).?; + const face = try group.faceFromIndex(idx); + const glyph_index = face.glyphIndex('A').?; + const glyph = try group.renderGlyph( + alloc, + &atlas_greyscale, + idx, + glyph_index, + null, + ); + + try testing.expectEqual(@as(u32, 21), glyph.height); + } +} + +test "discover monospace with fontconfig and freetype" { if (options.backend != .fontconfig_freetype) return error.SkipZigTest; const testing = std.testing; From 6218792710f7d1d09772b8caaaccd582b2603594 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 15:54:57 -0800 Subject: [PATCH 3/9] font: GroupCache supports resize --- src/font/GroupCache.zig | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 96a95cf1d..d4c627807 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -6,6 +6,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Atlas = @import("../Atlas.zig"); +const font = @import("main.zig"); const Face = @import("main.zig").Face; const DeferredFace = @import("main.zig").DeferredFace; const Library = @import("main.zig").Library; @@ -83,6 +84,18 @@ pub fn reset(self: *GroupCache) void { self.glyphs.clearRetainingCapacity(); } +/// Resize the fonts in the group. This will clear the cache. +pub fn setSize(self: *GroupCache, size: font.face.DesiredSize) !void { + try self.group.setSize(size); + + // Reset our internal state + self.reset(); + + // Clear our atlases + self.atlas_greyscale.clear(); + self.atlas_color.clear(); +} + /// Get the font index for a given codepoint. This is cached. pub fn indexForCodepoint( self: *GroupCache, @@ -216,3 +229,61 @@ test { } } } + +test "resize" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + // const testEmoji = @import("test.zig").fontEmoji; + + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + var lib = try Library.init(); + defer lib.deinit(); + + var cache = try init(alloc, try Group.init( + alloc, + lib, + .{ .points = 12 }, + )); + defer cache.deinit(alloc); + + // Setup group + try cache.group.addFace( + alloc, + .regular, + DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })), + ); + + // Load a letter + { + const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?; + const face = try cache.group.faceFromIndex(idx); + const glyph_index = face.glyphIndex('A').?; + const glyph = try cache.renderGlyph( + alloc, + idx, + glyph_index, + null, + ); + + try testing.expectEqual(@as(u32, 11), glyph.height); + } + + // Resize + try cache.setSize(.{ .points = 24 }); + { + const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?; + const face = try cache.group.faceFromIndex(idx); + const glyph_index = face.glyphIndex('A').?; + const glyph = try cache.renderGlyph( + alloc, + idx, + glyph_index, + null, + ); + + try testing.expectEqual(@as(u32, 21), glyph.height); + } +} From 6ec5684c2743d65449bd3f0957e7dcdf69520fad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 19:30:15 -0800 Subject: [PATCH 4/9] window caches all sizing so it doesn't depend on renderer state --- src/Window.zig | 45 ++++++++++++++++++++++++++-------------- src/renderer/Options.zig | 5 +++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index ad1b9b836..23f2a563c 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -79,8 +79,10 @@ io: termio.Impl, io_thread: termio.Thread, io_thr: std.Thread, -/// The dimensions of the grid in rows and columns. +/// 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, @@ -262,6 +264,9 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { }); errdefer font_group.deinit(alloc); + // Pre-calculate our initial cell size ourselves. + const cell_size = try renderer.CellSize.init(alloc, font_group); + // 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; @@ -279,6 +284,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .explicit = padding, .balance = config.@"window-padding-balance", }, + .window_mailbox = .{ .window = self, .app = app.mailbox }, }); errdefer renderer_impl.deinit(); renderer_impl.background = .{ @@ -298,13 +304,16 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .width = window_size.width, .height = window_size.height, }; - const grid_size = renderer_impl.gridSize(screen_size); + const grid_size = renderer.GridSize.init( + screen_size.subPadding(padding), + cell_size, + ); // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // but is otherwise somewhat arbitrary. try window.setSizeLimits(.{ - .width = @floatToInt(u32, renderer_impl.cell_size.width * 10), - .height = @floatToInt(u32, renderer_impl.cell_size.height * 4), + .width = @floatToInt(u32, cell_size.width * 10), + .height = @floatToInt(u32, cell_size.height * 4), }, .{ .width = null, .height = null }); // Create the cursor @@ -371,7 +380,9 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .io = io, .io_thread = io_thread, .io_thr = undefined, + .screen_size = screen_size, .grid_size = grid_size, + .cell_size = cell_size, .padding = padding, .config = config, @@ -569,18 +580,22 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { }; }; - const screen_size: renderer.ScreenSize = .{ - .width = px_size.width, - .height = px_size.height, - }; - const win = window.getUserPointer(Window) orelse return; // TODO: if our screen size didn't change, then we should avoid the // overhead of inter-thread communication + // Save our screen size + win.screen_size = .{ + .width = px_size.width, + .height = px_size.height, + }; + // Recalculate our grid size - win.grid_size = win.renderer.gridSize(screen_size); + win.grid_size = renderer.GridSize.init( + win.screen_size.subPadding(win.padding), + win.cell_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?", .{}); @@ -594,7 +609,7 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { _ = win.io_thread.mailbox.push(.{ .resize = .{ .grid_size = win.grid_size, - .screen_size = screen_size, + .screen_size = win.screen_size, .padding = win.padding, }, }, .{ .forever = {} }); @@ -1386,10 +1401,10 @@ fn cursorPosCallback( // // the boundary point at which we consider selection or non-selection - const cell_xboundary = win.renderer.cell_size.width * 0.6; + const cell_xboundary = win.cell_size.width * 0.6; // first xpos of the clicked cell - const cell_xstart = @intToFloat(f32, win.mouse.left_click_point.x) * win.renderer.cell_size.width; + const cell_xstart = @intToFloat(f32, win.mouse.left_click_point.x) * win.cell_size.width; const cell_start_xpos = win.mouse.left_click_xpos - cell_xstart; // If this is the same cell, then we only start the selection if weve @@ -1464,7 +1479,7 @@ fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { return .{ .x = if (xpos < 0) 0 else x: { // Our cell is the mouse divided by cell width - const cell_width = @floatCast(f64, self.renderer.cell_size.width); + const cell_width = @floatCast(f64, self.cell_size.width); const x = @floatToInt(usize, xpos / cell_width); // Can be off the screen if the user drags it out, so max @@ -1473,7 +1488,7 @@ fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { }, .y = if (ypos < 0) 0 else y: { - const cell_height = @floatCast(f64, self.renderer.cell_size.height); + const cell_height = @floatCast(f64, self.cell_size.height); const y = @floatToInt(usize, ypos / cell_height); break :y @min(y, self.io.terminal.rows - 1); }, diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index c313d91c6..38789f1c7 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -2,6 +2,7 @@ const font = @import("../font/main.zig"); const renderer = @import("../renderer.zig"); +const Window = @import("../Window.zig"); /// The font group that should be used. font_group: *font.GroupCache, @@ -9,6 +10,10 @@ font_group: *font.GroupCache, /// Padding options for the viewport. padding: Padding, +/// The mailbox for sending the window messages. This is only valid +/// once the thread has started and should not be used outside of the thread. +window_mailbox: Window.Mailbox, + pub const Padding = struct { // Explicit padding options, in pixels. The windowing thread is // expected to convert points to pixels for a given DPI. From 657c8540c845b7843b88195b0c303d0bf4d20b4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 19:48:32 -0800 Subject: [PATCH 5/9] renderer: font size changed event, OpenGL impl --- src/renderer/OpenGL.zig | 94 +++++++++++++++++++++++++++++++--------- src/renderer/Thread.zig | 4 ++ src/renderer/message.zig | 6 +++ src/window/message.zig | 1 + 4 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 35f0228c2..b56f9825a 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -156,22 +156,15 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { var shaper = try font.Shaper.init(shape_buf); errdefer shaper.deinit(); - // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? - // Doesn't matter, any normal ASCII will do we're just trying to make - // sure we use the regular font. - const metrics = metrics: { - const index = (try options.font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; - const face = try options.font_group.group.faceFromIndex(index); - break :metrics face.metrics; - }; - log.debug("cell dimensions={}", .{metrics}); - // Create our shader const program = try gl.Program.createVF( @embedFile("shaders/cell.v.glsl"), @embedFile("shaders/cell.f.glsl"), ); + // Setup our font metrics uniform + const metrics = try resetFontMetrics(alloc, program, options.font_group); + // Set our cell dimensions const pbind = try program.use(); defer pbind.unbind(); @@ -314,21 +307,26 @@ pub fn deinit(self: *OpenGL) void { self.vao.destroy(); self.program.destroy(); - { - // Our LRU values are array lists so we need to deallocate those first - var it = self.cells_lru.queue.first; - while (it) |node| { - it = node.next; - node.data.value.deinit(self.alloc); - } - - self.cells_lru.deinit(self.alloc); - } + self.resetCellsLRU(); + self.cells_lru.deinit(self.alloc); self.cells.deinit(self.alloc); self.* = undefined; } +fn resetCellsLRU(self: *OpenGL) void { + // Our LRU values are array lists so we need to deallocate those first + var it = self.cells_lru.queue.first; + while (it) |node| { + it = node.next; + node.data.value.deinit(self.alloc); + } + self.cells_lru.deinit(self.alloc); + + // Initialize our new LRU + self.cells_lru = CellsLRU.init(0); +} + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -448,15 +446,69 @@ pub fn threadExit(self: *const OpenGL) void { } /// Callback when the focus changes for the terminal this is rendering. +/// +/// Must be called on the render thread. pub fn setFocus(self: *OpenGL, focus: bool) !void { self.focused = focus; } /// Called to toggle the blink state of the cursor +/// +/// Must be called on the render thread. pub fn blinkCursor(self: *OpenGL, reset: bool) void { self.cursor_visible = reset or !self.cursor_visible; } +/// Set the new font size. +/// +/// Must be called on the render thread. +pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { + // Set our new size, this will also reset our font atlas. + try self.font_group.setSize(size); + + // Invalidate our cell cache. + self.resetCellsLRU(); + + // Reset our GPU uniforms + const metrics = try resetFontMetrics(self.alloc, self.program, self.font_group); + + // Recalculate our cell size. If it is the same as before, then we do + // nothing since the grid size couldn't have possibly changed. + const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }; + if (std.meta.eql(self.cell_size, new_cell_size)) return; + + // Notify the window that the cell size changed. +} + +/// Reload the font metrics, recalculate cell size, and send that all +/// down to the GPU. +fn resetFontMetrics( + alloc: Allocator, + program: gl.Program, + font_group: *font.GroupCache, +) !font.face.Metrics { + // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? + // Doesn't matter, any normal ASCII will do we're just trying to make + // sure we use the regular font. + const metrics = metrics: { + const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; + const face = try font_group.group.faceFromIndex(index); + break :metrics face.metrics; + }; + log.debug("cell dimensions={}", .{metrics}); + + // Set our uniforms that rely on metrics + const pbind = try program.use(); + defer pbind.unbind(); + try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height }); + try program.setUniform("underline_position", metrics.underline_position); + try program.setUniform("underline_thickness", metrics.underline_thickness); + try program.setUniform("strikethrough_position", metrics.strikethrough_position); + try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness); + + return metrics; +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, @@ -946,7 +998,7 @@ pub fn updateCell( /// 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 { +fn gridSize(self: *OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize { return renderer.GridSize.init( screen_size.subPadding(self.padding.explicit), self.cell_size, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index a8ad1f667..87a949a43 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -275,6 +275,10 @@ fn drainMailbox(self: *Thread) !void { _ = try self.cursor_h.again(); } }, + + .font_size => |size| { + try self.renderer.setFontSize(size); + }, } } } diff --git a/src/renderer/message.zig b/src/renderer/message.zig index 87cbc9f4e..02c9ddefc 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -1,6 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const font = @import("../font/main.zig"); /// The messages that can be sent to a renderer thread. pub const Message = union(enum) { @@ -12,4 +13,9 @@ pub const Message = union(enum) { /// Reset the cursor blink by immediately showing the cursor then /// restarting the timer. reset_cursor_blink: void, + + /// Change the font size. This should recalculate the grid size and + /// send a grid size change message back to the window thread if + /// the size changes. + font_size: font.face.DesiredSize, }; diff --git a/src/window/message.zig b/src/window/message.zig index 8dd814092..9f74f4e60 100644 --- a/src/window/message.zig +++ b/src/window/message.zig @@ -1,5 +1,6 @@ const App = @import("../App.zig"); const Window = @import("../Window.zig"); +const renderer = @import("../renderer.zig"); /// The message types that can be sent to a single window. pub const Message = union(enum) { From 3ce554462a65739c635f9f9bd23dbb19f2ec1382 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 19:54:05 -0800 Subject: [PATCH 6/9] window cell size event for changing cell size --- src/Window.zig | 25 +++++++++++++++++++++++++ src/renderer/OpenGL.zig | 9 +++++++++ src/window/message.zig | 3 +++ 3 files changed, 37 insertions(+) diff --git a/src/Window.zig b/src/Window.zig index 23f2a563c..640270956 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -527,9 +527,34 @@ pub fn handleMessage(self: *Window, msg: Message) !void { log.debug("changing title \"{s}\"", .{slice}); try self.window.setTitle(slice.ptr); }, + + .cell_size => |size| try self.setCellSize(size), } } +/// Change the cell size for the terminal grid. This can happen as +/// a result of changing the font size at runtime. +fn setCellSize(self: *Window, 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, + ); + + // Notify the terminal + _ = self.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, + }, + }, .{ .forever = {} }); + self.io_thread.wakeup.send() catch {}; +} + /// This queues a render operation with the renderer thread. The render /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index b56f9825a..a33c911b7 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -18,6 +18,7 @@ const trace = @import("tracy").trace; const math = @import("../math.zig"); const lru = @import("../lru.zig"); const DevMode = @import("../DevMode.zig"); +const Window = @import("../Window.zig"); const log = std.log.scoped(.grid); @@ -80,6 +81,9 @@ focused: bool, /// Padding options padding: renderer.Options.Padding, +/// The mailbox for communicating with the window. +window_mailbox: Window.Mailbox, + /// 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 /// Zig compiler. @@ -293,6 +297,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .foreground = .{ .r = 255, .g = 255, .b = 255 }, .focused = true, .padding = options.padding, + .window_mailbox = options.window_mailbox, }; } @@ -476,8 +481,12 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { // nothing since the grid size couldn't have possibly changed. const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }; if (std.meta.eql(self.cell_size, new_cell_size)) return; + self.cell_size = new_cell_size; // Notify the window that the cell size changed. + _ = self.window_mailbox.push(.{ + .cell_size = new_cell_size, + }, .{ .forever = {} }); } /// Reload the font metrics, recalculate cell size, and send that all diff --git a/src/window/message.zig b/src/window/message.zig index 9f74f4e60..ae14ffcdf 100644 --- a/src/window/message.zig +++ b/src/window/message.zig @@ -9,6 +9,9 @@ pub const Message = union(enum) { /// the termio message so that we can more efficiently send strings /// of any length set_title: [256]u8, + + /// Change the cell size. + cell_size: renderer.CellSize, }; /// A window mailbox. From dad49239015c3793e409e637016af01774eaa4a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 20:10:50 -0800 Subject: [PATCH 7/9] hook up all the keyboard actions --- src/Window.zig | 52 +++++++++++++++++++++++++++++++++++++++++ src/config.zig | 17 ++++++++++++++ src/input/Binding.zig | 7 ++++++ src/input/key.zig | 14 +++++++++++ src/renderer/OpenGL.zig | 7 +++++- 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/Window.zig b/src/Window.zig index 640270956..7e26fad68 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -49,6 +49,7 @@ app: *App, /// The font structures font_lib: font.Library, font_group: *font.GroupCache, +font_size: font.face.DesiredSize, /// The glfw window handle. window: glfw.Window, @@ -361,6 +362,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .app = app, .font_lib = font_lib, .font_group = font_group, + .font_size = font_size, .window = window, .cursor = cursor, .renderer = renderer_impl, @@ -555,6 +557,20 @@ fn setCellSize(self: *Window, size: renderer.CellSize) !void { self.io_thread.wakeup.send() catch {}; } +/// Change the font size. +fn setFontSize(self: *Window, size: font.face.DesiredSize) void { + // Update our font size so future changes work + self.font_size = size; + + // Notify our render thread of the font size. This triggers everything else. + _ = self.renderer_thread.mailbox.push(.{ + .font_size = size, + }, .{ .forever = {} }); + + // Schedule render which also drains our mailbox + self.queueRender() catch unreachable; +} + /// This queues a render operation with the renderer thread. The render /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. @@ -760,6 +776,16 @@ fn keyCallback( .x => .x, .y => .y, .z => .z, + .zero => .zero, + .one => .one, + .two => .three, + .three => .four, + .four => .four, + .five => .five, + .six => .six, + .seven => .seven, + .eight => .eight, + .nine => .nine, .up => .up, .down => .down, .right => .right, @@ -782,6 +808,8 @@ fn keyCallback( .F11 => .f11, .F12 => .f12, .grave_accent => .grave_accent, + .minus => .minus, + .equal => .equal, else => .invalid, }, }; @@ -858,6 +886,30 @@ fn keyCallback( } }, + .increase_font_size => |delta| { + log.debug("increase font size={}", .{delta}); + + var size = win.font_size; + size.points +|= delta; + win.setFontSize(size); + }, + + .decrease_font_size => |delta| { + log.debug("decrease font size={}", .{delta}); + + var size = win.font_size; + size.points = @max(1, size.points -| delta); + win.setFontSize(size); + }, + + .reset_font_size => { + log.debug("reset font size", .{}); + + var size = win.font_size; + size.points = win.config.@"font-size"; + win.setFontSize(size); + }, + .toggle_dev_mode => if (DevMode.enabled) { DevMode.instance.visible = !DevMode.instance.visible; win.queueRender() catch unreachable; diff --git a/src/config.zig b/src/config.zig index d7a60753e..2d94fc42c 100644 --- a/src/config.zig +++ b/src/config.zig @@ -176,6 +176,23 @@ pub const Config = struct { try result.keybind.set.put(alloc, .{ .key = .f11 }, .{ .csi = "23~" }); try result.keybind.set.put(alloc, .{ .key = .f12 }, .{ .csi = "24~" }); + // Fonts + try result.keybind.set.put( + alloc, + .{ .key = .equal, .mods = .{ .super = true } }, + .{ .increase_font_size = 1 }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .minus, .mods = .{ .super = true } }, + .{ .decrease_font_size = 1 }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .zero, .mods = .{ .super = true } }, + .{ .reset_font_size = {} }, + ); + // Dev Mode try result.keybind.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 81cc18fdc..8b909f7ad 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -131,6 +131,13 @@ pub const Action = union(enum) { copy_to_clipboard: void, paste_from_clipboard: void, + /// Increase/decrease the font size by a certain amount + increase_font_size: u16, + decrease_font_size: u16, + + /// Reset the font size to the original configured size + reset_font_size: void, + /// Dev mode toggle_dev_mode: void, diff --git a/src/input/key.zig b/src/input/key.zig index 8445547b4..da640ccf1 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -49,8 +49,22 @@ pub const Key = enum { y, z, + // numbers + zero, + one, + two, + three, + four, + five, + six, + seven, + eight, + nine, + // other grave_accent, // ` + minus, + equal, // control up, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index a33c911b7..3fcd7a7b9 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -320,6 +320,9 @@ pub fn deinit(self: *OpenGL) void { } fn resetCellsLRU(self: *OpenGL) void { + // Preserve the old capacity so that we have space in our LRU + const cap = self.cells_lru.capacity; + // Our LRU values are array lists so we need to deallocate those first var it = self.cells_lru.queue.first; while (it) |node| { @@ -329,7 +332,7 @@ fn resetCellsLRU(self: *OpenGL) void { self.cells_lru.deinit(self.alloc); // Initialize our new LRU - self.cells_lru = CellsLRU.init(0); + self.cells_lru = CellsLRU.init(cap); } /// Returns the hints that we want for this @@ -468,6 +471,8 @@ pub fn blinkCursor(self: *OpenGL, reset: bool) void { /// /// Must be called on the render thread. pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { + log.info("set font size={}", .{size}); + // Set our new size, this will also reset our font atlas. try self.font_group.setSize(size); From 7e24faac72affdc23af5bda17741cc02e37ad9b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 20:18:39 -0800 Subject: [PATCH 8/9] metal: implement font size changing --- src/renderer/Metal.zig | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b3656e735..a3a5af4f7 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -16,6 +16,7 @@ const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const math = @import("../math.zig"); const DevMode = @import("../DevMode.zig"); +const Window = @import("../Window.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Terminal = terminal.Terminal; @@ -30,6 +31,9 @@ const log = std.log.scoped(.metal); /// Allocator that can be used alloc: std.mem.Allocator, +/// The mailbox for communicating with the window. +window_mailbox: Window.Mailbox, + /// Current cell dimensions for this grid. cell_size: renderer.CellSize, @@ -208,6 +212,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { return Metal{ .alloc = alloc, + .window_mailbox = options.window_mailbox, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .padding = options.padding, .background = .{ .r = 0, .g = 0, .b = 0 }, @@ -308,7 +313,7 @@ pub fn threadExit(self: *const Metal) void { /// 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 { +fn gridSize(self: *Metal, screen_size: renderer.ScreenSize) renderer.GridSize { return renderer.GridSize.init( screen_size.subPadding(self.padding.explicit), self.cell_size, @@ -316,15 +321,57 @@ pub fn gridSize(self: *Metal, screen_size: renderer.ScreenSize) renderer.GridSiz } /// Callback when the focus changes for the terminal this is rendering. +/// +/// Must be called on the render thread. pub fn setFocus(self: *Metal, focus: bool) !void { self.focused = focus; } /// Called to toggle the blink state of the cursor +/// +/// Must be called on the render thread. pub fn blinkCursor(self: *Metal, reset: bool) void { self.cursor_visible = reset or !self.cursor_visible; } +/// Set the new font size. +/// +/// Must be called on the render thread. +pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { + log.info("set font size={}", .{size}); + + // Set our new size, this will also reset our font atlas. + try self.font_group.setSize(size); + + // Recalculate our metrics + const metrics = metrics: { + const index = (try self.font_group.indexForCodepoint(self.alloc, 'M', .regular, .text)).?; + const face = try self.font_group.group.faceFromIndex(index); + break :metrics face.metrics; + }; + const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }; + + // Update our uniforms + self.uniforms = .{ + .projection_matrix = self.uniforms.projection_matrix, + .cell_size = .{ new_cell_size.width, new_cell_size.height }, + .underline_position = metrics.underline_position, + .underline_thickness = metrics.underline_thickness, + .strikethrough_position = metrics.strikethrough_position, + .strikethrough_thickness = metrics.strikethrough_thickness, + }; + + // Recalculate our cell size. If it is the same as before, then we do + // nothing since the grid size couldn't have possibly changed. + if (std.meta.eql(self.cell_size, new_cell_size)) return; + self.cell_size = new_cell_size; + + // Notify the window that the cell size changed. + _ = self.window_mailbox.push(.{ + .cell_size = new_cell_size, + }, .{ .forever = {} }); +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *Metal, From 371a7f79cb4b747d3363d958b857d97ac9ee3a33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Nov 2022 20:29:58 -0800 Subject: [PATCH 9/9] coretext: implement resizing --- src/font/face/coretext.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bdb7338e4..c95d8d724 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -65,6 +65,15 @@ pub const Face = struct { self.* = undefined; } + /// Resize the font in-place. If this succeeds, the caller is responsible + /// for clearing any glyph caches, font atlas data, etc. + pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { + // We just create a copy and replace ourself + const face = try initFontCopy(self.font, size); + self.deinit(); + self.* = face; + } + /// Returns the glyph index for the given Unicode code point. If this /// face doesn't support this glyph, null is returned. pub fn glyphIndex(self: Face, cp: u32) ?u32 {