Compare commits
80 commits
main
...
zig-0.14.0
Author | SHA1 | Date | |
---|---|---|---|
46458f2e87 | |||
![]() |
4182b7fa42 | ||
![]() |
1e24e0dfb5 | ||
![]() |
6a37605dde | ||
![]() |
33191138b0 | ||
![]() |
3a70d898d5 | ||
![]() |
6b5a011f58 | ||
![]() |
5b48d2c40c | ||
![]() |
49fcd33812 | ||
![]() |
daa735628f | ||
![]() |
91a4ff5d55 | ||
![]() |
bdaa9f9c08 | ||
![]() |
25955db06b | ||
![]() |
01605eebf6 | ||
![]() |
5a7da722a3 | ||
![]() |
00e222ce11 | ||
![]() |
8db9a92429 | ||
![]() |
5e7c1ccf34 | ||
![]() |
3f639fb364 | ||
![]() |
155f88a885 | ||
![]() |
57634d7700 | ||
![]() |
f5ed5feb56 | ||
![]() |
b9fc10f9d3 | ||
![]() |
9d01c62e8a | ||
![]() |
16fba57999 | ||
![]() |
b6043f4497 | ||
![]() |
92657ef00e | ||
![]() |
b233673f18 | ||
![]() |
8c374cc51a | ||
![]() |
0a954a536f | ||
![]() |
72f1e22333 | ||
![]() |
bcc1d027cb | ||
![]() |
af450ebb1b | ||
![]() |
4816ee20fd | ||
![]() |
cf7c2da082 | ||
![]() |
89b44f541f | ||
![]() |
cee974a2ae | ||
![]() |
4f8fe8f101 | ||
![]() |
a653e84b33 | ||
![]() |
48e9d60340 | ||
![]() |
a112fc13f3 | ||
![]() |
b2e2588e69 | ||
![]() |
572af9309a | ||
![]() |
f8672276e5 | ||
![]() |
01e7b6644b | ||
![]() |
0fb96df48e | ||
![]() |
dbf7e0bf09 | ||
![]() |
4e07fb905e | ||
![]() |
d8e5ec0d7b | ||
![]() |
682d1ce98b | ||
![]() |
208e7f7062 | ||
![]() |
0eaf6226b2 | ||
![]() |
a41d3d8cea | ||
![]() |
46cdcca260 | ||
![]() |
ee6c47c500 | ||
![]() |
9ec42325a6 | ||
![]() |
4642abdd60 | ||
![]() |
539fd55602 | ||
![]() |
38f3e7b8fe | ||
![]() |
d34c4c6c7f | ||
![]() |
6afd4786eb | ||
![]() |
d6e26f1496 | ||
![]() |
d690f80e31 | ||
![]() |
67a13bd907 | ||
![]() |
0b00376cdc | ||
![]() |
9036ee27be | ||
![]() |
a61848861c | ||
0e930e2325 | |||
98de4f451f | |||
172523bbf2 | |||
f1f45294ab | |||
![]() |
73d46b31c7 | ||
![]() |
2fd1ccef0c | ||
![]() |
335df46243 | ||
![]() |
dcf9e9f711 | ||
![]() |
f73c938e9e | ||
![]() |
25c3d1f2d3 | ||
![]() |
0c3ca95e85 | ||
![]() |
775466278c | ||
![]() |
e238840c87 |
45 changed files with 2605 additions and 408 deletions
.github/workflows
README.mdUSAGE.mdbuild.zigbuild.zig.zonexamples
src
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
|
@ -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
|
||||
|
|
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
|
@ -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 .
|
||||
|
|
|
@ -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.
|
||||
|
|
8
USAGE.md
8
USAGE.md
|
@ -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 {
|
||||
|
|
|
@ -33,6 +33,8 @@ pub fn build(b: *std.Build) void {
|
|||
fuzzy,
|
||||
image,
|
||||
main,
|
||||
scroll,
|
||||
split_view,
|
||||
table,
|
||||
text_input,
|
||||
vaxis,
|
||||
|
|
|
@ -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 = .{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
214
examples/scroll.zig
Normal 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();
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
11
src/Key.zig
11
src/Key.zig
|
@ -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 },
|
||||
|
|
42
src/Loop.zig
42
src/Loop.zig
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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";
|
||||
|
|
19
src/main.zig
19
src/main.zig
|
@ -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
|
||||
|
|
20
src/tty.zig
20
src/tty.zig
|
@ -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;
|
||||
}
|
||||
|
|
443
src/vxfw/App.zig
443
src/vxfw/App.zig
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
632
src/vxfw/ScrollBars.zig
Normal 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
1087
src/vxfw/ScrollView.zig
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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 = .{} };
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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)});
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue