From 974138378bd3137f430d81bdce4443d99071845f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Aug 2022 13:04:42 -0700 Subject: [PATCH] introducing dedicated point types --- src/terminal/Screen.zig | 66 ++++++++++++++++++++++---------- src/terminal/Selection.zig | 12 +++--- src/terminal/Terminal.zig | 12 +++++- src/terminal/main.zig | 3 +- src/terminal/point.zig | 77 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 139 insertions(+), 31 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 384f19aa0..62c8d4550 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -3,11 +3,16 @@ //! //! Definitions: //! -//! * Active - The area that is the current edit-able screen. -//! * History - The area that contains lines from prior input. -//! * Display - The area that is currently visible to the user. If the -//! user is scrolled all the way down (latest) then the display -//! is equivalent to the active area. +//! * Screen - The full screen (active + history). +//! * Active - The area that is the current edit-able screen (the +//! bottom of the scrollback). This is "edit-able" because it is +//! the only part that escape sequences such as set cursor position +//! actually affect. +//! * History - The area that contains the lines prior to the active +//! area. This is the scrollback area. Escape sequences can no longer +//! affect this area. +//! * Viewport - The area that is currently visible to the user. This +//! can be thought of as the current window into the screen. //! const Screen = @This(); @@ -23,6 +28,8 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const color = @import("color.zig"); +const point = @import("point.zig"); +const Point = point.Point; const log = std.log.scoped(.screen); @@ -32,7 +39,7 @@ pub const Row = []Cell; /// Cursor represents the cursor state. pub const Cursor = struct { // x, y where the cursor currently exists (0-indexed). This x/y is - // always the offset in the display area. + // always the offset in the active area. x: usize = 0, y: usize = 0, @@ -142,8 +149,8 @@ pub fn deinit(self: *Screen, alloc: Allocator) void { self.* = undefined; } -/// This returns true if the display area is anchored at the bottom currently. -pub fn displayIsBottom(self: Screen) bool { +/// This returns true if the viewport is anchored at the bottom currently. +pub fn viewportIsBottom(self: Screen) bool { return self.visible_offset == self.bottomOffset(); } @@ -191,10 +198,10 @@ fn rowIndex(self: Screen, idx: usize) usize { /// Scroll behaviors for the scroll function. pub const Scroll = union(enum) { /// Scroll to the top of the scroll buffer. The first line of the - /// visible display will be the top line of the scroll buffer. + /// viewport will be the top line of the scroll buffer. top: void, - /// Scroll to the bottom, where the last line of the visible display + /// Scroll to the bottom, where the last line of the viewport /// will be the last line of the buffer. TODO: are we sure? bottom: void, @@ -216,7 +223,7 @@ pub const Scroll = union(enum) { /// or not). pub fn scroll(self: *Screen, behavior: Scroll) void { switch (behavior) { - // Setting display offset to zero makes row 0 be at self.top + // Setting viewport offset to zero makes row 0 be at self.top // which is the top! .top => self.visible_offset = 0, @@ -254,9 +261,9 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) void { // If we're scrolling down, we have more work to do beacuse we // need to determine if we're overwriting our scrollback. self.visible_offset +|= @intCast(usize, delta); - if (grow) - self.bottom +|= @intCast(usize, delta) - else { + if (grow) { + self.bottom +|= @intCast(usize, delta); + } else { // If we're not growing, then we want to ensure we don't scroll // off the bottom. Calculate the number of rows we can see. If we // can see less than the number of rows we have available, then scroll @@ -464,11 +471,11 @@ test "Screen: scrolling" { defer s.deinit(alloc); s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.displayIsBottom()); + try testing.expect(s.viewportIsBottom()); // Scroll down, should still be bottom s.scroll(.{ .delta = 1 }); - try testing.expect(s.displayIsBottom()); + try testing.expect(s.viewportIsBottom()); // Test our row index try testing.expectEqual(@as(usize, 5), s.rowIndex(0)); @@ -493,6 +500,27 @@ test "Screen: scrolling" { } } +// TODO +// test "Screen: scrolling more than size" { +// const testing = std.testing; +// const alloc = testing.allocator; +// +// var s = try init(alloc, 3, 5, 3); +// defer s.deinit(alloc); +// s.testWriteString("1ABCD\n2EFGH\n3IJKL"); +// +// try testing.expect(s.viewportIsBottom()); +// +// // Scroll down, should still be bottom +// s.scroll(.{ .delta = 7 }); +// try testing.expect(s.viewportIsBottom()); +// +// // Test our row index +// try testing.expectEqual(@as(usize, 5), s.rowIndex(0)); +// try testing.expectEqual(@as(usize, 10), s.rowIndex(1)); +// try testing.expectEqual(@as(usize, 15), s.rowIndex(2)); +// } + test "Screen: scroll down from 0" { const testing = std.testing; const alloc = testing.allocator; @@ -501,7 +529,7 @@ test "Screen: scroll down from 0" { defer s.deinit(alloc); s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.scroll(.{ .delta = -1 }); - try testing.expect(s.displayIsBottom()); + try testing.expect(s.viewportIsBottom()); { // Test our contents rotated @@ -534,7 +562,7 @@ test "Screen: scrollback" { // Scrolling to the bottom s.scroll(.{ .bottom = {} }); - try testing.expect(s.displayIsBottom()); + try testing.expect(s.viewportIsBottom()); { // Test our contents rotated @@ -545,7 +573,7 @@ test "Screen: scrollback" { // Scrolling back should make it visible again s.scroll(.{ .delta = -1 }); - try testing.expect(!s.displayIsBottom()); + try testing.expect(!s.viewportIsBottom()); { // Test our contents rotated diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index f06848329..f501dc994 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -4,21 +4,21 @@ const Selection = @This(); const std = @import("std"); const point = @import("point.zig"); -const Point = point.Point; +const ScreenPoint = point.ScreenPoint; /// Start and end of the selection. There is no guarantee that /// start is before end or vice versa. If a user selects backwards, /// start will be after end, and vice versa. Use the struct functions /// to not have to worry about this. -start: Point, -end: Point, +start: ScreenPoint, +end: ScreenPoint, /// Returns true if the selection contains the given point. /// /// This recalculates top left and bottom right each call. If you have /// many points to check, it is cheaper to do the containment logic /// yourself and cache the topleft/bottomright. -pub fn contains(self: Selection, p: Point) bool { +pub fn contains(self: Selection, p: ScreenPoint) bool { const tl = self.topLeft(); const br = self.bottomRight(); @@ -33,7 +33,7 @@ pub fn contains(self: Selection, p: Point) bool { } /// Returns the top left point of the selection. -pub fn topLeft(self: Selection) Point { +pub fn topLeft(self: Selection) ScreenPoint { return switch (self.order()) { .forward => self.start, .reverse => self.end, @@ -41,7 +41,7 @@ pub fn topLeft(self: Selection) Point { } /// Returns the bottom right point of the selection. -pub fn bottomRight(self: Selection) Point { +pub fn bottomRight(self: Selection) ScreenPoint { return switch (self.order()) { .forward => self.end, .reverse => self.start, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 69da93c32..104bf258b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -12,6 +12,7 @@ const Allocator = std.mem.Allocator; const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); +const Selection = @import("Selection.zig"); const Tabstops = @import("Tabstops.zig"); const trace = @import("../tracy/tracy.zig").trace; const color = @import("color.zig"); @@ -35,6 +36,9 @@ active_screen: ScreenType, screen: Screen, secondary_screen: Screen, +/// The current selection (if any). +selection: ?Selection = null, + /// Whether we're currently writing to the status line (DECSASD and DECSSDT). /// We don't support a status line currently so we just black hole this /// data so that it doesn't mess up our main display. @@ -127,6 +131,9 @@ pub fn alternateScreen(self: *Terminal, options: AlternateScreenOptions) void { self.secondary_screen = old; self.active_screen = .alternate; + // Clear our selection + self.selection = null; + if (options.clear_on_enter) { self.eraseDisplay(.complete); } @@ -149,6 +156,9 @@ pub fn primaryScreen(self: *Terminal, options: AlternateScreenOptions) void { self.secondary_screen = old; self.active_screen = .primary; + // Clear our selection + self.selection = null; + // Restore the cursor from the primary screen if (options.cursor_save) self.restoreCursor(); } @@ -330,7 +340,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (self.status_display != .main) return; // If we're not at the bottom, then we need to move there - if (!self.screen.displayIsBottom()) self.screen.scroll(.{ .bottom = {} }); + if (!self.screen.viewportIsBottom()) self.screen.scroll(.{ .bottom = {} }); // If we're soft-wrapping, then handle that first. if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) { diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 0294346d7..c608e47b8 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -8,6 +8,7 @@ pub const color = @import("color.zig"); pub const Terminal = @import("Terminal.zig"); pub const Parser = @import("Parser.zig"); pub const Selection = @import("Selection.zig"); +pub const Screen = @import("Screen.zig"); pub const Stream = stream.Stream; pub const CursorStyle = ansi.CursorStyle; pub const DeviceAttributeReq = ansi.DeviceAttributeReq; @@ -33,9 +34,9 @@ test { _ = Parser; _ = Selection; _ = Terminal; + _ = Screen; _ = @import("osc.zig"); _ = @import("parse_table.zig"); - _ = @import("Screen.zig"); _ = @import("Tabstops.zig"); } diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 2f834a213..4f7fadfc7 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -1,7 +1,76 @@ -/// Point is a point within the terminal grid. A point is ALWAYS -/// zero-indexed. If you see the "Point" type, you know that a -/// zero-indexed value is expected. -pub const Point = struct { +const std = @import("std"); +const terminal = @import("main.zig"); +const Screen = terminal.Screen; + +// This file contains various types to represent x/y coordinates. We +// use different types so that we can lean on type-safety to get the +// exact expected type of point. + +/// Viewport is a point within the viewport of the screen. +pub const Viewport = struct { + x: usize = 0, + y: usize = 0, + + pub fn toScreen(self: Viewport, screen: *const Screen) ScreenPoint { + // x is unchanged, y we have to add the visible offset to + // get the full offset from the top. + return .{ + .x = self.x, + .y = screen.visible_offset + self.y, + }; + } + + test "toScreen with no scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 3, 5, 0); + defer s.deinit(alloc); + + try testing.expectEqual(ScreenPoint{ + .x = 1, + .y = 1, + }, (Viewport{ .x = 1, .y = 1 }).toScreen(&s)); + } + + test "toScreen with scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 3, 5, 3); + defer s.deinit(alloc); + + // At the bottom + s.scroll(.{ .delta = 6 }); + try testing.expectEqual(ScreenPoint{ + .x = 0, + .y = 3, + }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); + + // Move the viewport a bit up + s.scroll(.{ .delta = -1 }); + try testing.expectEqual(ScreenPoint{ + .x = 0, + .y = 2, + }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); + + // Move the viewport to top + s.scroll(.{ .top = {} }); + try testing.expectEqual(ScreenPoint{ + .x = 0, + .y = 0, + }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); + } +}; + +/// A screen point. This is offset from the top of the scrollback +/// buffer. If the screen is scrolled or resized, this will have to +/// be recomputed. +pub const ScreenPoint = struct { x: usize = 0, y: usize = 0, }; + +test { + std.testing.refAllDecls(@This()); +}