Compare commits

...
Sign in to create a new pull request.

80 commits

Author SHA1 Message Date
46458f2e87 Merge remote-tracking branch 'upstream/main' into zig-0.14.0 2025-03-15 22:55:35 +01:00
Jeffrey C. Ollie
4182b7fa42 switch zigimg ref to merged commit
Needed for fixing https://github.com/ghostty-org/ghostty/issues/6734
2025-03-15 07:01:52 -05:00
Jost Alemann
1e24e0dfb5 build: allow building split_view example 2025-03-09 14:28:07 -05:00
Tim Culverhouse
6a37605dde
vxfw(TextField): fix freeing of previous_val 2025-03-07 11:46:26 -06:00
Tim Culverhouse
33191138b0
Revert "vxfw(TextField): free previous_val"
This reverts commit 3a70d898d5.
2025-03-07 11:12:56 -06:00
Tim Culverhouse
3a70d898d5
vxfw(TextField): free previous_val 2025-03-07 10:52:53 -06:00
Tim Culverhouse
6b5a011f58
vxfw: add empty Surface identifier 2025-03-07 08:32:02 -06:00
Tim Culverhouse
5b48d2c40c
vxfw: always subscribe to colorscheme updates 2025-03-05 13:03:58 -06:00
Tim Culverhouse
49fcd33812
vxfw: update use of popOrNull, fix examples 2025-03-05 09:38:44 -06:00
Tim Culverhouse
daa735628f
update readme 2025-03-05 09:33:41 -06:00
Tim Culverhouse
91a4ff5d55 zg: use upstream 2025-03-05 09:33:14 -06:00
Tim Culverhouse
bdaa9f9c08 zig fmt (0.14.0 had some formatter changes) 2025-03-05 09:33:14 -06:00
Tim Culverhouse
25955db06b ci: use zig 0.14.0 for test workflow 2025-03-05 09:33:14 -06:00
Tim Culverhouse
01605eebf6 ci: fix ci zig version 2025-03-05 09:33:14 -06:00
Tim Culverhouse
5a7da722a3 update for zig 0.14.0 2025-03-05 09:33:14 -06:00
Tim Culverhouse
00e222ce11 root: fix default panic handler 2025-03-05 09:33:14 -06:00
Tim Culverhouse
8db9a92429 WIP: replace zg with fork for recent zig
Squash this commit once upstream zg has accepted a PR which updates for
recent zig
2025-03-05 09:33:14 -06:00
Tim Culverhouse
5e7c1ccf34 ci: update zig version in setup-zig 2025-03-05 09:33:14 -06:00
Tim Culverhouse
3f639fb364 vt: fix for zig-0.14.0 2025-03-05 09:33:14 -06:00
Tim Culverhouse
155f88a885 view: fix View widget 2025-03-05 09:33:14 -06:00
Tim Culverhouse
57634d7700 table: fix table Type reflection fields 2025-03-05 09:33:14 -06:00
Tim Culverhouse
f5ed5feb56 deps: update zigimg 2025-03-05 09:33:14 -06:00
Adrià Arrufat
b9fc10f9d3 remove anonymous structs 2025-03-05 09:33:14 -06:00
Tim Culverhouse
9d01c62e8a posix: update type and fn signatures for api changes 2025-03-05 09:33:14 -06:00
Tim Culverhouse
16fba57999 zig: update apis for zig 0.14.0 2025-03-05 09:33:14 -06:00
Tim Culverhouse
b6043f4497
vxfw: add query_color command 2025-03-04 12:52:14 -06:00
Tim Culverhouse
92657ef00e
vxfw(ListView): obscure area when drawing cursor 2025-03-04 11:20:19 -06:00
Tim Culverhouse
b233673f18
vxfw(TextField): use style for text too 2025-03-04 11:10:07 -06:00
Tim Culverhouse
8c374cc51a
vxfw(TextField): let users set style 2025-03-04 11:04:57 -06:00
Tim Culverhouse
0a954a536f
parser: handle shift+space in kkp+disambiguate
Some terminals (foot, ghostty) encode shift + space as CSI 32 ; 2 u when
using the kkp disambiguate flag. We need to handle this as a printable
character. Use the logic from vaxis (the go version), which checks if
the mods were shift only and the character is printable. Then we upper
case it, set the text and the shifted char
2025-03-04 08:27:06 -06:00
Tim Culverhouse
72f1e22333
vxfw: send focus_in and focus_out commands explicitly 2025-03-03 11:36:05 -06:00
Tim Culverhouse
bcc1d027cb
vxfw: enable bracketed paste by default 2025-02-27 14:42:56 -06:00
Tim Culverhouse
af450ebb1b
vxfw(TextField): use ref to allocator to free onSubmit value 2025-02-24 14:13:21 -06:00
Tim Culverhouse
4816ee20fd
vxfw(TextField): get a new dupe for onSubmit 2025-02-24 14:03:32 -06:00
Tim Culverhouse
cf7c2da082
vxfw(ListView): fix rendering of cursored widget 2025-02-24 11:17:04 -06:00
Tim Culverhouse
89b44f541f
vxfw: add assertion comment 2025-02-24 10:56:36 -06:00
Tim Culverhouse
cee974a2ae
vxfw: remove focusable field 2025-02-24 10:55:15 -06:00
Tim Culverhouse
4f8fe8f101
vxfw: simplify focus management 2025-02-24 10:55:15 -06:00
Tim Culverhouse
a653e84b33 vxfw(Command): add notifications 2025-02-24 09:30:31 -06:00
Tim Culverhouse
48e9d60340 vxfw(Command): add queue_refresh command 2025-02-24 09:30:31 -06:00
Tim Culverhouse
a112fc13f3
vxfw: target widget should also get capture phase events 2025-02-24 09:05:32 -06:00
Tim Culverhouse
b2e2588e69 vxfw: improve mouse handling
Change the core loop for handling of mouse events. We now do a layout
phase, which calls draw on the root widget. From here, we see  if the
widget underneath the mouse has changed and, if so, send mouse_enter and
mouse_exit events, then do another layout, and then *finally* we render.

This fixes a case where the mouse hasn't moved, but the content of the
screen has changed. The hover state of any widget will be updated in the
second layout phase if the app indicates a redraw is necessary from the
mouse_enter and mouse_exit events.
2025-02-23 08:00:00 -06:00
Tim Culverhouse
572af9309a
window: fix tests 2025-02-23 07:49:02 -06:00
Tim Culverhouse
f8672276e5
vxfw(RichText): enable hyperlinks 2025-02-21 15:59:06 -06:00
Tim Culverhouse
01e7b6644b
window: fix lower bounds for children window
The lower bound must be within the parent window
2025-02-21 12:39:47 -06:00
Tim Culverhouse
0fb96df48e
vxfw: misc size checks for 0 width or height 2025-02-21 07:36:04 -06:00
Tim Culverhouse
dbf7e0bf09 vxfw: add set_title command 2025-02-20 12:17:00 -06:00
Tim Culverhouse
4e07fb905e
vxfw(ListView): fix integer overflow 2025-02-20 08:12:07 -06:00
Tim Culverhouse
d8e5ec0d7b
vxfw(SplitView): fix rhs offset 2025-02-19 14:05:43 -06:00
Tim Culverhouse
682d1ce98b vxfw(SplitView): fix calcs for rhs constraints 2025-02-19 14:03:33 -06:00
frehml
208e7f7062 feature: add special keys to name map 2025-02-19 10:24:07 -06:00
Tim Culverhouse
0eaf6226b2 caps!: implement explicit width extension
Implement explicit width hint extension, developed by kitty. When
both explicit width and mode 2027 are available, we default to explicit
width. Custom event loop authors will need to update their loops to add
support for this by setting the new capability value.

For simplicity, we don't actually add a flag in the parser for checking
between a cursor position and an F3 key. Instead, we send the cursor
home, then do an explicit width command, *then* check the cursor
position. If the cursor has moved - meaning the extension is supported -
we will see an F3 key with the shift modifier. The response will be
something like `\x1b[1;2R` which we parse as a shift+F3. But in the
loop, we check the flag if we have sent queries and handle this specific
event differently.

Reference: https://github.com/kovidgoyal/kitty/issues/8226
2025-02-03 14:53:06 -06:00
Kristófer R
a41d3d8cea examples: add scroll example
This adds an example demonstrating the use of the ScrollView and
ScrollBars widgets. It also includes keybindings to change how the child
widgets are laid out so you can see how that affects how the scroll bars
are rendered.

The content size estimates are deliberately wrong to demonstrate how
that affects the scroll bars. The value is intended to be modified by
whoever is testing the example.
2025-01-30 08:58:00 -08:00
Kristófer R
46cdcca260 vxfw: Add ScrollBars widget
This widget is intended to wrap a ScrollView widget and show vertical
and horizontal scroll bars as indicators of scroll position in the
ScrollView. It's recommended to provide the estimated content sizes
with as much accuracy as possible for the best user experience and
performance.

If estimated content sizes are not provided the scroll bar sizes and
positions will be estimated using the size of child arrays. This is not
perfect and will cause inconsistencies if the child widgets aren't all
the exact same heights.
2025-01-30 08:58:00 -08:00
Kristófer R
ee6c47c500 vxfw: Add ScrollView widget
The widget will render its children in a container that can be scrolled
both horizontally and vertically. The widget itself does not render any
scroll bars or other indicators of current scroll position.

Since this view is heavily based on the ListView widget it inherits the
same `cursor` functionality to show the current position of a selected
widget.

Known Issues
============

1. The view currently does not enforce a maximum width on the content to
   be able to correctly figure out whether the content can still be
   scrolled horizontally. This will cause the widget to draw beyond its
   boundaries horizontally.
2. When the last widget rendered is taller than a single row the whole
   widget will be drawn. This will cause the widget to draw beyond its
   boundaries vertically.
2025-01-30 08:58:00 -08:00
Tim Culverhouse
9ec42325a6
chore: update more github workflows 2025-01-30 10:51:32 -06:00
Tim Culverhouse
4642abdd60 chore: update github workflows 2025-01-30 08:49:45 -08:00
Tim Culverhouse
539fd55602
windows(Loop): implement mouse and focus handling
Implement mouse and focus events in windows. Thanks @neurocyte for
locating this.

Fixes: 
2025-01-30 10:39:39 -06:00
Tim Culverhouse
38f3e7b8fe
vaxis: expose tty namespace 2025-01-27 14:51:39 -06:00
Tim Culverhouse
d34c4c6c7f
screen: return all cell fields in readCell 2025-01-24 21:01:23 -06:00
Tim Culverhouse
6afd4786eb
vxfw: add cell_size to all tests 2025-01-22 14:26:56 -06:00
Tim Culverhouse
d6e26f1496
vxfw: report cell size to app 2025-01-22 13:11:42 -06:00
Tim Culverhouse
d690f80e31
vxfw: report release and press events with kkp 2025-01-22 10:22:38 -06:00
Jari Vetoniemi
67a13bd907 vaxis: expose loop namespace
vaxis-aio reuses the handleEventGeneric function
2025-01-20 15:44:02 -06:00
Jari Vetoniemi
0b00376cdc Scrollbar: usize -> u16 casts 2025-01-20 15:44:02 -06:00
Jari Vetoniemi
9036ee27be ScrollView: usize -> u16 casts 2025-01-20 15:44:02 -06:00
Jari Vetoniemi
a61848861c LineNumbers: usize -> u16 casts 2025-01-20 15:44:02 -06:00
0e930e2325 ci: Set repositories that uses actions from github 2025-01-20 22:35:48 +01:00
98de4f451f ci: Use docker runner 2025-01-20 22:33:03 +01:00
172523bbf2 ci: Add ci for zig-0.14.0 branch 2025-01-20 22:31:23 +01:00
f1f45294ab fix: Use patched zigimg 2025-01-18 15:43:14 +01:00
Tim Culverhouse
73d46b31c7
WIP: replace zg with fork for recent zig
Squash this commit once upstream zg has accepted a PR which updates for
recent zig
2025-01-15 09:57:50 -06:00
Tim Culverhouse
2fd1ccef0c
ci: update zig version in setup-zig 2025-01-15 09:57:50 -06:00
Tim Culverhouse
335df46243
vt: fix for zig-0.14.0 2025-01-15 09:57:50 -06:00
Tim Culverhouse
dcf9e9f711
view: fix View widget 2025-01-15 09:57:50 -06:00
Tim Culverhouse
f73c938e9e
table: fix table Type reflection fields 2025-01-15 09:57:50 -06:00
Tim Culverhouse
25c3d1f2d3
deps: update zigimg 2025-01-15 09:57:50 -06:00
Adrià Arrufat
0c3ca95e85
remove anonymous structs 2025-01-15 09:57:50 -06:00
Tim Culverhouse
775466278c
posix: update type and fn signatures for api changes 2025-01-15 09:57:50 -06:00
Tim Culverhouse
e238840c87
zig: update apis for zig 0.14.0 2025-01-15 09:24:17 -06:00
45 changed files with 2605 additions and 408 deletions

View file

@ -27,12 +27,12 @@ jobs:
uses: actions/configure-pages@v2
- uses: mlugg/setup-zig@v1
with:
version: 0.13.0
version: 0.14.0
- run: zig build docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: "zig-out/docs"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4

View file

