track mouse state, setup selection state on cursor move with click

This commit is contained in:
Mitchell Hashimoto
2022-08-04 14:21:40 -07:00
parent f2e9746d2b
commit 40161a9eff
2 changed files with 117 additions and 2 deletions

View File

@ -50,6 +50,9 @@ pty: Pty,
/// The command we're running for our tty. /// The command we're running for our tty.
command: Command, command: Command,
/// Mouse state.
mouse: Mouse,
/// The terminal emulator internal state. This is the abstract "terminal" /// The terminal emulator internal state. This is the abstract "terminal"
/// that manages input, grid updating, etc. and is renderer-agnostic. It /// that manages input, grid updating, etc. and is renderer-agnostic. It
/// just stores internal state about a grid. This is connected back to /// just stores internal state about a grid. This is connected back to
@ -130,6 +133,18 @@ const Cursor = struct {
} }
}; };
/// Mouse state for the window.
const Mouse = struct {
/// The current state of mouse click.
click_state: ClickState = .none,
/// The point at which the mouse click happened. This is in screen
/// coordinates so that scrolling preserves the location.
click_point: terminal.point.ScreenPoint = .{},
const ClickState = enum { none, left };
};
/// Create a new window. This allocates and returns a pointer because we /// Create a new window. This allocates and returns a pointer because we
/// need a stable pointer for user data callbacks. Therefore, a stack-only /// need a stable pointer for user data callbacks. Therefore, a stack-only
/// initialization is not currently possible. /// initialization is not currently possible.
@ -267,6 +282,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
.grid = grid, .grid = grid,
.pty = pty, .pty = pty,
.command = cmd, .command = cmd,
.mouse = .{},
.terminal = term, .terminal = term,
.terminal_stream = .{ .handler = self }, .terminal_stream = .{ .handler = self },
.terminal_cursor = .{ .terminal_cursor = .{
@ -290,6 +306,8 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
window.setFocusCallback(focusCallback); window.setFocusCallback(focusCallback);
window.setRefreshCallback(refreshCallback); window.setRefreshCallback(refreshCallback);
window.setScrollCallback(scrollCallback); window.setScrollCallback(scrollCallback);
window.setCursorPosCallback(cursorPosCallback);
window.setMouseButtonCallback(mouseButtonCallback);
return self; return self;
} }
@ -548,6 +566,104 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
win.render_timer.schedule() catch unreachable; win.render_timer.schedule() catch unreachable;
} }
fn mouseButtonCallback(
window: glfw.Window,
button: glfw.MouseButton,
action: glfw.Action,
mods: glfw.Mods,
) void {
_ = mods;
const tracy = trace(@src());
defer tracy.end();
if (button == .left) {
switch (action) {
.press => {
const win = window.getUserPointer(Window) orelse return;
const pos = window.getCursorPos() catch |err| {
log.err("error reading cursor position: {}", .{err});
return;
};
// Store it
const point = win.posToViewport(pos.xpos, pos.ypos);
win.mouse.click_state = .left;
win.mouse.click_point = point.toScreen(&win.terminal.screen);
log.debug("click start state={} viewport={} screen={}", .{
win.mouse.click_state,
point,
win.mouse.click_point,
});
// Selection is always cleared
if (win.terminal.selection != null) {
win.terminal.selection = null;
win.render_timer.schedule() catch |err|
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
}
},
.release => {
const win = window.getUserPointer(Window) orelse return;
win.mouse.click_state = .none;
log.debug("click end", .{});
},
.repeat => {},
}
}
}
fn cursorPosCallback(
window: glfw.Window,
xpos: f64,
ypos: f64,
) void {
const tracy = trace(@src());
defer tracy.end();
const win = window.getUserPointer(Window) orelse return;
// If the cursor isn't clicked currently, it doesn't matter
if (win.mouse.click_state != .left) return;
// All roads lead to requiring a re-render at this pont.
win.render_timer.schedule() catch |err|
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
// Convert to points
const viewport_point = win.posToViewport(xpos, ypos);
const screen_point = viewport_point.toScreen(&win.terminal.screen);
// If the start point is the same as this point, we clear selection.
// This means we either haven't moved enough OR we moved the mouse back
// to the same place, and we "unhighlighted" everything.
if (std.meta.eql(screen_point, win.mouse.click_point)) {
win.terminal.selection = null;
return;
}
// TODO: detect if selection point is passed the point where we've
// actually written data before and disallow it.
// We moved! Set the selection
win.terminal.selection = .{
.start = win.mouse.click_point,
.end = screen_point,
};
}
fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport {
// Convert the mouse position to the viewport x/y
const cell_width = @floatCast(f64, self.grid.cell_size.width);
const cell_height = @floatCast(f64, self.grid.cell_size.height);
return .{
.x = @floatToInt(usize, xpos / cell_width),
.y = @floatToInt(usize, ypos / cell_height),
};
}
fn cursorTimerCallback(t: *libuv.Timer) void { fn cursorTimerCallback(t: *libuv.Timer) void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();

View File

@ -2,7 +2,7 @@ const stream = @import("stream.zig");
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 point = @import("point.zig"); pub const point = @import("point.zig");
pub const color = @import("color.zig"); pub const color = @import("color.zig");
pub const Terminal = @import("Terminal.zig"); pub const Terminal = @import("Terminal.zig");
@ -20,7 +20,6 @@ pub const EraseDisplay = csi.EraseDisplay;
pub const EraseLine = csi.EraseLine; pub const EraseLine = csi.EraseLine;
pub const TabClear = csi.TabClear; pub const TabClear = csi.TabClear;
pub const Attribute = sgr.Attribute; pub const Attribute = sgr.Attribute;
pub const Point = point.Point;
// Not exported because they're just used for tests. // Not exported because they're just used for tests.