diff --git a/examples/main.zig b/examples/main.zig index fbf116f..4b30187 100644 --- a/examples/main.zig +++ b/examples/main.zig @@ -34,6 +34,8 @@ pub fn main() !void { var color_idx: u8 = 0; const msg = "Hello, world!"; + var scale: u3 = 1; + // The main event loop. Vaxis provides a thread safe, blocking, buffered // queue which can serve as the primary event queue for an application while (true) { @@ -51,6 +53,16 @@ pub fn main() !void { if (key.codepoint == 'c' and key.mods.ctrl) { break; } + if (key.matches('j', .{})) { + if (vx.caps.scaled_text and scale > 1) { + scale -= 1; + } + } + if (key.matches('k', .{})) { + if (vx.caps.scaled_text and scale < 7) { + scale += 1; + } + } }, .winsize => |ws| { try vx.resize(alloc, tty.anyWriter(), ws); @@ -86,8 +98,19 @@ pub fn main() !void { .style = .{ .fg = .{ .index = color_idx }, }, + .scale = .{ + .scale = scale, + }, + }; + const second_cell: Cell = .{ + .char = .{ .grapheme = msg[i .. i + 1] }, + .style = .{ + .fg = .{ .index = color_idx }, + }, }; - child.writeCell(@intCast(i), 0, cell); + child.writeCell(@intCast(i * scale), 0, cell); + child.writeCell(@intCast(i), scale - 1, second_cell); + child.writeCell(@intCast(i), scale, second_cell); } // Render the screen try vx.render(tty.anyWriter()); diff --git a/src/Cell.zig b/src/Cell.zig index 2249b20..2850f42 100644 --- a/src/Cell.zig +++ b/src/Cell.zig @@ -9,6 +9,7 @@ default: bool = false, /// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if /// it should rely on the terminal's autowrap feature which can help with primary screen resizes wrapped: bool = false, +scale: Scale = .{}, /// Segment is a contiguous run of text that has a constant style pub const Segment = struct { @@ -42,6 +43,25 @@ pub const Hyperlink = struct { params: []const u8 = "", }; +pub const Scale = packed struct { + scale: u3 = 1, + // The spec allows up to 15, but we limit to 7 + numerator: u4 = 1, + // The spec allows up to 15, but we limit to 7 + denominator: u4 = 1, + vertical_alignment: enum(u2) { + top = 0, + bottom = 1, + center = 2, + } = .top, + + pub fn eql(self: Scale, other: Scale) bool { + const a_scale: u13 = @bitCast(self); + const b_scale: u13 = @bitCast(other); + return a_scale == b_scale; + } +}; + pub const Style = struct { pub const Underline = enum { off, diff --git a/src/InternalScreen.zig b/src/InternalScreen.zig index ef95924..8db2bdb 100644 --- a/src/InternalScreen.zig +++ b/src/InternalScreen.zig @@ -18,6 +18,12 @@ pub const InternalCell = struct { skipped: bool = false, default: bool = true, + // If we should skip rendering *this* round due to being printed over previously (from a scaled + // cell, for example) + skip: bool = false, + + scale: Cell.Scale = .{}, + pub fn eql(self: InternalCell, cell: Cell) bool { // fastpath when both cells are default diff --git a/src/Loop.zig b/src/Loop.zig index de47a3d..05245da 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -177,7 +177,7 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even } }, .key_press => |key| { - // Check for a cursor position response for our explicity width query. This will + // Check for a cursor position response for our explicit width query. This will // always be an F3 key with shift = true, and we must be looking for queries if (key.codepoint == vaxis.Key.f3 and key.mods.shift and @@ -189,6 +189,16 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even vx.screen.width_method = .unicode; return; } + // Check for a cursor position response for our scaled text query. This will + // always be an F3 key with alt = true, and we must be looking for queries + if (key.codepoint == vaxis.Key.f3 and + key.mods.alt and + !vx.queries_done.load(.unordered)) + { + log.info("scaled text capability detected", .{}); + vx.caps.scaled_text = true; + return; + } if (@hasField(Event, "key_press")) { // HACK: yuck. there has to be a better way var mut_key = key; @@ -245,6 +255,16 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even vx.screen.width_method = .unicode; return; } + // Check for a cursor position response for our scaled text query. This will + // always be an F3 key with alt = true, and we must be looking for queries + if (key.codepoint == vaxis.Key.f3 and + key.mods.alt and + !vx.queries_done.load(.unordered)) + { + log.info("scaled text capability detected", .{}); + vx.caps.scaled_text = true; + return; + } if (@hasField(Event, "key_press")) { // HACK: yuck. there has to be a better way var mut_key = key; diff --git a/src/Vaxis.zig b/src/Vaxis.zig index d179e8d..5ac4451 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -37,6 +37,7 @@ pub const Capabilities = struct { sgr_pixels: bool = false, color_scheme_updates: bool = false, explicit_width: bool = false, + scaled_text: bool = false, }; pub const Options = struct { @@ -280,6 +281,14 @@ pub fn queryTerminalSend(vx: *Vaxis, tty: AnyWriter) !void { ctlseqs.home ++ ctlseqs.explicit_width_query ++ ctlseqs.cursor_position_request ++ + // Explicit width query. We send the cursor home, then do an scaled text command, then + // query the position. If the parsed value is an F3 with al, we support scaled text. + // The returned response will be something like \x1b[1;3R...which when parsed as a Key is a + // alt + F3 (the row is ignored). We only care if the column has moved from 1->3, which is + // why we see a Shift modifier + ctlseqs.home ++ + ctlseqs.scaled_text_query ++ + ctlseqs.cursor_position_request ++ ctlseqs.xtversion ++ ctlseqs.csi_u_query ++ ctlseqs.kitty_graphics_query ++ @@ -373,6 +382,11 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void { if (self.caps.kitty_graphics) try tty.writeAll(ctlseqs.kitty_graphics_clear); + // Reset skip flag on all last_screen cells + for (self.screen_last.buf) |*last_cell| { + last_cell.skip = false; + } + var i: usize = 0; while (i < self.screen.buf.len) { const cell = self.screen.buf[i]; @@ -404,7 +418,12 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void { // If cell is the same as our last frame, we don't need to do // anything const last = self.screen_last.buf[i]; - if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { + if ((!self.refresh and + last.eql(cell) and + !last.skipped and + cell.image == null) or + last.skip) + { reposition = true; // Close any osc8 sequence we might be in before // repositioning @@ -421,6 +440,24 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void { // Set this cell in the last frame self.screen_last.writeCell(col, row, cell); + // If we support scaled text, we set the flags now + if (self.caps.scaled_text and cell.scale.scale > 1) { + // The cell is scaled. Set appropriate skips. We only need to do this if the scale factor is + // > 1 + assert(cell.char.width > 0); + const cols = cell.scale.scale * cell.char.width; + const rows = cell.scale.scale; + for (0..rows) |skipped_row| { + for (0..cols) |skipped_col| { + if (skipped_row == 0 and skipped_col == 0) { + continue; + } + const skipped_i = (@as(usize, @intCast(skipped_row + row)) * self.screen_last.width) + (skipped_col + col); + self.screen_last.buf[skipped_i].skip = true; + } + } + } + // reposition the cursor, if needed if (reposition) { reposition = false; @@ -630,6 +667,41 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void { try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri }); } + // scale + if (self.caps.scaled_text and !cell.scale.eql(.{})) { + const scale = cell.scale; + // We have a scaled cell. + switch (cell.scale.denominator) { + // Denominator cannot be 0 + 0 => unreachable, + 1 => { + // no fractional scaling, just a straight scale factor + try tty.print( + ctlseqs.scaled_text, + .{ scale.scale, w, cell.char.grapheme }, + ); + }, + else => { + // fractional scaling + // no fractional scaling, just a straight scale factor + try tty.print( + ctlseqs.scaled_text_with_fractions, + .{ + scale.scale, + w, + scale.numerator, + scale.denominator, + @intFromEnum(scale.vertical_alignment), + cell.char.grapheme, + }, + ); + }, + } + cursor_pos.col = col + (w * scale.scale); + cursor_pos.row = row; + continue; + } + // If we have explicit width and our width is greater than 1, let's use it if (self.caps.explicit_width and w > 1) { try tty.print(ctlseqs.explicit_width, .{ w, cell.char.grapheme }); diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index 41aefe7..73a869b 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -13,6 +13,7 @@ pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\"; pub const sixel_geometry_query = "\x1b[?2;1;0S"; pub const cursor_position_request = "\x1b[6n"; pub const explicit_width_query = "\x1b]66;w=1; \x1b\\"; +pub const scaled_text_query = "\x1b]66;s=2; \x1b\\"; // mouse. We try for button motion and any motion. terminals will enable the // last one we tried (any motion). This was added because zellij doesn't @@ -35,6 +36,10 @@ pub const unicode_set = "\x1b[?2027h"; pub const unicode_reset = "\x1b[?2027l"; pub const explicit_width = "\x1b]66;w={d};{s}\x1b\\"; +// text sizing +pub const scaled_text = "\x1b]66;s={d}:w={d};{s}\x1b\\"; +pub const scaled_text_with_fractions = "\x1b]66;s={d}:w={d}:n={d}:d={d}:v={d};{s}\x1b\\"; + // bracketed paste pub const bp_set = "\x1b[?2004h"; pub const bp_reset = "\x1b[?2004l";