mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
introducing dedicated point types
This commit is contained in:
@ -3,11 +3,16 @@
|
|||||||
//!
|
//!
|
||||||
//! Definitions:
|
//! Definitions:
|
||||||
//!
|
//!
|
||||||
//! * Active - The area that is the current edit-able screen.
|
//! * Screen - The full screen (active + history).
|
||||||
//! * History - The area that contains lines from prior input.
|
//! * Active - The area that is the current edit-able screen (the
|
||||||
//! * Display - The area that is currently visible to the user. If the
|
//! bottom of the scrollback). This is "edit-able" because it is
|
||||||
//! user is scrolled all the way down (latest) then the display
|
//! the only part that escape sequences such as set cursor position
|
||||||
//! is equivalent to the active area.
|
//! 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();
|
const Screen = @This();
|
||||||
|
|
||||||
@ -23,6 +28,8 @@ const std = @import("std");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
|
const point = @import("point.zig");
|
||||||
|
const Point = point.Point;
|
||||||
|
|
||||||
const log = std.log.scoped(.screen);
|
const log = std.log.scoped(.screen);
|
||||||
|
|
||||||
@ -32,7 +39,7 @@ pub const Row = []Cell;
|
|||||||
/// Cursor represents the cursor state.
|
/// Cursor represents the cursor state.
|
||||||
pub const Cursor = struct {
|
pub const Cursor = struct {
|
||||||
// x, y where the cursor currently exists (0-indexed). This x/y is
|
// 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,
|
x: usize = 0,
|
||||||
y: usize = 0,
|
y: usize = 0,
|
||||||
|
|
||||||
@ -142,8 +149,8 @@ pub fn deinit(self: *Screen, alloc: Allocator) void {
|
|||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This returns true if the display area is anchored at the bottom currently.
|
/// This returns true if the viewport is anchored at the bottom currently.
|
||||||
pub fn displayIsBottom(self: Screen) bool {
|
pub fn viewportIsBottom(self: Screen) bool {
|
||||||
return self.visible_offset == self.bottomOffset();
|
return self.visible_offset == self.bottomOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,10 +198,10 @@ fn rowIndex(self: Screen, idx: usize) usize {
|
|||||||
/// Scroll behaviors for the scroll function.
|
/// Scroll behaviors for the scroll function.
|
||||||
pub const Scroll = union(enum) {
|
pub const Scroll = union(enum) {
|
||||||
/// Scroll to the top of the scroll buffer. The first line of the
|
/// 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,
|
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?
|
/// will be the last line of the buffer. TODO: are we sure?
|
||||||
bottom: void,
|
bottom: void,
|
||||||
|
|
||||||
@ -216,7 +223,7 @@ pub const Scroll = union(enum) {
|
|||||||
/// or not).
|
/// or not).
|
||||||
pub fn scroll(self: *Screen, behavior: Scroll) void {
|
pub fn scroll(self: *Screen, behavior: Scroll) void {
|
||||||
switch (behavior) {
|
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!
|
// which is the top!
|
||||||
.top => self.visible_offset = 0,
|
.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
|
// If we're scrolling down, we have more work to do beacuse we
|
||||||
// need to determine if we're overwriting our scrollback.
|
// need to determine if we're overwriting our scrollback.
|
||||||
self.visible_offset +|= @intCast(usize, delta);
|
self.visible_offset +|= @intCast(usize, delta);
|
||||||
if (grow)
|
if (grow) {
|
||||||
self.bottom +|= @intCast(usize, delta)
|
self.bottom +|= @intCast(usize, delta);
|
||||||
else {
|
} else {
|
||||||
// If we're not growing, then we want to ensure we don't scroll
|
// 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
|
// 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
|
// can see less than the number of rows we have available, then scroll
|
||||||
@ -464,11 +471,11 @@ test "Screen: scrolling" {
|
|||||||
defer s.deinit(alloc);
|
defer s.deinit(alloc);
|
||||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
|
||||||
try testing.expect(s.displayIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
// Scroll down, should still be bottom
|
// Scroll down, should still be bottom
|
||||||
s.scroll(.{ .delta = 1 });
|
s.scroll(.{ .delta = 1 });
|
||||||
try testing.expect(s.displayIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
// Test our row index
|
// Test our row index
|
||||||
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
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" {
|
test "Screen: scroll down from 0" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
@ -501,7 +529,7 @@ test "Screen: scroll down from 0" {
|
|||||||
defer s.deinit(alloc);
|
defer s.deinit(alloc);
|
||||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
s.scroll(.{ .delta = -1 });
|
s.scroll(.{ .delta = -1 });
|
||||||
try testing.expect(s.displayIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -534,7 +562,7 @@ test "Screen: scrollback" {
|
|||||||
|
|
||||||
// Scrolling to the bottom
|
// Scrolling to the bottom
|
||||||
s.scroll(.{ .bottom = {} });
|
s.scroll(.{ .bottom = {} });
|
||||||
try testing.expect(s.displayIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -545,7 +573,7 @@ test "Screen: scrollback" {
|
|||||||
|
|
||||||
// Scrolling back should make it visible again
|
// Scrolling back should make it visible again
|
||||||
s.scroll(.{ .delta = -1 });
|
s.scroll(.{ .delta = -1 });
|
||||||
try testing.expect(!s.displayIsBottom());
|
try testing.expect(!s.viewportIsBottom());
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
|
@ -4,21 +4,21 @@ const Selection = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const point = @import("point.zig");
|
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 and end of the selection. There is no guarantee that
|
||||||
/// start is before end or vice versa. If a user selects backwards,
|
/// start is before end or vice versa. If a user selects backwards,
|
||||||
/// start will be after end, and vice versa. Use the struct functions
|
/// start will be after end, and vice versa. Use the struct functions
|
||||||
/// to not have to worry about this.
|
/// to not have to worry about this.
|
||||||
start: Point,
|
start: ScreenPoint,
|
||||||
end: Point,
|
end: ScreenPoint,
|
||||||
|
|
||||||
/// Returns true if the selection contains the given point.
|
/// Returns true if the selection contains the given point.
|
||||||
///
|
///
|
||||||
/// This recalculates top left and bottom right each call. If you have
|
/// This recalculates top left and bottom right each call. If you have
|
||||||
/// many points to check, it is cheaper to do the containment logic
|
/// many points to check, it is cheaper to do the containment logic
|
||||||
/// yourself and cache the topleft/bottomright.
|
/// 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 tl = self.topLeft();
|
||||||
const br = self.bottomRight();
|
const br = self.bottomRight();
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ pub fn contains(self: Selection, p: Point) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the top left point of the selection.
|
/// Returns the top left point of the selection.
|
||||||
pub fn topLeft(self: Selection) Point {
|
pub fn topLeft(self: Selection) ScreenPoint {
|
||||||
return switch (self.order()) {
|
return switch (self.order()) {
|
||||||
.forward => self.start,
|
.forward => self.start,
|
||||||
.reverse => self.end,
|
.reverse => self.end,
|
||||||
@ -41,7 +41,7 @@ pub fn topLeft(self: Selection) Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the bottom right point of the selection.
|
/// Returns the bottom right point of the selection.
|
||||||
pub fn bottomRight(self: Selection) Point {
|
pub fn bottomRight(self: Selection) ScreenPoint {
|
||||||
return switch (self.order()) {
|
return switch (self.order()) {
|
||||||
.forward => self.end,
|
.forward => self.end,
|
||||||
.reverse => self.start,
|
.reverse => self.start,
|
||||||
|
@ -12,6 +12,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
|
const Selection = @import("Selection.zig");
|
||||||
const Tabstops = @import("Tabstops.zig");
|
const Tabstops = @import("Tabstops.zig");
|
||||||
const trace = @import("../tracy/tracy.zig").trace;
|
const trace = @import("../tracy/tracy.zig").trace;
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
@ -35,6 +36,9 @@ active_screen: ScreenType,
|
|||||||
screen: Screen,
|
screen: Screen,
|
||||||
secondary_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).
|
/// 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
|
/// 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.
|
/// 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.secondary_screen = old;
|
||||||
self.active_screen = .alternate;
|
self.active_screen = .alternate;
|
||||||
|
|
||||||
|
// Clear our selection
|
||||||
|
self.selection = null;
|
||||||
|
|
||||||
if (options.clear_on_enter) {
|
if (options.clear_on_enter) {
|
||||||
self.eraseDisplay(.complete);
|
self.eraseDisplay(.complete);
|
||||||
}
|
}
|
||||||
@ -149,6 +156,9 @@ pub fn primaryScreen(self: *Terminal, options: AlternateScreenOptions) void {
|
|||||||
self.secondary_screen = old;
|
self.secondary_screen = old;
|
||||||
self.active_screen = .primary;
|
self.active_screen = .primary;
|
||||||
|
|
||||||
|
// Clear our selection
|
||||||
|
self.selection = null;
|
||||||
|
|
||||||
// Restore the cursor from the primary screen
|
// Restore the cursor from the primary screen
|
||||||
if (options.cursor_save) self.restoreCursor();
|
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 (self.status_display != .main) return;
|
||||||
|
|
||||||
// If we're not at the bottom, then we need to move there
|
// 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 we're soft-wrapping, then handle that first.
|
||||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
||||||
|
@ -8,6 +8,7 @@ pub const color = @import("color.zig");
|
|||||||
pub const Terminal = @import("Terminal.zig");
|
pub const Terminal = @import("Terminal.zig");
|
||||||
pub const Parser = @import("Parser.zig");
|
pub const Parser = @import("Parser.zig");
|
||||||
pub const Selection = @import("Selection.zig");
|
pub const Selection = @import("Selection.zig");
|
||||||
|
pub const Screen = @import("Screen.zig");
|
||||||
pub const Stream = stream.Stream;
|
pub const Stream = stream.Stream;
|
||||||
pub const CursorStyle = ansi.CursorStyle;
|
pub const CursorStyle = ansi.CursorStyle;
|
||||||
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
||||||
@ -33,9 +34,9 @@ test {
|
|||||||
_ = Parser;
|
_ = Parser;
|
||||||
_ = Selection;
|
_ = Selection;
|
||||||
_ = Terminal;
|
_ = Terminal;
|
||||||
|
_ = Screen;
|
||||||
|
|
||||||
_ = @import("osc.zig");
|
_ = @import("osc.zig");
|
||||||
_ = @import("parse_table.zig");
|
_ = @import("parse_table.zig");
|
||||||
_ = @import("Screen.zig");
|
|
||||||
_ = @import("Tabstops.zig");
|
_ = @import("Tabstops.zig");
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,76 @@
|
|||||||
/// Point is a point within the terminal grid. A point is ALWAYS
|
const std = @import("std");
|
||||||
/// zero-indexed. If you see the "Point" type, you know that a
|
const terminal = @import("main.zig");
|
||||||
/// zero-indexed value is expected.
|
const Screen = terminal.Screen;
|
||||||
pub const Point = struct {
|
|
||||||
|
// 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,
|
x: usize = 0,
|
||||||
y: usize = 0,
|
y: usize = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user