@ -2,7 +2,7 @@ name: test
on:
pull_request:
branches: ["main"]
branches: ["main", "zig-0.14.0"]
workflow_dispatch:
jobs:
@ -10,18 +10,18 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.os}}
runs-on: macos-arm64
steps:
- uses: actions/checkout@v3
- uses: mlugg/setup-zig@v1
- uses: actions/checkout@v4
- uses: https://github.com/mlugg/setup-zig@v1
with:
version: 0.13.0
version: 0.14.0
- run: zig build test
check-fmt:
runs-on: ubuntu-latest
runs-on: docker
steps:
- uses: actions/checkout@v3
- uses: mlugg/setup-zig@v1
- uses: actions/checkout@v4
- uses: https://github.com/mlugg/setup-zig@v1
with:
version: 0.13.0
version: 0.14.0
- run: zig fmt --check .

View file

@ -9,7 +9,7 @@ It begins with them, but ends with me. Their son, Vaxis
Libvaxis _does not use terminfo_. Support for vt features is detected through
terminal queries.
Vaxis uses zig `0.13.0`.
Vaxis uses zig `0.14.0`.
## Features
@ -30,6 +30,7 @@ Unix-likes.
- Color Mode Updates (Mode 2031)
- [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) (Mode 2048)
- Images ([kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/))
- [Explicit Width](https://github.com/kovidgoyal/kitty/blob/master/docs/text-sizing-protocol.rst) (width modifiers only)
## Usage
@ -154,7 +155,6 @@ const Model = struct {
// A Surface must have a size. Our root widget is the size of the screen
.size = max_size,
.widget = self.widget(),
.focusable = false,
// We didn't actually need to draw anything for the root. In this case, we can set
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
// assert that it's length is equal to the size.width * size.height.

View file

@ -305,16 +305,12 @@ release and is shown here merely as a proof of concept.
```zig
const builtin = @import("builtin");
const std = @import("std");
const vaxis = @import("main.zig");
const handleEventGeneric = @import("Loop.zig").handleEventGeneric;
const vaxis = @import("vaxis");
const handleEventGeneric = vaxis.loop.handleEventGeneric;
const log = std.log.scoped(.vaxis_aio);
const Yield = enum { no_state, took_event };
pub fn Loop(T: type) type {
return LoopWithModules(T, @import("aio"), @import("coro"));
}
/// zig-aio based event loop
/// <https://github.com/Cloudef/zig-aio>
pub fn LoopWithModules(T: type, aio: type, coro: type) type {

View file

@ -33,6 +33,8 @@ pub fn build(b: *std.Build) void {
fuzzy,
image,
main,
scroll,
split_view,
table,
text_input,
vaxis,

View file

@ -1,15 +1,16 @@
.{
.name = "vaxis",
.name = .vaxis,
.fingerprint = 0x14fbbb94fc556305,
.version = "0.1.0",
.minimum_zig_version = "0.13.0",
.minimum_zig_version = "0.14.0",
.dependencies = .{
.zigimg = .{
.url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
.hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5",
.url = "https://git.kcbark.net/zig/zigimg/archive/refs/heads/master.tar.gz",
.hash = "zigimg-0.1.0-6EC2bT5oEACE-3wd0vLyYpL40DmplOztjn0APgTCyg7y",
},
.zg = .{
.url = "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz",
.hash = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40",
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
.hash = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM",
},
},
.paths = .{

View file

@ -93,7 +93,6 @@ const Model = struct {
// A Surface must have a size. Our root widget is the size of the screen
.size = max_size,
.widget = self.widget(),
.focusable = false,
// We didn't actually need to draw anything for the root. In this case, we can set
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
// assert that it's length is equal to the size.width * size.height.

View file

@ -54,14 +54,13 @@ const Model = struct {
const self: *Model = @ptrCast(@alignCast(ptr));
const max = ctx.max.size();
var list_view: vxfw.SubSurface = .{
const list_view: vxfw.SubSurface = .{
.origin = .{ .row = 2, .col = 0 },
.surface = try self.list_view.draw(ctx.withConstraints(
ctx.min,
.{ .width = max.width, .height = max.height - 3 },
)),
};
list_view.surface.focusable = false;
const text_field: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 2 },
@ -86,7 +85,6 @@ const Model = struct {
return .{
.size = max,
.widget = self.widget(),
.focusable = true,
.buffer = &.{},
.children = children,
};
@ -209,12 +207,12 @@ pub fn main() !void {
var fd = std.process.Child.init(&.{"fd"}, allocator);
fd.stdout_behavior = .Pipe;
fd.stderr_behavior = .Pipe;
var stdout = std.ArrayList(u8).init(allocator);
var stderr = std.ArrayList(u8).init(allocator);
defer stdout.deinit();
defer stderr.deinit();
var stdout: std.ArrayListUnmanaged(u8) = .empty;
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(allocator);
defer stderr.deinit(allocator);
try fd.spawn();
try fd.collectOutput(&stdout, &stderr, 10_000_000);
try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000);
_ = try fd.wait();
var iter = std.mem.splitScalar(u8, stdout.items, '\n');

214
examples/scroll.zig Normal file
View file

@ -0,0 +1,214 @@
const std = @import("std");
const vaxis = @import("vaxis");
const vxfw = vaxis.vxfw;
const ModelRow = struct {
text: []const u8,
idx: usize,
wrap_lines: bool = true,
pub fn widget(self: *ModelRow) vxfw.Widget {
return .{
.userdata = self,
.drawFn = ModelRow.typeErasedDrawFn,
};
}
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
const self: *ModelRow = @ptrCast(@alignCast(ptr));
const idx_text = try std.fmt.allocPrint(ctx.arena, "{d: >4}", .{self.idx});
const idx_widget: vxfw.Text = .{ .text = idx_text };
const idx_surf: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 0 },
.surface = try idx_widget.draw(ctx.withConstraints(
// We're only interested in constraining the width, and we know the height will
// always be 1 row.
.{ .width = 1, .height = 1 },
.{ .width = 4, .height = 1 },
)),
};
const text_widget: vxfw.Text = .{ .text = self.text, .softwrap = self.wrap_lines };
const text_surf: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 6 },
.surface = try text_widget.draw(ctx.withConstraints(
ctx.min,
// We've shifted the origin over 6 columns so we need to take that into account or
// we'll draw outside the window.
if (self.wrap_lines)
.{ .width = ctx.min.width -| 6, .height = ctx.max.height }
else
.{ .width = if (ctx.max.width) |w| w - 6 else null, .height = ctx.max.height },
)),
};
const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
children[0] = idx_surf;
children[1] = text_surf;
return .{
.size = .{
.width = 6 + text_surf.surface.size.width,
.height = @max(idx_surf.surface.size.height, text_surf.surface.size.height),
},
.widget = self.widget(),
.buffer = &.{},
.children = children,
};
}
};
const Model = struct {
scroll_bars: vxfw.ScrollBars,
rows: std.ArrayList(ModelRow),
pub fn widget(self: *Model) vxfw.Widget {
return .{
.userdata = self,
.eventHandler = Model.typeErasedEventHandler,
.drawFn = Model.typeErasedDrawFn,
};
}
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const self: *Model = @ptrCast(@alignCast(ptr));
switch (event) {
.key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) {
ctx.quit = true;
return;
}
if (key.matches('w', .{ .ctrl = true })) {
for (self.rows.items) |*row| {
row.wrap_lines = !row.wrap_lines;
}
self.scroll_bars.estimated_content_height =
if (self.scroll_bars.estimated_content_height == 800)
@intCast(self.rows.items.len)
else
800;
return ctx.consumeAndRedraw();
}
if (key.matches('e', .{ .ctrl = true })) {
if (self.scroll_bars.estimated_content_height == null)
self.scroll_bars.estimated_content_height = 800
else
self.scroll_bars.estimated_content_height = null;
return ctx.consumeAndRedraw();
}
if (key.matches(vaxis.Key.tab, .{})) {
self.scroll_bars.scroll_view.draw_cursor = !self.scroll_bars.scroll_view.draw_cursor;
return ctx.consumeAndRedraw();
}
if (key.matches('v', .{ .ctrl = true })) {
self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar;
return ctx.consumeAndRedraw();
}
if (key.matches('h', .{ .ctrl = true })) {
self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar;
return ctx.consumeAndRedraw();
}
if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar;
self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar;
return ctx.consumeAndRedraw();
}
return self.scroll_bars.scroll_view.handleEvent(ctx, event);
},
else => {},
}
}
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
const self: *Model = @ptrCast(@alignCast(ptr));
const max = ctx.max.size();
const scroll_view: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 0 },
.surface = try self.scroll_bars.draw(ctx),
};
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
children[0] = scroll_view;
return .{
.size = max,
.widget = self.widget(),
.buffer = &.{},
.children = children,
};
}
fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
const self: *const Model = @ptrCast(@alignCast(ptr));
if (idx >= self.rows.items.len) return null;
return self.rows.items[idx].widget();
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var app = try vxfw.App.init(allocator);
errdefer app.deinit();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const model = try allocator.create(Model);
defer allocator.destroy(model);
model.* = .{
.scroll_bars = .{
.scroll_view = .{
.children = .{
.builder = .{
.userdata = model,
.buildFn = Model.widgetBuilder,
},
},
},
// NOTE: This is not the actual content height, but rather an estimate. In reality
// you would want to do some calculations to keep this up to date and as close to
// the real value as possible, but this suffices for the sake of the example. Try
// playing around with the value to see how it affects the scrollbar. Try removing
// it as well to see what that does.
.estimated_content_height = 800,
},
.rows = std.ArrayList(ModelRow).init(allocator),
};
defer model.rows.deinit();
var lipsum = std.ArrayList([]const u8).init(allocator);
defer lipsum.deinit();
try lipsum.append(" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet nunc porta, commodo tellus eu, blandit lectus. Aliquam dignissim rhoncus mi eu ultrices. Suspendisse lectus massa, bibendum sed lorem sit amet, egestas aliquam ante. Mauris venenatis nibh neque. Nulla a mi eget purus porttitor malesuada. Sed ac porta felis. Morbi ultricies urna nisi, et maximus elit convallis a. Morbi ut felis nec orci euismod congue efficitur egestas ex. Quisque eu feugiat magna. Pellentesque porttitor tortor ut iaculis dictum. Nulla erat neque, sollicitudin vitae enim nec, pharetra blandit tortor. Sed orci ante, condimentum vitae sodales in, sodales ut nulla. Suspendisse quam felis, aliquet ut neque a, lacinia sagittis turpis. Vivamus nec dui purus. Proin tempor nisl et porttitor consequat.");
try lipsum.append(" Vivamus elit massa, commodo in laoreet nec, scelerisque ac orci. Donec nec ante sit amet nisi ullamcorper dictum quis non enim. Proin ante libero, consequat sit amet semper a, vulputate non odio. Mauris ut suscipit lacus. Mauris nec dolor id ex mollis tempor at quis ligula. Integer varius commodo ipsum id gravida. Sed ut lobortis est, id egestas nunc. In fringilla ullamcorper porttitor. Donec quis dignissim arcu, vitae sagittis tortor. Sed tempor porttitor arcu, sit amet elementum est ornare id. Morbi rhoncus, ipsum eget tincidunt volutpat, mauris enim vestibulum nibh, mollis iaculis ante enim quis enim. Donec pharetra odio vel ex fringilla, ut laoreet ipsum commodo. Praesent tempus, leo a pellentesque sodales, erat ipsum pretium nulla, id faucibus sem turpis at nibh. Aenean ut dui luctus, vehicula felis vel, aliquam nulla.");
try lipsum.append(" Cras interdum mattis elit non varius. In condimentum velit a tellus sollicitudin interdum. Etiam pulvinar semper ex, eget congue ante tristique ut. Phasellus commodo magna magna, at fermentum tortor porttitor ac. Fusce a efficitur diam, a congue ante. Mauris maximus ultrices leo, non viverra ex hendrerit eu. Donec laoreet turpis nulla, eget imperdiet tortor mollis aliquam. Donec a est eget ante consequat rhoncus.");
try lipsum.append(" Morbi facilisis libero nec viverra imperdiet. Ut dictum faucibus bibendum. Vestibulum ut nisl eu magna sollicitudin elementum vel eu ante. Phasellus euismod ligula massa, vel rutrum elit hendrerit ut. Vivamus id luctus lectus, at ullamcorper leo. Pellentesque in risus finibus, viverra ligula sed, porta nisl. Aliquam pretium accumsan placerat. Etiam a elit posuere, varius erat sed, aliquet quam. Morbi finibus gravida erat, non imperdiet dolor sollicitudin dictum. Aenean eget ullamcorper lacus, et hendrerit lorem. Quisque sed varius mauris.");
try lipsum.append(" Nullam vitae euismod mauris, eu gravida dolor. Nunc vel urna laoreet justo faucibus tempus. Vestibulum tincidunt sagittis metus ac dignissim. Curabitur eleifend dolor consequat malesuada posuere. In hac habitasse platea dictumst. Fusce eget ipsum tincidunt, placerat orci ut, malesuada ante. Vivamus ultrices purus vel orci posuere, sed posuere eros porta. Vestibulum a tellus et tortor scelerisque varius. Pellentesque vel leo sed est semper bibendum. Mauris tellus ante, cursus et nunc vitae, dictum pellentesque ex. In tristique purus felis, non efficitur ante mollis id. Nulla quam nisi, suscipit sit amet mattis vel, placerat sit amet lectus. Vestibulum cursus auctor quam, at convallis felis euismod non. Sed nec magna nisi. Morbi scelerisque accumsan nunc, sed sagittis sem varius sit amet. Maecenas arcu dui, euismod et sem quis, condimentum blandit tellus.");
try lipsum.append(" Nullam auctor lobortis libero non viverra. Mauris a imperdiet eros, a luctus est. Integer pellentesque eros et metus rhoncus egestas. Suspendisse eu risus mauris. Mauris posuere nulla in justo pharetra molestie. Maecenas sagittis at nunc et finibus. Vestibulum quis leo ac mauris malesuada vestibulum vitae eu enim. Ut et maximus elit. Pellentesque lorem felis, tristique vitae posuere vitae, auctor tempus magna. Fusce cursus purus sit amet risus pulvinar, non egestas ligula imperdiet.");
try lipsum.append(" Proin rhoncus tincidunt congue. Curabitur pretium mauris eu erat iaculis semper. Vestibulum augue tortor, vehicula id maximus at, semper eu leo. Vivamus feugiat at purus eu dapibus. Mauris luctus sollicitudin nibh, in placerat est mattis vitae. Morbi ut risus felis. Etiam lobortis mollis diam, id tempor odio sollicitudin a. Morbi congue, lacus ac accumsan consequat, ipsum eros facilisis est, in congue metus ex nec ligula. Vestibulum dolor ligula, interdum nec iaculis vel, interdum a diam. Curabitur mattis, risus at rhoncus gravida, diam est viverra diam, ut mattis augue nulla sed lacus.");
try lipsum.append(" Duis rutrum orci sit amet dui imperdiet porta. In pulvinar imperdiet enim nec tristique. Etiam egestas pulvinar arcu, viverra mollis ipsum. Ut sit amet sapien nibh. Maecenas ut velit egestas, suscipit dolor vel, interdum tellus. Pellentesque faucibus euismod risus, ac vehicula erat sodales a. Aliquam egestas sit amet enim ac posuere. In id venenatis eros, et pharetra neque. Proin facilisis, odio id vehicula elementum, sapien ligula interdum dui, quis vestibulum est quam sit amet nisl. Aliquam in orci et felis aliquet tempus quis id magna. Sed interdum malesuada sem. Proin sagittis est metus, eu vestibulum nunc lacinia in. Vestibulum enim erat, cursus at justo at, porta feugiat quam. Phasellus vestibulum finibus nulla, at egestas augue imperdiet dapibus. Nunc in felis at ante congue interdum ut nec sapien.");
try lipsum.append(" Etiam lacinia ornare mauris, ut lacinia elit sollicitudin non. Morbi cursus dictum enim, et vulputate mi sollicitudin vel. Fusce rutrum augue justo. Phasellus et mauris tincidunt erat lacinia bibendum sed eu orci. Sed nunc lectus, dignissim sit amet ultricies sit amet, efficitur eu urna. Fusce feugiat malesuada ipsum nec congue. Praesent ultrices metus eu pulvinar laoreet. Maecenas pellentesque, metus ac lobortis rhoncus, ligula eros consequat urna, eget dictum lectus sem ut orci. Donec lobortis, lacus sed bibendum auctor, odio turpis suscipit odio, vitae feugiat leo metus ac lectus. Curabitur sed sem arcu.");
try lipsum.append(" Mauris nisi tortor, auctor venenatis turpis a, finibus condimentum lectus. Donec id velit odio. Curabitur ac varius lorem. Nam cursus quam in velit gravida, in bibendum purus fermentum. Sed non rutrum dui, nec ultrices ligula. Integer lacinia blandit nisl non sollicitudin. Praesent nec malesuada eros, sit amet tincidunt nunc.");
// Try playing around with the amount of items in the scroll view to see how the scrollbar
// reacts.
for (0..10) |i| {
for (lipsum.items, 0..) |paragraph, j| {
const number = i * 10 + j;
try model.rows.append(.{ .idx = number, .text = paragraph });
}
}
try app.run(model.widget(), .{});
app.deinit();
}

View file

@ -171,7 +171,7 @@ pub fn main() !void {
},
.btm => {
if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{}) and moving) active = .mid
// Run Command and Clear Command Bar
// Run Command and Clear Command Bar
else if (key.matchExact(vaxis.Key.enter, .{}) or key.matchExact('j', .{ .ctrl = true })) {
const cmd = try cmd_input.toOwnedSlice();
defer alloc.free(cmd);

View file

@ -121,7 +121,7 @@ pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
.rows = win.height,
}
// Does the image require horizontal scaling?
// Does the image require horizontal scaling?
else if (!fit_x and fit_y)
p_opts.size = .{
.cols = win.width,

View file

@ -116,8 +116,14 @@ pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell {
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
const cell = self.buf[i];
return .{
.char = .{ .grapheme = self.buf[i].char.items },
.style = self.buf[i].style,
.char = .{ .grapheme = cell.char.items },
.style = cell.style,
.link = .{
.uri = cell.uri.items,
.params = cell.uri_id.items,
},
.default = cell.default,
};
}

View file

@ -13,6 +13,12 @@ pub const Modifiers = packed struct(u8) {
meta: bool = false,
caps_lock: bool = false,
num_lock: bool = false,
pub fn eql(self: Modifiers, other: Modifiers) bool {
const a: u8 = @bitCast(self);
const b: u8 = @bitCast(other);
return a == b;
}
};
/// Flags for the Kitty Protocol.
@ -283,6 +289,11 @@ pub const name_map = blk: {
.{ "comma", ',' },
// special keys
.{ "tab", tab },
.{ "enter", enter },
.{ "escape", escape },
.{ "space", space },
.{ "backspace", backspace },
.{ "insert", insert },
.{ "delete", delete },
.{ "left", left },

View file

@ -177,6 +177,18 @@ 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
// 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
!vx.queries_done.load(.unordered))
{
log.info("explicit width capability detected", .{});
vx.caps.explicit_width = true;
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
return;
}
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
@ -198,14 +210,41 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
vx.queries_done.store(true, .unordered);
},
.mouse => {}, // Unsupported currently
.mouse => |mouse| {
if (@hasField(Event, "mouse")) {
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
return self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
return self.postEvent(.focus_out);
}
}, // Unsupported currently
else => {},
}
},
else => {
switch (event) {
.key_press => |key| {
// Check for a cursor position response for our explicity 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
!vx.queries_done.load(.unordered))
{
log.info("explicit width capability detected", .{});
vx.caps.explicit_width = true;
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
return;
}
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
@ -297,6 +336,7 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
vx.queries_done.store(true, .unordered);
},
.winsize => |winsize| {
vx.state.in_band_resize = true;

View file

@ -592,6 +592,28 @@ inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
key.text = text_buf[0..total];
}
{
// We check if we have *only* shift, no text, and a printable character. This can
// happen when we have disambiguate on and a key is pressed and encoded as CSI u,
// for example shift + space can produce CSI 32 ; 2 u
const mod_test: Key.Modifiers = .{
.shift = true,
.caps_lock = key.mods.caps_lock,
.num_lock = key.mods.num_lock,
};
if (key.text == null and
key.mods.eql(mod_test) and
key.codepoint <= std.math.maxInt(u8) and
std.ascii.isPrint(@intCast(key.codepoint)))
{
// Encode the codepoint as upper
const upper = std.ascii.toUpper(@intCast(key.codepoint));
const n = std.unicode.utf8Encode(upper, text_buf) catch unreachable;
key.text = text_buf[0..n];
key.shifted_codepoint = upper;
}
}
const event: Event = if (is_release)
.{ .key_release = key }
else
@ -907,11 +929,12 @@ test "parse: kitty: shift+a without text reporting" {
.codepoint = 'a',
.shifted_codepoint = 'A',
.mods = .{ .shift = true },
.text = "A",
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(10, result.n);
try testing.expectEqual(expected_event, result.event);
try testing.expectEqualDeep(expected_event, result.event);
}
test "parse: kitty: alt+shift+a without text reporting" {
@ -1127,3 +1150,22 @@ test "parse(csi): mouse" {
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
test "parse: disambiguate shift + space" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[32;2u";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = ' ',
.shifted_codepoint = ' ',
.mods = .{ .shift = true },
.text = " ",
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(7, result.n);
try testing.expectEqualDeep(expected_event, result.event);
}

View file

@ -36,6 +36,7 @@ pub const Capabilities = struct {
unicode: gwidth.Method = .wcwidth,
sgr_pixels: bool = false,
color_scheme_updates: bool = false,
explicit_width: bool = false,
};
pub const Options = struct {
@ -63,6 +64,11 @@ refresh: bool = false,
/// futex times out
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),
/// If Queries were sent, we set this to false. We reset to true when all queries are complete. This
/// is used because we do explicit cursor position reports in the queries, which interfere with F3
/// key encoding. This can be used as a flag to determine how we should evaluate this sequence
queries_done: atomic.Value(bool) = atomic.Value(bool).init(true),
// images
next_img_id: u32 = 1,
@ -236,13 +242,15 @@ pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void {
try self.queryTerminalSend(tty);
// 1 second timeout
std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
self.queries_done.store(true, .unordered);
try self.enableDetectedFeatures(tty);
}
/// write queries to the terminal to determine capabilities. This function
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
/// you are using Loop.run()
pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
pub fn queryTerminalSend(vx: *Vaxis, tty: AnyWriter) !void {
vx.queries_done.store(false, .unordered);
// TODO: re-enable this
// const colorterm = std.posix.getenv("COLORTERM") orelse "";
@ -263,6 +271,15 @@ pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
ctlseqs.decrqm_unicode ++
ctlseqs.decrqm_color_scheme ++
ctlseqs.in_band_resize_set ++
// Explicit width query. We send the cursor home, then do an explicit width command, then
// query the position. If the parsed value is an F3 with shift, we support explicit width.
// The returned response will be something like \x1b[1;2R...which when parsed as a Key is a
// shift + F3 (the row is ignored). We only care if the column has moved from 1->2, which is
// why we see a Shift modifier
ctlseqs.home ++
ctlseqs.explicit_width_query ++
ctlseqs.cursor_position_request ++
ctlseqs.xtversion ++
ctlseqs.csi_u_query ++
ctlseqs.kitty_graphics_query ++
@ -302,7 +319,8 @@ pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void {
if (self.caps.kitty_keyboard) {
try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
}
if (self.caps.unicode == .unicode) {
// Only enable mode 2027 if we don't have explicit width
if (self.caps.unicode == .unicode and !self.caps.explicit_width) {
try tty.writeAll(ctlseqs.unicode_set);
}
},
@ -611,7 +629,13 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
}
try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
}
try tty.writeAll(cell.char.grapheme);
// 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 });
} else {
try tty.writeAll(cell.char.grapheme);
}
cursor_pos.col = col + w;
cursor_pos.row = row;
}

View file

@ -36,16 +36,18 @@ fn initChild(
maybe_width: ?u16,
maybe_height: ?u16,
) Window {
const width: u16 = maybe_width orelse @max(self.width - x_off, 0);
const height: u16 = maybe_height orelse @max(self.height - y_off, 0);
const max_height = @max(self.height - y_off, 0);
const max_width = @max(self.width - x_off, 0);
const width: u16 = maybe_width orelse max_width;
const height: u16 = maybe_height orelse max_height;
return Window{
.x_off = x_off + self.x_off,
.y_off = y_off + self.y_off,
.parent_x_off = @min(self.parent_x_off + x_off, 0),
.parent_y_off = @min(self.parent_y_off + y_off, 0),
.width = width,
.height = height,
.width = @min(width, max_width),
.height = @min(height, max_height),
.screen = self.screen,
};
}
@ -504,8 +506,8 @@ test "Window size set too big" {
};
const ch = parent.initChild(0, 0, 21, 21);
try std.testing.expectEqual(21, ch.width);
try std.testing.expectEqual(21, ch.height);
try std.testing.expectEqual(20, ch.width);
try std.testing.expectEqual(20, ch.height);
}
test "Window size set too big with offset" {
@ -520,8 +522,8 @@ test "Window size set too big with offset" {
};
const ch = parent.initChild(10, 10, 21, 21);
try std.testing.expectEqual(21, ch.width);
try std.testing.expectEqual(21, ch.height);
try std.testing.expectEqual(10, ch.width);
try std.testing.expectEqual(10, ch.height);
}
test "Window size nested offsets" {

View file

@ -11,6 +11,8 @@ pub const decrqm_color_scheme = "\x1b[?2031$p";
pub const csi_u_query = "\x1b[?u";
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\\";
// 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
@ -31,6 +33,7 @@ pub const sync_reset = "\x1b[?2026l";
// unicode
pub const unicode_set = "\x1b[?2027h";
pub const unicode_reset = "\x1b[?2027l";
pub const explicit_width = "\x1b]66;w={d};{s}\x1b\\";
// bracketed paste
pub const bp_set = "\x1b[?2004h";

View file

@ -1,11 +1,12 @@
const std = @import("std");
const builtin = @import("builtin");
const tty = @import("tty.zig");
pub const tty = @import("tty.zig");
pub const Vaxis = @import("Vaxis.zig");
pub const Loop = @import("Loop.zig").Loop;
pub const loop = @import("Loop.zig");
pub const Loop = loop.Loop;
pub const zigimg = @import("zigimg");
@ -47,10 +48,20 @@ pub fn init(alloc: std.mem.Allocator, opts: Vaxis.Options) !Vaxis {
return Vaxis.init(alloc, opts);
}
pub const Panic = struct {
pub const call = panic_handler;
pub const sentinelMismatch = std.debug.FormattedPanic.sentinelMismatch;
pub const unwrapError = std.debug.FormattedPanic.unwrapError;
pub const outOfBounds = std.debug.FormattedPanic.outOfBounds;
pub const startGreaterThanEnd = std.debug.FormattedPanic.startGreaterThanEnd;
pub const inactiveUnionField = std.debug.FormattedPanic.inactiveUnionField;
pub const messages = std.debug.FormattedPanic.messages;
};
/// Resets terminal state on a panic, then calls the default zig panic handler
pub fn panic_handler(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
recover();
std.builtin.default_panic(msg, error_return_trace, ret_addr);
std.debug.defaultPanic(msg, ret_addr);
}
/// Resets the terminal state using the global tty instance. Use this only to recover during a panic

View file

@ -63,7 +63,7 @@ pub const PosixTty = struct {
},
.flags = 0,
};
try posix.sigaction(posix.SIG.WINCH, &act, null);
posix.sigaction(posix.SIG.WINCH, &act, null);
handler_installed = true;
const self: PosixTty = .{
@ -97,7 +97,7 @@ pub const PosixTty = struct {
},
.flags = 0,
};
posix.sigaction(posix.SIG.WINCH, &act, null) catch {};
posix.sigaction(posix.SIG.WINCH, &act, null);
}
/// Write bytes to the tty
@ -187,19 +187,19 @@ pub const PosixTty = struct {
/// Get the window size from the kernel
pub fn getWinsize(fd: posix.fd_t) !Winsize {
var winsize = posix.winsize{
.ws_row = 0,
.ws_col = 0,
.ws_xpixel = 0,
.ws_ypixel = 0,
.row = 0,
.col = 0,
.xpixel = 0,
.ypixel = 0,
};
const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
if (posix.errno(err) == .SUCCESS)
return Winsize{
.rows = winsize.ws_row,
.cols = winsize.ws_col,
.x_pixel = winsize.ws_xpixel,
.y_pixel = winsize.ws_ypixel,
.rows = winsize.row,
.cols = winsize.col,
.x_pixel = winsize.xpixel,
.y_pixel = winsize.ypixel,
};
return error.IoctlError;
}

View file

@ -30,7 +30,12 @@ pub fn init(allocator: Allocator) !App {
return .{
.allocator = allocator,
.tty = try vaxis.Tty.init(),
.vx = try vaxis.init(allocator, .{ .system_clipboard_allocator = allocator }),
.vx = try vaxis.init(allocator, .{
.system_clipboard_allocator = allocator,
.kitty_keyboard_flags = .{
.report_events = true,
},
}),
.timers = std.ArrayList(vxfw.Tick).init(allocator),
.wants_focus = null,
};
@ -52,9 +57,13 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
// Send the init event
loop.postEvent(.init);
// Also always initialize the app with a focus event
loop.postEvent(.focus_in);
try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
try vx.setBracketedPaste(tty.anyWriter(), true);
try vx.subscribeToColorSchemeUpdates(tty.anyWriter());
{
// This part deserves a comment. loop.init installs a signal handler for the tty. We wait to
@ -78,12 +87,9 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
var buffered = tty.bufferedWriter();
var mouse_handler = MouseHandler.init(widget);
defer mouse_handler.deinit(self.allocator);
var focus_handler = FocusHandler.init(self.allocator, widget);
focus_handler.intrusiveInit();
try focus_handler.path_to_focused.append(widget);
defer focus_handler.deinit();
@ -124,11 +130,16 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
try focus_handler.handleEvent(&ctx, event);
try self.handleCommand(&ctx.cmds);
},
.focus_out => try mouse_handler.mouseExit(self, &ctx),
.focus_out => {
try mouse_handler.mouseExit(self, &ctx);
try focus_handler.handleEvent(&ctx, .focus_out);
},
.focus_in => {
try focus_handler.handleEvent(&ctx, .focus_in);
},
.mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse),
.winsize => |ws| {
try vx.resize(self.allocator, buffered.writer().any(), ws);
try buffered.flush();
try vx.resize(self.allocator, tty.anyWriter(), ws);
ctx.redraw = true;
},
else => {
@ -138,9 +149,11 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
}
}
// If we have a focus change, handle that event before we layout
if (self.wants_focus) |wants_focus| {
try focus_handler.focusWidget(&ctx, wants_focus);
try self.handleCommand(&ctx.cmds);
self.wants_focus = null;
}
// Check if we should quit
@ -149,40 +162,84 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
// Check if we need a redraw
if (!ctx.redraw) continue;
ctx.redraw = false;
// Clear the arena.
_ = arena.reset(.free_all);
// Assert that we have handled all commands
assert(ctx.cmds.items.len == 0);
_ = arena.reset(.retain_capacity);
const surface: vxfw.Surface = blk: {
// Draw the root widget
const surface = try self.doLayout(widget, &arena);
const draw_context: vxfw.DrawContext = .{
.arena = arena.allocator(),
.min = .{ .width = 0, .height = 0 },
.max = .{
.width = @intCast(vx.screen.width),
.height = @intCast(vx.screen.height),
},
// Check if any hover or mouse effects changed
try mouse_handler.updateMouse(self, surface, &ctx);
// Our focus may have changed. Handle that here
if (self.wants_focus) |wants_focus| {
try focus_handler.focusWidget(&ctx, wants_focus);
try self.handleCommand(&ctx.cmds);
self.wants_focus = null;
}
assert(ctx.cmds.items.len == 0);
if (!ctx.redraw) break :blk surface;
// If updating the mouse required a redraw, we do the layout again
break :blk try self.doLayout(widget, &arena);
};
const win = vx.window();
win.clear();
win.hideCursor();
win.setCursorShape(.default);
const surface = try widget.draw(draw_context);
const root_win = win.child(.{
.width = surface.size.width,
.height = surface.size.height,
});
surface.render(root_win, focus_handler.focused_widget);
try vx.render(buffered.writer().any());
try buffered.flush();
// Store the last frame
mouse_handler.last_frame = surface;
// Update the focus handler list
try focus_handler.update(surface);
self.wants_focus = null;
try self.render(surface, focus_handler.focused_widget);
}
}
fn doLayout(
self: *App,
widget: vxfw.Widget,
arena: *std.heap.ArenaAllocator,
) !vxfw.Surface {
const vx = &self.vx;
const draw_context: vxfw.DrawContext = .{
.arena = arena.allocator(),
.min = .{ .width = 0, .height = 0 },
.max = .{
.width = @intCast(vx.screen.width),
.height = @intCast(vx.screen.height),
},
.cell_size = .{
.width = vx.screen.width_pix / vx.screen.width,
.height = vx.screen.height_pix / vx.screen.height,
},
};
return widget.draw(draw_context);
}
fn render(
self: *App,
surface: vxfw.Surface,
focused_widget: vxfw.Widget,
) !void {
const vx = &self.vx;
const tty = &self.tty;
const win = vx.window();
win.clear();
win.hideCursor();
win.setCursorShape(.default);
const root_win = win.child(.{
.width = surface.size.width,
.height = surface.size.height,
});
surface.render(root_win, focused_widget);
var buffered = tty.bufferedWriter();
try vx.render(buffered.writer().any());
try buffered.flush();
}
fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void {
try self.timers.append(tick);
std.sort.insertion(vxfw.Tick, self.timers.items, {}, vxfw.Tick.lessThan);
@ -203,6 +260,27 @@ fn handleCommand(self: *App, cmds: *vxfw.CommandList) Allocator.Error!void {
}
};
},
.set_title => |title| {
self.vx.setTitle(self.tty.anyWriter(), title) catch |err| {
std.log.err("set_title error: {}", .{err});
};
},
.queue_refresh => self.vx.queueRefresh(),
.notify => |notification| {
self.vx.notify(self.tty.anyWriter(), notification.title, notification.body) catch |err| {
std.log.err("notify error: {}", .{err});
};
const alloc = cmds.allocator;
if (notification.title) |title| {
alloc.free(title);
}
alloc.free(notification.body);
},
.query_color => |kind| {
self.vx.queryColor(self.tty.anyWriter(), kind) catch |err| {
std.log.err("queryColor error: {}", .{err});
};
},
}
}
}
@ -211,7 +289,7 @@ fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void {
const now_ms = std.time.milliTimestamp();
// timers are always sorted descending
while (self.timers.popOrNull()) |tick| {
while (self.timers.pop()) |tick| {
if (now_ms < tick.deadline_ms) {
// re-add the timer
try self.timers.append(tick);
@ -225,6 +303,7 @@ fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void {
const MouseHandler = struct {
last_frame: vxfw.Surface,
last_hit_list: []vxfw.HitResult,
mouse: ?vaxis.Mouse,
fn init(root: Widget) MouseHandler {
return .{
@ -235,6 +314,7 @@ const MouseHandler = struct {
.children = &.{},
},
.last_hit_list = &.{},
.mouse = null,
};
}
@ -242,9 +322,74 @@ const MouseHandler = struct {
gpa.free(self.last_hit_list);
}
fn updateMouse(
self: *MouseHandler,
app: *App,
surface: vxfw.Surface,
ctx: *vxfw.EventContext,
) anyerror!void {
const mouse = self.mouse orelse return;
// For mouse events we store the last frame and use that for hit testing
const last_frame = surface;
var hits = std.ArrayList(vxfw.HitResult).init(app.allocator);
defer hits.deinit();
const sub: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 0 },
.surface = last_frame,
.z_index = 0,
};
const mouse_point: vxfw.Point = .{
.row = @intCast(mouse.row),
.col = @intCast(mouse.col),
};
if (sub.containsPoint(mouse_point)) {
try last_frame.hitTest(&hits, mouse_point);
}
// We store the hit list from the last mouse event to determine mouse_enter and mouse_leave
// events. If list a is the previous hit list, and list b is the current hit list:
// - Widgets in a but not in b get a mouse_leave event
// - Widgets in b but not in a get a mouse_enter event
// - Widgets in both receive nothing
const a = self.last_hit_list;
const b = hits.items;
// Find widgets in a but not b
for (a) |a_item| {
const a_widget = a_item.widget;
for (b) |b_item| {
const b_widget = b_item.widget;
if (a_widget.eql(b_widget)) break;
} else {
// a_item is not in b
try a_widget.handleEvent(ctx, .mouse_leave);
try app.handleCommand(&ctx.cmds);
}
}
// Widgets in b but not in a
for (b) |b_item| {
const b_widget = b_item.widget;
for (a) |a_item| {
const a_widget = a_item.widget;
if (b_widget.eql(a_widget)) break;
} else {
// b_item is not in a.
try b_widget.handleEvent(ctx, .mouse_enter);
try app.handleCommand(&ctx.cmds);
}
}
// Store a copy of this hit list for next frame
app.allocator.free(self.last_hit_list);
self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items);
}
fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void {
// For mouse events we store the last frame and use that for hit testing
const last_frame = self.last_frame;
self.mouse = mouse;
var hits = std.ArrayList(vxfw.HitResult).init(app.allocator);
defer hits.deinit();
@ -302,7 +447,7 @@ const MouseHandler = struct {
self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items);
}
const target = hits.popOrNull() orelse return;
const target = hits.pop() orelse return;
// capturing phase
ctx.phase = .capturing;
@ -330,7 +475,7 @@ const MouseHandler = struct {
// Bubbling phase
ctx.phase = .bubbling;
while (hits.popOrNull()) |item| {
while (hits.pop()) |item| {
var m_local = mouse;
m_local.col = item.local.col;
m_local.row = item.local.row;
@ -353,159 +498,39 @@ const MouseHandler = struct {
/// Maintains a tree of focusable nodes. Delivers events to the currently focused node, walking up
/// the tree until the event is handled
const FocusHandler = struct {
arena: std.heap.ArenaAllocator,
root: Node,
focused: *Node,
root: Widget,
focused_widget: vxfw.Widget,
path_to_focused: std.ArrayList(Widget),
const Node = struct {
widget: Widget,
parent: ?*Node,
children: []*Node,
fn nextSibling(self: Node) ?*Node {
const parent = self.parent orelse return null;
const idx = for (0..parent.children.len) |i| {
const node = parent.children[i];
if (self.widget.eql(node.widget))
break i;
} else unreachable;
// Return null if last child
if (idx == parent.children.len - 1)
return null
else
return parent.children[idx + 1];
}
fn prevSibling(self: Node) ?*Node {
const parent = self.parent orelse return null;
const idx = for (0..parent.children.len) |i| {
const node = parent.children[i];
if (self.widget.eql(node.widget))
break i;
} else unreachable;
// Return null if first child
if (idx == 0)
return null
else
return parent.children[idx - 1];
}
fn lastChild(self: Node) ?*Node {
if (self.children.len > 0)
return self.children[self.children.len - 1]
else
return null;
}
fn firstChild(self: Node) ?*Node {
if (self.children.len > 0)
return self.children[0]
else
return null;
}
/// returns the next logical node in the tree
fn nextNode(self: *Node) *Node {
// If we have a sibling, we return it's first descendant line
if (self.nextSibling()) |sibling| {
var node = sibling;
while (node.firstChild()) |child| {
node = child;
}
return node;
}
// If we don't have a sibling, we return our parent
if (self.parent) |parent| return parent;
// If we don't have a parent, we are the root and we return or first descendant
var node = self;
while (node.firstChild()) |child| {
node = child;
}
return node;
}
fn prevNode(self: *Node) *Node {
// If we have children, we return the last child descendant
if (self.children.len > 0) {
var node = self;
while (node.lastChild()) |child| {
node = child;
}
return node;
}
// If we have siblings, we return the last descendant line of the sibling
if (self.prevSibling()) |sibling| {
var node = sibling;
while (node.lastChild()) |child| {
node = child;
}
return node;
}
// If we don't have a sibling, we return our parent
if (self.parent) |parent| return parent;
// If we don't have a parent, we are the root and we return our last descendant
var node = self;
while (node.lastChild()) |child| {
node = child;
}
return node;
}
};
fn init(allocator: Allocator, root: Widget) FocusHandler {
const node: Node = .{
.widget = root,
.parent = null,
.children = &.{},
};
return .{
.root = node,
.focused = undefined,
.root = root,
.focused_widget = root,
.arena = std.heap.ArenaAllocator.init(allocator),
.path_to_focused = std.ArrayList(Widget).init(allocator),
};
}
fn intrusiveInit(self: *FocusHandler) void {
self.focused = &self.root;
}
fn deinit(self: *FocusHandler) void {
self.path_to_focused.deinit();
self.arena.deinit();
}
/// Update the focus list
fn update(self: *FocusHandler, root: vxfw.Surface) Allocator.Error!void {
_ = self.arena.reset(.retain_capacity);
var list = std.ArrayList(*Node).init(self.arena.allocator());
for (root.children) |child| {
try self.findFocusableChildren(&self.root, &list, child.surface);
}
// Update children
self.root.children = list.items;
// Update path
fn update(self: *FocusHandler, surface: vxfw.Surface) Allocator.Error!void {
// clear path
self.path_to_focused.clearAndFree();
if (!self.root.widget.eql(root.widget)) {
// Always make sure the root widget (the one we started with) is the first item, even if
// it isn't focusable or in the path
try self.path_to_focused.append(self.root.widget);
// Find the path to the focused widget. This builds a list that has the first element as the
// focused widget, and walks backward to the root. It's possible our focused widget is *not*
// in this tree. If this is the case, we refocus to the root widget
const has_focus = try self.childHasFocus(surface);
// We assert that the focused widget *must* be in the widget tree. There is certianly a
// logic bug in the code somewhere if this is not the case
assert(has_focus); // Focused widget not found in Surface tree
if (!self.root.eql(surface.widget)) {
// If the root of surface is not the initial widget, we append the initial widget
try self.path_to_focused.append(self.root);
}
_ = try childHasFocus(root, &self.path_to_focused, self.focused.widget);
// reverse path_to_focused so that it is root first
std.mem.reverse(Widget, self.path_to_focused.items);
@ -513,62 +538,27 @@ const FocusHandler = struct {
/// Returns true if a child of surface is the focused widget
fn childHasFocus(
self: *FocusHandler,
surface: vxfw.Surface,
list: *std.ArrayList(Widget),
focused: Widget,
) Allocator.Error!bool {
// Check if we are the focused widget
if (focused.eql(surface.widget)) {
try list.append(surface.widget);
if (self.focused_widget.eql(surface.widget)) {
try self.path_to_focused.append(surface.widget);
return true;
}
for (surface.children) |child| {
// Add child to list if it is the focused widget or one of it's own children is
if (try childHasFocus(child.surface, list, focused)) {
try list.append(surface.widget);
if (try self.childHasFocus(child.surface)) {
try self.path_to_focused.append(surface.widget);
return true;
}
}
return false;
}
/// Walks the surface tree, adding all focusable nodes to list
fn findFocusableChildren(
self: *FocusHandler,
parent: *Node,
list: *std.ArrayList(*Node),
surface: vxfw.Surface,
) Allocator.Error!void {
if (self.root.widget.eql(surface.widget)) {
// Never add the root_widget. We will always have this as the root
for (surface.children) |child| {
try self.findFocusableChildren(parent, list, child.surface);
}
} else if (surface.focusable) {
// We are a focusable child of parent. Create a new node, and find our own focusable
// children
const node = try self.arena.allocator().create(Node);
var child_list = std.ArrayList(*Node).init(self.arena.allocator());
for (surface.children) |child| {
try self.findFocusableChildren(node, &child_list, child.surface);
}
node.* = .{
.widget = surface.widget,
.parent = parent,
.children = child_list.items,
};
if (self.focused_widget.eql(surface.widget)) {
self.focused = node;
}
try list.append(node);
} else {
for (surface.children) |child| {
try self.findFocusableChildren(parent, list, child.surface);
}
}
}
fn focusWidget(self: *FocusHandler, ctx: *vxfw.EventContext, widget: vxfw.Widget) anyerror!void {
// Focusing a widget requires it to have an event handler
assert(widget.eventHandler != null);
if (self.focused_widget.eql(widget)) return;
ctx.phase = .at_target;
@ -577,45 +567,26 @@ const FocusHandler = struct {
try self.focused_widget.handleEvent(ctx, .focus_in);
}
fn focusNode(self: *FocusHandler, ctx: *vxfw.EventContext, node: *Node) anyerror!void {
if (self.focused.widget.eql(node.widget)) return;
try self.focused.widget.handleEvent(ctx, .focus_out);
self.focused = node;
try self.focused.widget.handleEvent(ctx, .focus_in);
}
/// Focuses the next focusable widget
fn focusNext(self: *FocusHandler, ctx: *vxfw.EventContext) anyerror!void {
return self.focusNode(ctx, self.focused.nextNode());
}
/// Focuses the previous focusable widget
fn focusPrev(self: *FocusHandler, ctx: *vxfw.EventContext) anyerror!void {
return self.focusNode(ctx, self.focused.prevNode());
}
fn handleEvent(self: *FocusHandler, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const path = self.path_to_focused.items;
if (path.len == 0) return;
assert(path.len > 0);
const target_idx = path.len - 1;
// Capturing phase
// Capturing phase. We send capture events from the root to the target (inclusive of target)
ctx.phase = .capturing;
for (path[0..target_idx]) |widget| {
for (path) |widget| {
try widget.captureEvent(ctx, event);
if (ctx.consume_event) return;
}
// Target phase
// Target phase. This is only sent to the target
ctx.phase = .at_target;
const target = path[target_idx];
const target = self.path_to_focused.getLast();
try target.handleEvent(ctx, event);
if (ctx.consume_event) return;
// Bubbling phase
// Bubbling phase. Bubbling phase moves from target (exclusive) to the root
ctx.phase = .bubbling;
const target_idx = path.len - 1;
var iter = std.mem.reverseIterator(path[0..target_idx]);
while (iter.next()) |widget| {
try widget.handleEvent(ctx, event);

View file

@ -85,6 +85,7 @@ test Border {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 10, .height = 10 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try border.draw(ctx);

View file

@ -107,9 +107,8 @@ pub fn draw(self: *Button, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
const center: Center = .{ .child = text.widget() };
const surf = try center.draw(ctx);
var button_surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), surf.size, surf.children);
const button_surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), surf.size, surf.children);
@memset(button_surf.buffer, .{ .style = style });
button_surf.focusable = true;
return button_surf;
}
@ -194,6 +193,7 @@ test Button {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 13, .height = 3 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try b_widget.draw(draw_ctx);

View file

@ -64,6 +64,7 @@ test Center {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 10, .height = 10 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try center.draw(ctx);
@ -88,6 +89,7 @@ test Center {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 5, .height = 3 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try center.draw(ctx);

View file

@ -35,6 +35,7 @@ pub fn draw(self: *const FlexColumn, ctx: vxfw.DrawContext) Allocator.Error!vxfw
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = ctx.max.width, .height = null },
.arena = layout_arena.allocator(),
.cell_size = ctx.cell_size,
};
// Store the inherent size of each widget
@ -83,7 +84,7 @@ pub fn draw(self: *const FlexColumn, ctx: vxfw.DrawContext) Allocator.Error!vxfw
second_pass_height += surf.size.height;
}
const size = .{ .width = max_width, .height = second_pass_height };
const size: vxfw.Size = .{ .width = max_width, .height = second_pass_height };
return .{
.size = size,
.widget = self.widget(),
@ -122,6 +123,7 @@ test FlexColumn {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 16 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try flex_widget.draw(ctx);

View file

@ -35,6 +35,7 @@ pub fn draw(self: *const FlexRow, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Su
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = null, .height = ctx.max.height },
.arena = layout_arena.allocator(),
.cell_size = ctx.cell_size,
};
var first_pass_width: u16 = 0;
@ -81,7 +82,7 @@ pub fn draw(self: *const FlexRow, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Su
max_height = @max(max_height, surf.size.height);
second_pass_width += surf.size.width;
}
const size = .{ .width = second_pass_width, .height = max_height };
const size: vxfw.Size = .{ .width = second_pass_width, .height = max_height };
return .{
.size = size,
.widget = self.widget(),
@ -120,6 +121,7 @@ test FlexRow {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 16 },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try flex_widget.draw(ctx);

View file

@ -289,10 +289,14 @@ fn drawBuilder(self: *ListView, ctx: vxfw.DrawContext, builder: Builder) Allocat
.buffer = &.{},
.children = &.{},
};
if (self.draw_cursor) {
// If we are drawing the cursor, we need to allocate a buffer so that we obscure anything
// underneath us
surface.buffer = try vxfw.Surface.createBuffer(ctx.arena, max_size);
}
// Set state
{
surface.focusable = true;
// Assume we have more. We only know we don't after drawing
self.scroll.has_more = true;
}
@ -350,14 +354,12 @@ fn drawBuilder(self: *ListView, ctx: vxfw.DrawContext, builder: Builder) Allocat
// Set up constraints. We let the child be the entire height if it wants
const child_ctx = ctx.withConstraints(
.{ .width = max_size.width - child_offset, .height = 0 },
.{ .width = max_size.width - child_offset, .height = null },
.{ .width = max_size.width -| child_offset, .height = 0 },
.{ .width = max_size.width -| child_offset, .height = null },
);
// Draw the child
var surf = try child.draw(child_ctx);
// We set the child to non-focusable so that we can manage where the keyevents go
surf.focusable = false;
const surf = try child.draw(child_ctx);
// Add the child surface to our list. It's offset from parent is the accumulated height
try child_list.append(.{
@ -401,10 +403,11 @@ fn drawBuilder(self: *ListView, ctx: vxfw.DrawContext, builder: Builder) Allocat
.surface = child.surface,
.z_index = 0,
};
const size = child.surface.size;
const cursor_surf = try vxfw.Surface.initWithChildren(
ctx.arena,
self.widget(),
.{ .width = child_offset, .height = child.surface.size.height },
.{ .width = child_offset + size.width, .height = size.height },
sub,
);
for (0..cursor_surf.size.height) |row| {
@ -541,6 +544,7 @@ test ListView {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 4 },
.cell_size = .{ .width = 10, .height = 20 },
};
var surface = try list_widget.draw(draw_ctx);
@ -712,6 +716,7 @@ test "ListView: uneven scroll" {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 4 },
.cell_size = .{ .width = 10, .height = 20 },
};
var surface = try list_widget.draw(draw_ctx);

View file

@ -86,7 +86,7 @@ pub fn draw(self: *const Padding, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Su
.origin = .{ .row = pad.top, .col = pad.left },
};
const size = .{
const size: vxfw.Size = .{
.width = child_surface.size.width + (pad.right + pad.left),
.height = child_surface.size.height + (pad.top + pad.bottom),
};
@ -121,6 +121,7 @@ test Padding {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 10, .height = 10 },
.cell_size = .{ .width = 10, .height = 20 },
};
const pad_widget = padding.widget();

View file

@ -7,10 +7,7 @@ const Allocator = std.mem.Allocator;
const RichText = @This();
pub const TextSpan = struct {
text: []const u8,
style: vaxis.Style = .{},
};
pub const TextSpan = vaxis.Segment;
text: []const TextSpan,
text_align: enum { left, center, right } = .left,
@ -32,6 +29,14 @@ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw
}
pub fn draw(self: *const RichText, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
if (ctx.max.width != null and ctx.max.width.? == 0) {
return .{
.size = ctx.min,
.widget = self.widget(),
.buffer = &.{},
.children = &.{},
};
}
var iter = try SoftwrapIterator.init(self.text, ctx);
const container_size = self.findContainerSize(&iter);
@ -166,6 +171,7 @@ pub const SoftwrapIterator = struct {
const cell: vaxis.Cell = .{
.char = .{ .grapheme = " ", .width = 1 },
.style = span.style,
.link = span.link,
};
for (0..8) |_| {
try list.append(cell);
@ -176,6 +182,7 @@ pub const SoftwrapIterator = struct {
const cell: vaxis.Cell = .{
.char = .{ .grapheme = char, .width = @intCast(width) },
.style = span.style,
.link = span.link,
};
try list.append(cell);
}
@ -364,6 +371,7 @@ test RichText {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 7, .height = 2 },
.cell_size = .{ .width = 10, .height = 20 },
};
{
@ -403,6 +411,7 @@ test "long word wrapping" {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = width, .height = null },
.cell_size = .{ .width = 10, .height = 20 },
};
const surface = try rich_widget.draw(ctx);

632
src/vxfw/ScrollBars.zig Normal file
View file

@ -0,0 +1,632 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const vxfw = @import("vxfw.zig");
const Allocator = std.mem.Allocator;
const ScrollBars = @This();
/// The ScrollBars widget must contain a ScrollView widget. The scroll bars drawn will be for the
/// scroll view contained in the ScrollBars widget.
scroll_view: vxfw.ScrollView,
/// If `true` a horizontal scroll bar will be drawn. Set to `false` to hide the horizontal scroll
/// bar. Defaults to `true`.
draw_horizontal_scrollbar: bool = true,
/// If `true` a vertical scroll bar will be drawn. Set to `false` to hide the vertical scroll bar.
/// Defaults to `true`.
draw_vertical_scrollbar: bool = true,
/// The estimated height of all the content in the ScrollView. When provided this height will be
/// used to calculate the size of the scrollbar's thumb. If this is not provided the widget will
/// make a best effort estimate of the size of the thumb using the number of elements rendered at
/// any given time. This will cause inconsistent thumb sizes - and possibly inconsistent
/// positioning - if different elements in the ScrollView have different heights. For the best user
/// experience, providing this estimate is strongly recommended.
///
/// Note that this doesn't necessarily have to be an accurate estimate and the tolerance for larger
/// views is quite forgiving, especially if you overshoot the estimate.
estimated_content_height: ?u32 = null,
/// The estimated width of all the content in the ScrollView. When provided this width will be used
/// to calculate the size of the scrollbar's thumb. If this is not provided the widget will make a
/// best effort estimate of the size of the thumb using the width of the elements rendered at any
/// given time. This will cause inconsistent thumb sizes - and possibly inconsistent positioning -
/// if different elements in the ScrollView have different widths. For the best user experience,
/// providing this estimate is strongly recommended.
///
/// Note that this doesn't necessarily have to be
/// an accurate estimate and the tolerance for larger views is quite forgiving, especially if you
/// overshoot the estimate.
estimated_content_width: ?u32 = null,
/// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must
/// have a 1 column width.
vertical_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "", .width = 1 } },
/// The cell drawn for the vertical scroll thumb while it's being hovered. Replace this to customize
/// the scroll thumb. Must have a 1 column width.
vertical_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "", .width = 1 } },
/// The cell drawn for the vertical scroll thumb while it's being dragged by the mouse. Replace this
/// to customize the scroll thumb. Must have a 1 column width.
vertical_scrollbar_drag_thumb: vaxis.Cell = .{
.char = .{ .grapheme = "", .width = 1 },
.style = .{ .fg = .{ .index = 4 } },
},
/// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must
/// have a 1 column width.
horizontal_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "", .width = 1 } },
/// The cell drawn for the horizontal scroll thumb while it's being hovered. Replace this to
/// customize the scroll thumb. Must have a 1 column width.
horizontal_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "", .width = 1 } },
/// The cell drawn for the horizontal scroll thumb while it's being dragged by the mouse. Replace
/// this to customize the scroll thumb. Must have a 1 column width.
horizontal_scrollbar_drag_thumb: vaxis.Cell = .{
.char = .{ .grapheme = "", .width = 1 },
.style = .{ .fg = .{ .index = 4 } },
},
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the size of the widget so we can locate scroll bars for mouse interaction.
last_frame_size: vxfw.Size = .{ .width = 0, .height = 0 },
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the width of the content so we map horizontal scroll thumb position to view position.
last_frame_max_content_width: u32 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the position of the mouse relative to the scroll thumb for mouse interaction.
mouse_offset_into_thumb: u8 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the position of the scroll thumb for mouse interaction.
vertical_thumb_top_row: u32 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the position of the scroll thumb for mouse interaction.
vertical_thumb_bottom_row: u32 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb.
is_hovering_vertical_thumb: bool = false,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// whether the thumb is currently being dragged, which is important to allowing the mouse to leave
/// the scroll thumb while it's being dragged.
is_dragging_vertical_thumb: bool = false,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the position of the scroll thumb for mouse interaction.
horizontal_thumb_start_col: u32 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// the position of the scroll thumb for mouse interaction.
horizontal_thumb_end_col: u32 = 0,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb.
is_hovering_horizontal_thumb: bool = false,
/// You should not change this variable, treat it as private to the implementation. Used to track
/// whether the thumb is currently being dragged, which is important to allowing the mouse to leave
/// the scroll thumb while it's being dragged.
is_dragging_horizontal_thumb: bool = false,
pub fn widget(self: *const ScrollBars) vxfw.Widget {
return .{
.userdata = @constCast(self),
.eventHandler = typeErasedEventHandler,
.captureHandler = typeErasedCaptureHandler,
.drawFn = typeErasedDrawFn,
};
}
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
return self.handleEvent(ctx, event);
}
fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
return self.handleCapture(ctx, event);
}
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
return self.draw(ctx);
}
pub fn handleCapture(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
switch (event) {
.mouse => |mouse| {
if (self.is_dragging_vertical_thumb) {
// Stop dragging the thumb when the mouse is released.
if (mouse.type == .release and
mouse.button == .left and
self.is_dragging_vertical_thumb)
{
// If we just let the scroll thumb go after dragging we need to make sure we
// redraw so the right style is immediately applied to the thumb.
if (self.is_dragging_vertical_thumb) {
self.is_dragging_vertical_thumb = false;
ctx.redraw = true;
}
const is_mouse_over_vertical_thumb =
mouse.col == self.last_frame_size.width -| 1 and
mouse.row >= self.vertical_thumb_top_row and
mouse.row < self.vertical_thumb_bottom_row;
// If we're not hovering the scroll bar after letting it go, we should trigger a
// redraw so it goes back to its narrow, non-active, state immediately.
if (!is_mouse_over_vertical_thumb) {
self.is_hovering_vertical_thumb = false;
ctx.redraw = true;
}
// No need to redraw yet, but we must consume the event so ending the drag
// action doesn't trigger some other event handler.
return ctx.consumeEvent();
}
// Process dragging the vertical thumb.
if (mouse.type == .drag) {
// Make sure we consume the event if we're currently dragging the mouse so other
// events aren't sent in the mean time.
ctx.consumeEvent();
// New scroll thumb position.
const new_thumb_top = mouse.row -| self.mouse_offset_into_thumb;
// If the new thumb position is at the top we know we've scrolled to the top of
// the scroll view.
if (new_thumb_top == 0) {
self.scroll_view.scroll.top = 0;
return ctx.consumeAndRedraw();
}
const new_thumb_top_f: f32 = @floatFromInt(new_thumb_top);
const widget_height_f: f32 = @floatFromInt(self.last_frame_size.height);
const total_num_children_f: f32 = count: {
if (self.scroll_view.item_count) |c| break :count @floatFromInt(c);
switch (self.scroll_view.children) {
.slice => |slice| break :count @floatFromInt(slice.len),
.builder => |builder| {
var counter: usize = 0;
while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_|
counter += 1;
break :count @floatFromInt(counter);
},
}
};
const new_top_child_idx_f =
new_thumb_top_f *
total_num_children_f / widget_height_f;
self.scroll_view.scroll.top = @intFromFloat(new_top_child_idx_f);
return ctx.consumeAndRedraw();
}
}
if (self.is_dragging_horizontal_thumb) {
// Stop dragging the thumb when the mouse is released.
if (mouse.type == .release and
mouse.button == .left and
self.is_dragging_horizontal_thumb)
{
// If we just let the scroll thumb go after dragging we need to make sure we
// redraw so the right style is immediately applied to the thumb.
if (self.is_dragging_horizontal_thumb) {
self.is_dragging_horizontal_thumb = false;
ctx.redraw = true;
}
const is_mouse_over_horizontal_thumb =
mouse.row == self.last_frame_size.height -| 1 and
mouse.col >= self.horizontal_thumb_start_col and
mouse.col < self.horizontal_thumb_end_col;
// If we're not hovering the scroll bar after letting it go, we should trigger a
// redraw so it goes back to its narrow, non-active, state immediately.
if (!is_mouse_over_horizontal_thumb) {
self.is_hovering_horizontal_thumb = false;
ctx.redraw = true;
}
// No need to redraw yet, but we must consume the event so ending the drag
// action doesn't trigger some other event handler.
return ctx.consumeEvent();
}
// Process dragging the horizontal thumb.
if (mouse.type == .drag) {
// Make sure we consume the event if we're currently dragging the mouse so other
// events aren't sent in the mean time.
ctx.consumeEvent();
// New scroll thumb position.
const new_thumb_col_start = mouse.col -| self.mouse_offset_into_thumb;
// If the new thumb position is at the horizontal beginning of the current view
// we know we've scrolled to the beginning of the scroll view.
if (new_thumb_col_start == 0) {
self.scroll_view.scroll.left = 0;
return ctx.consumeAndRedraw();
}
const new_thumb_col_start_f: f32 = @floatFromInt(new_thumb_col_start);
const widget_width_f: f32 = @floatFromInt(self.last_frame_size.width);
const max_content_width_f: f32 =
@floatFromInt(self.last_frame_max_content_width);
const new_view_col_start_f =
new_thumb_col_start_f * max_content_width_f / widget_width_f;
const new_view_col_start: u32 = @intFromFloat(@ceil(new_view_col_start_f));
self.scroll_view.scroll.left =
@min(new_view_col_start, self.last_frame_max_content_width);
return ctx.consumeAndRedraw();
}
}
},
else => {},
}
}
pub fn handleEvent(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
switch (event) {
.mouse => |mouse| {
// 1. Process vertical scroll thumb hover.
const is_mouse_over_vertical_thumb =
mouse.col == self.last_frame_size.width -| 1 and
mouse.row >= self.vertical_thumb_top_row and
mouse.row < self.vertical_thumb_bottom_row;
// Make sure we only update the state and redraw when it's necessary.
if (!self.is_hovering_vertical_thumb and is_mouse_over_vertical_thumb) {
self.is_hovering_vertical_thumb = true;
ctx.redraw = true;
} else if (self.is_hovering_vertical_thumb and !is_mouse_over_vertical_thumb) {
self.is_hovering_vertical_thumb = false;
ctx.redraw = true;
}
const did_start_dragging_vertical_thumb = is_mouse_over_vertical_thumb and
mouse.type == .press and mouse.button == .left;
if (did_start_dragging_vertical_thumb) {
self.is_dragging_vertical_thumb = true;
self.mouse_offset_into_thumb = @intCast(mouse.row -| self.vertical_thumb_top_row);
// No need to redraw yet, but we must consume the event.
return ctx.consumeEvent();
}
// 2. Process horizontal scroll thumb hover.
const is_mouse_over_horizontal_thumb =
mouse.row == self.last_frame_size.height -| 1 and
mouse.col >= self.horizontal_thumb_start_col and
mouse.col < self.horizontal_thumb_end_col;
// Make sure we only update the state and redraw when it's necessary.
if (!self.is_hovering_horizontal_thumb and is_mouse_over_horizontal_thumb) {
self.is_hovering_horizontal_thumb = true;
ctx.redraw = true;
} else if (self.is_hovering_horizontal_thumb and !is_mouse_over_horizontal_thumb) {
self.is_hovering_horizontal_thumb = false;
ctx.redraw = true;
}
const did_start_dragging_horizontal_thumb = is_mouse_over_horizontal_thumb and
mouse.type == .press and mouse.button == .left;
if (did_start_dragging_horizontal_thumb) {
self.is_dragging_horizontal_thumb = true;
self.mouse_offset_into_thumb = @intCast(
mouse.col -| self.horizontal_thumb_start_col,
);
// No need to redraw yet, but we must consume the event.
return ctx.consumeEvent();
}
},
.mouse_leave => self.is_dragging_vertical_thumb = false,
else => {},
}
}
pub fn draw(self: *ScrollBars, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
// 1. If we're not drawing the scrollbars we can just draw the ScrollView directly.
if (!self.draw_vertical_scrollbar and !self.draw_horizontal_scrollbar) {
try children.append(.{
.origin = .{ .row = 0, .col = 0 },
.surface = try self.scroll_view.draw(ctx),
});
return .{
.size = ctx.max.size(),
.widget = self.widget(),
.buffer = &.{},
.children = children.items,
};
}
// 2. Otherwise we can draw the scrollbars.
const max = ctx.max.size();
self.last_frame_size = max;
// 3. Draw the scroll view itself.
const scroll_view_surface = try self.scroll_view.draw(ctx.withConstraints(
ctx.min,
.{
// We make sure to make room for the scrollbars if required.
.width = max.width -| @intFromBool(self.draw_vertical_scrollbar),
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar),
},
));
try children.append(.{
.origin = .{ .row = 0, .col = 0 },
.surface = scroll_view_surface,
});
// 4. Draw the vertical scroll bar.
if (self.draw_vertical_scrollbar) vertical: {
// If we can't scroll, then there's no need to draw the scroll bar.
if (self.scroll_view.scroll.top == 0 and !self.scroll_view.scroll.has_more_vertical)
break :vertical;
// To draw the vertical scrollbar we need to know how big the scroll bar thumb should be.
// If we've been provided with an estimated height we use that to figure out how big the
// thumb should be, otherwise we estimate the size based on how many of the children were
// actually drawn in the ScrollView.
const widget_height_f: f32 = @floatFromInt(scroll_view_surface.size.height);
const total_num_children_f: f32 = count: {
if (self.scroll_view.item_count) |c| break :count @floatFromInt(c);
switch (self.scroll_view.children) {
.slice => |slice| break :count @floatFromInt(slice.len),
.builder => |builder| {
var counter: usize = 0;
while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_|
counter += 1;
break :count @floatFromInt(counter);
},
}
};
const thumb_height: u16 = height: {
// If we know the height, we can use the height of the current view to determine the
// size of the thumb.
if (self.estimated_content_height) |h| {
const content_height_f: f32 = @floatFromInt(h);
const thumb_height_f = widget_height_f * widget_height_f / content_height_f;
break :height @intFromFloat(@max(thumb_height_f, 1));
}
// Otherwise we estimate the size of the thumb based on the number of child elements
// drawn in the scroll view, and the number of total child elements.
const num_children_rendered_f: f32 = @floatFromInt(scroll_view_surface.children.len);
const thumb_height_f = widget_height_f * num_children_rendered_f / total_num_children_f;
break :height @intFromFloat(@max(thumb_height_f, 1));
};
// We also need to know the position of the thumb in the scroll bar. To find that we use the
// index of the top-most child widget rendered in the ScrollView.
const thumb_top: u32 = if (self.scroll_view.scroll.top == 0)
0
else if (self.scroll_view.scroll.has_more_vertical) pos: {
const top_child_idx_f: f32 = @floatFromInt(self.scroll_view.scroll.top);
const thumb_top_f = widget_height_f * top_child_idx_f / total_num_children_f;
break :pos @intFromFloat(thumb_top_f);
} else max.height -| thumb_height;
// Once we know the thumb height and its position we can draw the scroll bar.
const scroll_bar = try vxfw.Surface.init(
ctx.arena,
self.widget(),
.{
.width = 1,
// We make sure to make room for the horizontal scroll bar if it's being drawn.
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar),
},
);
const thumb_end_row = thumb_top + thumb_height;
for (thumb_top..thumb_end_row) |row| {
scroll_bar.writeCell(
0,
@intCast(row),
if (self.is_dragging_vertical_thumb)
self.vertical_scrollbar_drag_thumb
else if (self.is_hovering_vertical_thumb)
self.vertical_scrollbar_hover_thumb
else
self.vertical_scrollbar_thumb,
);
}
self.vertical_thumb_top_row = thumb_top;
self.vertical_thumb_bottom_row = thumb_end_row;
try children.append(.{
.origin = .{ .row = 0, .col = max.width -| 1 },
.surface = scroll_bar,
});
}
// 5. Draw the horizontal scroll bar.
const is_horizontally_scrolled = self.scroll_view.scroll.left > 0;
const has_more_horizontal_content = self.scroll_view.scroll.has_more_horizontal;
const should_draw_scrollbar = is_horizontally_scrolled or has_more_horizontal_content;
if (self.draw_horizontal_scrollbar and should_draw_scrollbar) {
const scroll_bar = try vxfw.Surface.init(
ctx.arena,
self.widget(),
.{ .width = max.width, .height = 1 },
);
const widget_width_f: f32 = @floatFromInt(max.width);
const max_content_width: u32 = width: {
if (self.estimated_content_width) |w| break :width w;
var max_content_width: u32 = 0;
for (scroll_view_surface.children) |child| {
max_content_width = @max(max_content_width, child.surface.size.width);
}
break :width max_content_width;
};
const max_content_width_f: f32 =
if (self.scroll_view.scroll.left + max.width > max_content_width)
// If we've managed to overscroll horizontally for whatever reason - for example if the
// content changes - we make sure the scroll thumb doesn't disappear by increasing the
// max content width to match the current overscrolled position.
@floatFromInt(self.scroll_view.scroll.left + max.width)
else
@floatFromInt(max_content_width);
self.last_frame_max_content_width = max_content_width;
const thumb_width_f: f32 = widget_width_f * widget_width_f / max_content_width_f;
const thumb_width: u32 = @intFromFloat(@max(thumb_width_f, 1));
const view_start_col_f: f32 = @floatFromInt(self.scroll_view.scroll.left);
const thumb_start_f = view_start_col_f * widget_width_f / max_content_width_f;
const thumb_start: u32 = @intFromFloat(thumb_start_f);
const thumb_end = thumb_start + thumb_width;
for (thumb_start..thumb_end) |col| {
scroll_bar.writeCell(
@intCast(col),
0,
if (self.is_dragging_horizontal_thumb)
self.horizontal_scrollbar_drag_thumb
else if (self.is_hovering_horizontal_thumb)
self.horizontal_scrollbar_hover_thumb
else
self.horizontal_scrollbar_thumb,
);
}
self.horizontal_thumb_start_col = thumb_start;
self.horizontal_thumb_end_col = thumb_end;
try children.append(.{
.origin = .{ .row = max.height -| 1, .col = 0 },
.surface = scroll_bar,
});
}
return .{
.size = ctx.max.size(),
.widget = self.widget(),
.buffer = &.{},
.children = children.items,
};
}
test ScrollBars {
// Create child widgets
const Text = @import("Text.zig");
const abc: Text = .{ .text = "abc\n def\n ghi" };
const def: Text = .{ .text = "def" };
const ghi: Text = .{ .text = "ghi" };
const jklmno: Text = .{ .text = "jkl\n mno" };
//
// 0 |abc|
// 1 | d|ef
// 2 | g|hi
// 3 |def|
// 4 ghi
// 5 jkl
// 6 mno
// Create the scroll view
const ScrollView = @import("ScrollView.zig");
const scroll_view: ScrollView = .{
.wheel_scroll = 1, // Set wheel scroll to one
.children = .{ .slice = &.{
abc.widget(),
def.widget(),
ghi.widget(),
jklmno.widget(),
} },
};
// Create the scroll bars.
var scroll_bars: ScrollBars = .{
.scroll_view = scroll_view,
.estimated_content_height = 7,
.estimated_content_width = 5,
};
// Boiler plate draw context
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const ucd = try vaxis.Unicode.init(arena.allocator());
vxfw.DrawContext.init(&ucd, .unicode);
const scroll_widget = scroll_bars.widget();
const draw_ctx: vxfw.DrawContext = .{
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 3, .height = 4 },
.cell_size = .{ .width = 10, .height = 20 },
};
var surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 3 children: both scrollbars and the scroll view.
try std.testing.expectEqual(3, surface.children.len);
// Hide only the horizontal scroll bar.
scroll_bars.draw_horizontal_scrollbar = false;
surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 2 children: vertical scroll bar and the scroll view.
try std.testing.expectEqual(2, surface.children.len);
// Hide only the vertical scroll bar.
scroll_bars.draw_horizontal_scrollbar = true;
scroll_bars.draw_vertical_scrollbar = false;
surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 2 children: vertical scroll bar and the scroll view.
try std.testing.expectEqual(2, surface.children.len);
// Hide both scroll bars.
scroll_bars.draw_horizontal_scrollbar = false;
surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 1 child: the scroll view.
try std.testing.expectEqual(1, surface.children.len);
// Re-enable scroll bars.
scroll_bars.draw_horizontal_scrollbar = true;
scroll_bars.draw_vertical_scrollbar = true;
// Even though the estimated size is smaller than the draw area, we still render the scroll
// bars if the scroll view knows we haven't rendered everything.
scroll_bars.estimated_content_height = 2;
scroll_bars.estimated_content_width = 1;
surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 3 children: both scrollbars and the scroll view.
try std.testing.expectEqual(3, surface.children.len);
// The scroll view should be able to tell whether the scroll bars need to be rendered or not
// even if estimated content sizes aren't provided.
scroll_bars.estimated_content_height = null;
scroll_bars.estimated_content_width = null;
surface = try scroll_widget.draw(draw_ctx);
// Scroll bars should have 3 children: both scrollbars and the scroll view.
try std.testing.expectEqual(3, surface.children.len);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

1087
src/vxfw/ScrollView.zig Normal file

File diff suppressed because it is too large Load diff

View file

@ -66,6 +66,7 @@ test SizedBox {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 16 },
.cell_size = .{ .width = 10, .height = 20 },
};
var test_widget: TestWidget = .{ .min = .{}, .max = .{} };

View file

@ -133,7 +133,12 @@ test Spinner {
try std.testing.expectEqual(1, spinner.frame);
// Simulate a draw
const surface = try spinner_widget.draw(.{ .arena = arena.allocator(), .min = .{}, .max = .{} });
const surface = try spinner_widget.draw(.{
.arena = arena.allocator(),
.min = .{},
.max = .{},
.cell_size = .{ .width = 10, .height = 20 },
});
// Spinner will try to be 1x1
try std.testing.expectEqual(1, surface.size.width);

View file

@ -18,8 +18,8 @@ max_width: ?u16 = null,
/// Target width to draw at
width: u16,
/// Statically allocated children
children: [2]vxfw.SubSurface = undefined,
/// Used to calculate mouse events when our constraint is rhs
last_max_width: ?u16 = null,
// State
pressed: bool = false,
@ -46,8 +46,13 @@ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.
const mouse = event.mouse;
const separator_col: u16 = switch (self.constrain) {
.lhs => self.width + 1,
.rhs => self.width -| 1,
.lhs => self.width,
.rhs => if (self.last_max_width) |max|
max -| self.width -| 1
else {
ctx.redraw = true;
return;
},
};
// If we are on the separator, we always set the mouse shape
@ -74,9 +79,20 @@ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.
// If pressed, we always keep the mouse shape and we update the width
if (self.pressed) {
try ctx.setMouseShape(.@"ew-resize");
self.width = @max(self.min_width, mouse.col -| 1);
if (self.max_width) |max| {
self.width = @min(self.width, max);
switch (self.constrain) {
.lhs => {
self.width = @max(self.min_width, mouse.col);
if (self.max_width) |max| {
self.width = @min(self.width, max);
}
},
.rhs => {
const last_max = self.last_max_width orelse return;
self.width = @min(last_max -| self.min_width, last_max -| mouse.col -| 1);
if (self.max_width) |max| {
self.width = @max(self.width, max);
}
},
}
ctx.consume_event = true;
}
@ -88,54 +104,81 @@ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw
const max = ctx.max.size();
// Constrain width to the max
self.width = @min(self.width, max.width);
self.last_max_width = max.width;
// The constrained side is equal to the width
const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height };
const constrained_max: vxfw.MaxSize = .{ .width = self.width, .height = max.height };
const constrained_max = vxfw.MaxSize.fromSize(constrained_min);
const unconstrained_min: vxfw.Size = .{ .width = max.width - self.width - 2, .height = max.height };
const unconstrained_max: vxfw.MaxSize = .{ .width = max.width - self.width - 2, .height = max.height };
const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height };
const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min);
var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2);
switch (self.constrain) {
.lhs => {
const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
const lhs_surface = try self.lhs.draw(lhs_ctx);
self.children[0] = .{
.surface = lhs_surface,
.origin = .{ .row = 0, .col = 0 },
};
const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
const rhs_surface = try self.rhs.draw(rhs_ctx);
self.children[1] = .{
.surface = rhs_surface,
.origin = .{ .row = 0, .col = self.width + 2 },
};
if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
const lhs_surface = try self.lhs.draw(lhs_ctx);
children.appendAssumeCapacity(.{
.surface = lhs_surface,
.origin = .{ .row = 0, .col = 0 },
});
}
if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
const rhs_surface = try self.rhs.draw(rhs_ctx);
children.appendAssumeCapacity(.{
.surface = rhs_surface,
.origin = .{ .row = 0, .col = self.width + 1 },
});
}
var surface = try vxfw.Surface.initWithChildren(
ctx.arena,
self.widget(),
max,
children.items,
);
for (0..max.height) |row| {
surface.writeCell(self.width, @intCast(row), .{
.char = .{ .grapheme = "", .width = 1 },
.style = self.style,
});
}
return surface;
},
.rhs => {
const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
const lhs_surface = try self.lhs.draw(lhs_ctx);
self.children[0] = .{
.surface = lhs_surface,
.origin = .{ .row = 0, .col = 0 },
};
const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
const rhs_surface = try self.rhs.draw(rhs_ctx);
self.children[1] = .{
.surface = rhs_surface,
.origin = .{ .row = 0, .col = self.width + 2 },
};
if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
const lhs_surface = try self.lhs.draw(lhs_ctx);
children.appendAssumeCapacity(.{
.surface = lhs_surface,
.origin = .{ .row = 0, .col = 0 },
});
}
if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
const rhs_surface = try self.rhs.draw(rhs_ctx);
children.appendAssumeCapacity(.{
.surface = rhs_surface,
.origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 },
});
}
var surface = try vxfw.Surface.initWithChildren(
ctx.arena,
self.widget(),
max,
children.items,
);
for (0..max.height) |row| {
surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{
.char = .{ .grapheme = "", .width = 1 },
.style = self.style,
});
}
return surface;
},
}
var surface = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), max, &self.children);
for (0..max.height) |row| {
surface.writeCell(self.width + 1, @intCast(row), .{
.char = .{ .grapheme = "", .width = 1 },
.style = self.style,
});
}
return surface;
}
test SplitView {
@ -149,6 +192,7 @@ test SplitView {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 16 },
.cell_size = .{ .width = 10, .height = 20 },
};
// Create LHS and RHS widgets
@ -174,8 +218,8 @@ test SplitView {
// Send the widget a mouse press on the separator
var mouse: vaxis.Mouse = .{
// The separator is width + 1
.col = split_view.width + 1,
// The separator is at width
.col = split_view.width,
.row = 0,
.type = .press,
.button = .left,
@ -197,7 +241,7 @@ test SplitView {
try split_widget.handleEvent(&ctx, .{ .mouse = mouse });
try std.testing.expect(ctx.redraw);
try std.testing.expect(split_view.pressed);
try std.testing.expectEqual(mouse.col - 1, split_view.width);
try std.testing.expectEqual(mouse.col, split_view.width);
}
test "refAllDecls" {

View file

@ -27,6 +27,14 @@ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw
}
pub fn draw(self: *const Text, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
if (ctx.max.width != null and ctx.max.width.? == 0) {
return .{
.size = ctx.min,
.widget = self.widget(),
.buffer = &.{},
.children = &.{},
};
}
const container_size = self.findContainerSize(ctx);
// Create a surface of target width and max height. We'll trim the result after drawing
@ -295,6 +303,7 @@ test "SoftwrapIterator: LF breaks" {
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = 20, .height = 10 },
.arena = arena.allocator(),
.cell_size = .{ .width = 10, .height = 20 },
};
var iter = SoftwrapIterator.init("Hello, \n world", ctx);
const first = iter.next();
@ -322,6 +331,7 @@ test "SoftwrapIterator: soft breaks that fit" {
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = 6, .height = 10 },
.arena = arena.allocator(),
.cell_size = .{ .width = 10, .height = 20 },
};
var iter = SoftwrapIterator.init("Hello, \nworld", ctx);
const first = iter.next();
@ -349,6 +359,7 @@ test "SoftwrapIterator: soft breaks that are longer than width" {
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = 6, .height = 10 },
.arena = arena.allocator(),
.cell_size = .{ .width = 10, .height = 20 },
};
var iter = SoftwrapIterator.init("very-long-word \nworld", ctx);
const first = iter.next();
@ -386,6 +397,7 @@ test "SoftwrapIterator: soft breaks with leading spaces" {
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = 6, .height = 10 },
.arena = arena.allocator(),
.cell_size = .{ .width = 10, .height = 20 },
};
var iter = SoftwrapIterator.init("Hello, \n world", ctx);
const first = iter.next();
@ -481,6 +493,7 @@ test Text {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 7, .height = 2 },
.cell_size = .{ .width = 10, .height = 20 },
};
{

View file

@ -18,6 +18,9 @@ const ellipsis: Cell.Character = .{ .grapheme = "…", .width = 1 };
// Index of our cursor
buf: Buffer,
/// Style to draw the TextField with
style: vaxis.Style = .{},
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
draw_offset: u16 = 0,
/// the column we placed the cursor the last time we drew
@ -104,7 +107,11 @@ pub fn handleEvent(self: *TextField, ctx: *vxfw.EventContext, event: vxfw.Event)
return self.checkChanged(ctx);
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
if (self.onSubmit) |onSubmit| {
try onSubmit(self.userdata, ctx, self.previous_val);
const value = try self.toOwnedSlice();
// Get a ref to the allocator in case onSubmit deinits the TextField
const allocator = self.buf.allocator;
defer allocator.free(value);
try onSubmit(self.userdata, ctx, value);
return ctx.consumeAndRedraw();
}
} else if (key.text) |text| {
@ -117,17 +124,15 @@ pub fn handleEvent(self: *TextField, ctx: *vxfw.EventContext, event: vxfw.Event)
}
fn checkChanged(self: *TextField, ctx: *vxfw.EventContext) anyerror!void {
const new = try self.buf.dupe();
if (std.mem.eql(u8, new, self.previous_val)) {
self.buf.allocator.free(new);
return ctx.consumeAndRedraw();
}
self.buf.allocator.free(self.previous_val);
self.previous_val = new;
if (self.onChange) |onChange| {
try onChange(self.userdata, ctx, new);
}
ctx.consumeAndRedraw();
const onChange = self.onChange orelse return;
const new = try self.buf.dupe();
defer {
self.buf.allocator.free(self.previous_val);
self.previous_val = new;
}
if (std.mem.eql(u8, new, self.previous_val)) return;
try onChange(self.userdata, ctx, new);
}
/// insert text at the cursor position
@ -206,11 +211,10 @@ pub fn draw(self: *TextField, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surfac
self.widget(),
.{ .width = max_width, .height = @max(ctx.min.height, 1) },
);
surface.focusable = true;
const base: vaxis.Cell = .{ .style = .{} };
const base: vaxis.Cell = .{ .style = self.style };
@memset(surface.buffer, base);
const style: vaxis.Style = .{};
const style = self.style;
const cursor_idx = self.graphemesBeforeCursor();
if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx;
if (max_width == 0) return surface;
@ -558,6 +562,7 @@ test TextField {
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 8, .height = 1 },
.cell_size = .{ .width = 10, .height = 20 },
};
_ = draw_ctx;

View file

@ -19,6 +19,8 @@ pub const FlexRow = @import("FlexRow.zig");
pub const ListView = @import("ListView.zig");
pub const Padding = @import("Padding.zig");
pub const RichText = @import("RichText.zig");
pub const ScrollView = @import("ScrollView.zig");
pub const ScrollBars = @import("ScrollBars.zig");
pub const SizedBox = @import("SizedBox.zig");
pub const SplitView = @import("SplitView.zig");
pub const Spinner = @import("Spinner.zig");
@ -79,6 +81,20 @@ pub const Command = union(enum) {
/// Try to copy the provided text to the host clipboard. Uses OSC 52. Silently fails if terminal
/// doesn't support OSC 52
copy_to_clipboard: []const u8,
/// Set the title of the terminal
set_title: []const u8,
/// Queue a refresh of the entire screen. Implicitly sets redraw
queue_refresh,
/// Send a system notification
notify: struct {
title: ?[]const u8,
body: []const u8,
},
query_color: vaxis.Cell.Color.Kind,
};
pub const EventContext = struct {
@ -127,6 +143,39 @@ pub const EventContext = struct {
pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void {
try self.addCmd(.{ .copy_to_clipboard = content });
}
pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void {
try self.addCmd(.{ .set_title = title });
}
pub fn queueRefresh(self: *EventContext) Allocator.Error!void {
try self.addCmd(.queue_refresh);
self.redraw = true;
}
/// Send a system notification. This function dupes title and body using it's own allocator.
/// They will be freed once the notification has been sent
pub fn sendNotification(
self: *EventContext,
maybe_title: ?[]const u8,
body: []const u8,
) Allocator.Error!void {
const alloc = self.cmds.allocator;
if (maybe_title) |title| {
return self.addCmd(.{ .notify = .{
.title = try alloc.dupe(u8, title),
.body = try alloc.dupe(u8, body),
} });
}
return self.addCmd(.{ .notify = .{
.title = null,
.body = try alloc.dupe(u8, body),
} });
}
pub fn queryColor(self: *EventContext, kind: vaxis.Cell.Color.Kind) Allocator.Error!void {
try self.addCmd(.{ .query_color = kind });
}
};
pub const DrawContext = struct {
@ -137,6 +186,9 @@ pub const DrawContext = struct {
min: Size,
max: MaxSize,
// Size of a single cell, in pixels
cell_size: Size,
// Unicode stuff
var unicode: ?*const vaxis.Unicode = null;
var width_method: vaxis.gwidth.Method = .unicode;
@ -165,6 +217,7 @@ pub const DrawContext = struct {
.arena = self.arena,
.min = min,
.max = max,
.cell_size = self.cell_size,
};
}
};
@ -201,6 +254,13 @@ pub const MaxSize = struct {
.height = self.height.?,
};
}
pub fn fromSize(other: Size) MaxSize {
return .{
.width = other.width,
.height = other.height,
};
}
};
/// The Widget interface
@ -275,9 +335,6 @@ pub const Surface = struct {
/// The widget this surface belongs to
widget: Widget,
/// If this widget / Surface is focusable
focusable: bool = false,
/// Cursor state
cursor: ?CursorState = null,
@ -286,6 +343,15 @@ pub const Surface = struct {
children: []SubSurface,
pub fn empty(widget: Widget) Surface {
return .{
.size = .{},
.widget = widget,
.buffer = &.{},
.children = &.{},
};
}
/// Creates a slice of vaxis.Cell's equal to size.width * size.height
pub fn createBuffer(allocator: Allocator, size: Size) Allocator.Error![]vaxis.Cell {
const buffer = try allocator.alloc(vaxis.Cell, size.width * size.height);
@ -339,7 +405,6 @@ pub const Surface = struct {
.widget = self.widget,
.buffer = self.buffer[0 .. self.size.width * height],
.children = self.children,
.focusable = self.focusable,
};
}

View file

@ -35,7 +35,7 @@ pub fn draw(self: @This(), win: vaxis.Window, y_scroll: usize) void {
const num_digits = numDigits(line);
for (0..num_digits) |i| {
const digit = extractDigit(line, i);
win.writeCell(win.width -| (i + 2), line -| (y_scroll +| 1), .{
win.writeCell(@intCast(win.width -| (i + 2)), @intCast(line -| (y_scroll +| 1)), .{
.char = .{
.width = 1,
.grapheme = digits[digit .. digit + 1],
@ -45,7 +45,7 @@ pub fn draw(self: @This(), win: vaxis.Window, y_scroll: usize) void {
}
if (highlighted) {
for (num_digits + 1..win.width) |i| {
win.writeCell(i, line -| (y_scroll +| 1), .{
win.writeCell(@intCast(i), @intCast(line -| (y_scroll +| 1)), .{
.style = if (highlighted) self.highlighted_style else self.style,
});
}

View file

@ -115,14 +115,14 @@ pub fn bounds(self: *@This(), parent: vaxis.Window) BoundingBox {
pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = b.x2 - b.x1, .height = b.y2 - b.y1 });
win.writeCell(col -| self.scroll.x, row -| self.scroll.y, cell);
const win = parent.child(.{ .width = @intCast(b.x2 - b.x1), .height = @intCast(b.y2 - b.y1) });
win.writeCell(@intCast(col -| self.scroll.x), @intCast(row -| self.scroll.y), cell);
}
/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = b.width, .height = b.height });
return win.readCell(col -| self.scroll.x, row -| self.scroll.y);
const win = parent.child(.{ .width = @intCast(b.x2 - b.x1), .height = @intCast(b.y2 - b.y1) });
return win.readCell(@intCast(col -| self.scroll.x), @intCast(row -| self.scroll.y));
}

View file

@ -29,5 +29,5 @@ pub fn draw(self: Scrollbar, win: vaxis.Window) void {
const bar_top = self.top * win.height / self.total;
var i: usize = 0;
while (i < bar_height) : (i += 1)
win.writeCell(0, i + bar_top, .{ .char = self.character, .style = self.style });
win.writeCell(0, @intCast(i + bar_top), .{ .char = self.character, .style = self.style });
}

View file

@ -133,11 +133,11 @@ pub fn drawTable(
const DataListT = @TypeOf(data_list);
const data_ti = @typeInfo(DataListT);
switch (data_ti) {
.Pointer => |ptr| {
.pointer => |ptr| {
if (ptr.size != .Slice) return error.UnsupportedTableDataType;
break :getData data_list;
},
.Struct => {
.@"struct" => {
const di_fields = meta.fields(DataListT);
const al_fields = meta.fields(std.ArrayList([]const u8));
const mal_fields = meta.fields(std.MultiArrayList(struct { a: u8 = 0, b: u32 = 0 }));
@ -168,7 +168,7 @@ pub fn drawTable(
const mal_slice = data_list.slice();
const DataT = dataType: {
const fn_info = @typeInfo(@TypeOf(@field(@TypeOf(mal_slice), "get")));
break :dataType fn_info.Fn.return_type orelse @panic("No Child Type");
break :dataType fn_info.@"fn".return_type orelse @panic("No Child Type");
};
var data_out_list = std.ArrayList(DataT).init(_alloc);
for (0..mal_slice.len) |idx| try data_out_list.append(mal_slice.get(idx));
@ -340,10 +340,10 @@ pub fn drawTable(
},
else => nonStr: {
switch (@typeInfo(ItemT)) {
.Enum => break :nonStr @tagName(item),
.Optional => {
.@"enum" => break :nonStr @tagName(item),
.optional => {
const opt_item = item orelse break :nonStr "-";
switch (@typeInfo(ItemT).Optional.child) {
switch (@typeInfo(ItemT).optional.child) {
[]const u8 => break :nonStr opt_item,
[][]const u8, []const []const u8 => {
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{s}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});

View file

@ -46,6 +46,8 @@ pub fn window(self: *View) Window {
return .{
.x_off = 0,
.y_off = 0,
.parent_x_off = 0,
.parent_y_off = 0,
.width = self.screen.width,
.height = self.screen.height,
.screen = &self.screen,
@ -69,11 +71,12 @@ pub fn draw(self: *View, win: Window, opts: DrawOptions) void {
const width = @min(win.width, self.screen.width - opts.x_off);
const height = @min(win.height, self.screen.height - opts.y_off);
for (0..height) |row| {
const src_start = opts.x_off + ((row + opts.y_off) * self.screen.width);
const src_end = src_start + width;
const dst_start = win.x_off + ((row + win.y_off) * win.screen.width);
const dst_end = dst_start + width;
for (0..height) |_row| {
const row: i17 = @intCast(_row);
const src_start: usize = @intCast(opts.x_off + ((row + opts.y_off) * self.screen.width));
const src_end: usize = @intCast(src_start + width);
const dst_start: usize = @intCast(win.x_off + ((row + win.y_off) * win.screen.width));
const dst_end: usize = @intCast(dst_start + width);
@memcpy(win.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
}
}

View file

@ -69,7 +69,7 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
},
.flags = 0,
};
try posix.sigaction(posix.SIG.CHLD, &act, null);
posix.sigaction(posix.SIG.CHLD, &act, null);
}
return;

View file

@ -27,10 +27,10 @@ pub fn deinit(self: Pty) void {
/// sets the size of the pty
pub fn setSize(self: Pty, ws: Winsize) !void {
const _ws: posix.winsize = .{
.ws_row = @truncate(ws.rows),
.ws_col = @truncate(ws.cols),
.ws_xpixel = @truncate(ws.x_pixel),
.ws_ypixel = @truncate(ws.y_pixel),
.row = @truncate(ws.rows),
.col = @truncate(ws.cols),
.xpixel = @truncate(ws.x_pixel),
.ypixel = @truncate(ws.y_pixel),
};
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
return error.SetWinsizeError;

View file

@ -169,7 +169,7 @@ pub fn spawn(self: *Terminal) !void {
try self.working_directory.appendSlice(pwd);
} else {
const pwd = std.fs.cwd();
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var buffer: [std.fs.max_path_bytes]u8 = undefined;
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
try self.working_directory.appendSlice(out_path);
}