feat(scrollable): make Container scrollable through Element Scrollable
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m35s
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m35s
This commit is contained in:
25
README.md
25
README.md
@@ -93,22 +93,25 @@ the primary use-case for myself to create this library in the first place.
|
|||||||
- [x] separators
|
- [x] separators
|
||||||
- [x] Rectangle
|
- [x] Rectangle
|
||||||
- [ ] User control
|
- [ ] User control
|
||||||
- [x] event handling
|
- [x] event loop handling
|
||||||
|
- [x] mouse support
|
||||||
- [x] user content
|
- [x] user content
|
||||||
- [ ] Default `Element` implementations
|
- [ ] Default `Element` implementations
|
||||||
- [ ] Scrollable
|
- [ ] Scrollable
|
||||||
- [ ] user input handling
|
- [x] user input handling
|
||||||
- [ ] vertical
|
- [x] vertical
|
||||||
- [ ] horizontal
|
- [x] horizontal
|
||||||
|
- [x] mouse input
|
||||||
- [ ] scroll bar(s) rendering
|
- [ ] scroll bar(s) rendering
|
||||||
- [ ] vertical
|
- [ ] vertical
|
||||||
- [ ] horizontal
|
- [ ] horizontal
|
||||||
- [ ] Content alignment (i.e. standard calculations done with the provided `Size`)
|
- [ ] Content alignment (i.e. standard calculations done with the provided `Size`)
|
||||||
- [ ] Text display
|
- [ ] Text display
|
||||||
- [ ] User input
|
- [x] User input
|
||||||
- [ ] single line
|
- [x] single line
|
||||||
- [ ] multi line
|
- [x] multi line
|
||||||
- [ ] min size? (I don't have access to the `.resize` `Event`..)
|
- [x] min size (provide size to use which would be a minimal size - as if the actual size is smaller then the `Container` will scroll and otherwise the contents expand to the available space instead?)
|
||||||
|
- [ ] image support through kitty protocol
|
||||||
|
|
||||||
Decorations should respect the layout and the viewport accordingly. This means
|
Decorations should respect the layout and the viewport accordingly. This means
|
||||||
that scrollbars are always visible (except there is no need to have a scrollbar)
|
that scrollbars are always visible (except there is no need to have a scrollbar)
|
||||||
@@ -117,6 +120,12 @@ cells of the content (and may be overwritten by child elements contents).
|
|||||||
The border of an element should be around independent of the scrolling of the
|
The border of an element should be around independent of the scrolling of the
|
||||||
contents, just like padding.
|
contents, just like padding.
|
||||||
|
|
||||||
|
For most of the `Element`s a standalone implementation would not make a lot
|
||||||
|
of sense due to the complexity of the user application states. Therefore the
|
||||||
|
library should instead provide small examples to show how you can implement
|
||||||
|
such user fields yourself and connect them using your own event system loops to
|
||||||
|
communicate with other `Container`s and/or `Element`s.
|
||||||
|
|
||||||
### Scrollable contents
|
### Scrollable contents
|
||||||
|
|
||||||
Contents that is scrollable should be done *virtually* through the contents of
|
Contents that is scrollable should be done *virtually* through the contents of
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zterm = @import("zterm");
|
const zterm = @import("zterm");
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
const App = zterm.App(union(enum) {
|
||||||
|
send: []u21,
|
||||||
|
});
|
||||||
|
|
||||||
const log = std.log.scoped(.example);
|
const log = std.log.scoped(.example);
|
||||||
|
|
||||||
pub const ExampleElement = packed struct {
|
pub const InputField = struct {
|
||||||
|
// TODO: I would need the following features:
|
||||||
|
// - current position of the input (i.e. a cursor)
|
||||||
|
// - gnu readline keybindings
|
||||||
|
// - truncate inputs?
|
||||||
|
input: std.ArrayList(u21),
|
||||||
|
/// The thread safe `App.Event` queue used to send events to the application's event loop.
|
||||||
|
queue: *App.Queue,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
||||||
|
return .{
|
||||||
|
.input = .init(allocator),
|
||||||
|
.queue = queue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(this: *@This()) void {
|
||||||
|
this.input.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{
|
return .{
|
||||||
.ptr = this,
|
.ptr = this,
|
||||||
@@ -16,27 +37,43 @@ pub const ExampleElement = packed struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// example function to render contents for a `Container`
|
// example function to handle events for a `Container`
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||||
_ = ctx;
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
switch (event) {
|
||||||
|
.key => |key| {
|
||||||
|
if (key.isAscii()) try this.input.append(key.cp);
|
||||||
|
|
||||||
// NOTE: error should only be returned here in case an in-recoverable exception has occurred
|
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
||||||
const row = size.rows / 2;
|
this.queue.push(.{ .send = try this.input.toOwnedSlice() });
|
||||||
const col = size.cols / 2 -| 3;
|
|
||||||
|
|
||||||
for (0..5) |c| {
|
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
||||||
cells[(row * size.cols) + col + c].style.fg = .black;
|
_ = this.input.popOrNull();
|
||||||
cells[(row * size.cols) + col + c].cp = '-';
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// example function to handle events for a `Container`
|
// example function to render contents for a `Container`
|
||||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||||
_ = ctx;
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||||
.init => log.debug(".init event", .{}),
|
|
||||||
else => {},
|
if (this.input.items.len == 0) return;
|
||||||
|
|
||||||
|
const row = size.rows / 2;
|
||||||
|
const col: u16 = 5;
|
||||||
|
|
||||||
|
for (this.input.items, 0..) |cp, idx| {
|
||||||
|
cells[(row * size.cols) + col + idx].style.fg = .black;
|
||||||
|
cells[(row * size.cols) + col + idx].cp = cp;
|
||||||
|
// NOTE: line wrapping happens automatically due to the way the container cells are constructed
|
||||||
|
// - it would also be possible to limit the line to cols you want to display (i.e. then only for a single row)
|
||||||
|
// - for areas you would rather go ahead and create another
|
||||||
|
// container and fit the text inside of that container (so it is dynamic to the screen size, etc.)
|
||||||
|
|
||||||
|
// NOTE: do not write over the contents of this `Container`'s `Size`
|
||||||
|
if ((row * size.cols) + col + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -58,7 +95,8 @@ pub fn main() !void {
|
|||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var element_wrapper = ExampleElement{};
|
var element_wrapper: InputField = .init(allocator, &app.queue);
|
||||||
|
defer element_wrapper.deinit();
|
||||||
const element = element_wrapper.element();
|
const element = element_wrapper.element();
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
@@ -73,7 +111,7 @@ pub fn main() !void {
|
|||||||
.padding = .all(5),
|
.padding = .all(5),
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, element);
|
}, .{});
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
.layout = .{
|
.layout = .{
|
||||||
@@ -82,15 +120,15 @@ pub fn main() !void {
|
|||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
try box.append(try App.Container.init(allocator, .{
|
|
||||||
.rectangle = .{ .fill = .light_green },
|
|
||||||
}, element));
|
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
}, element));
|
}, element));
|
||||||
|
try box.append(try App.Container.init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .light_green },
|
||||||
|
}, .{}));
|
||||||
try container.append(box);
|
try container.append(box);
|
||||||
try container.append(try App.Container.init(allocator, .{
|
try container.append(try App.Container.init(allocator, .{
|
||||||
.border = .{
|
.border = .{
|
||||||
@@ -124,6 +162,7 @@ pub fn main() !void {
|
|||||||
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||||
try app.interrupt();
|
try app.interrupt();
|
||||||
defer app.start() catch @panic("could not start app event loop");
|
defer app.start() catch @panic("could not start app event loop");
|
||||||
|
// FIX: the rendering is afterwards incorrect (wrong state of the double buffered renderer?)
|
||||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||||
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
@@ -133,6 +172,12 @@ pub fn main() !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
.send => |input| {
|
||||||
|
// NOTE: as the input in owned by the caller, this means that it still needs to be freed
|
||||||
|
defer allocator.free(input);
|
||||||
|
|
||||||
|
log.info("accepted user input: {any}", .{input});
|
||||||
|
},
|
||||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const code_point = @import("code_point");
|
|||||||
const event = @import("event.zig");
|
const event = @import("event.zig");
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const terminal = @import("terminal.zig");
|
const terminal = @import("terminal.zig");
|
||||||
|
const queue = @import("queue.zig");
|
||||||
|
|
||||||
const mergeTaggedUnions = event.mergeTaggedUnions;
|
const mergeTaggedUnions = event.mergeTaggedUnions;
|
||||||
const isTaggedUnion = event.isTaggedUnion;
|
const isTaggedUnion = event.isTaggedUnion;
|
||||||
@@ -11,7 +12,6 @@ const isTaggedUnion = event.isTaggedUnion;
|
|||||||
const Mouse = input.Mouse;
|
const Mouse = input.Mouse;
|
||||||
const Key = input.Key;
|
const Key = input.Key;
|
||||||
const Size = @import("size.zig").Size;
|
const Size = @import("size.zig").Size;
|
||||||
const Queue = @import("queue.zig").Queue;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
@@ -44,8 +44,9 @@ pub fn App(comptime E: type) type {
|
|||||||
const element = @import("element.zig");
|
const element = @import("element.zig");
|
||||||
pub const Element = element.Element(Event);
|
pub const Element = element.Element(Event);
|
||||||
pub const Scrollable = element.Scrollable(Event);
|
pub const Scrollable = element.Scrollable(Event);
|
||||||
|
pub const Queue = queue.Queue(Event, 256);
|
||||||
|
|
||||||
queue: Queue(Event, 256),
|
queue: Queue,
|
||||||
thread: ?std.Thread,
|
thread: ?std.Thread,
|
||||||
quit_event: std.Thread.ResetEvent,
|
quit_event: std.Thread.ResetEvent,
|
||||||
termios: ?std.posix.termios = null,
|
termios: ?std.posix.termios = null,
|
||||||
@@ -175,6 +176,7 @@ pub fn App(comptime E: type) type {
|
|||||||
while (true) {
|
while (true) {
|
||||||
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
|
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
|
||||||
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
|
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
|
||||||
|
// FIX: in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
|
||||||
const read_bytes = try terminal.read(buf[0..]);
|
const read_bytes = try terminal.read(buf[0..]);
|
||||||
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
||||||
// escape key presses
|
// escape key presses
|
||||||
|
|||||||
@@ -225,9 +225,8 @@ pub const Layout = packed struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn Container(comptime Event: type) type {
|
pub fn Container(comptime Event: type) type {
|
||||||
if (!isTaggedUnion(Event)) {
|
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
|
||||||
@compileError("Provided user event `Event` for `Container(comptime Event: type)`");
|
|
||||||
}
|
|
||||||
const Element = @import("element.zig").Element(Event);
|
const Element = @import("element.zig").Element(Event);
|
||||||
return struct {
|
return struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@@ -243,6 +242,7 @@ pub fn Container(comptime Event: type) type {
|
|||||||
border: Border = .{},
|
border: Border = .{},
|
||||||
rectangle: Rectangle = .{},
|
rectangle: Rectangle = .{},
|
||||||
layout: Layout = .{},
|
layout: Layout = .{},
|
||||||
|
min_size: Size = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
@@ -270,6 +270,24 @@ pub fn Container(comptime Event: type) type {
|
|||||||
try this.elements.append(element);
|
try this.elements.append(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn minSize(this: @This()) Size {
|
||||||
|
var size: Size = .{};
|
||||||
|
const len: u16 = @truncate(this.elements.items.len);
|
||||||
|
if (len > 0) {
|
||||||
|
for (this.elements.items) |element| {
|
||||||
|
size = size.merge(element.minSize());
|
||||||
|
}
|
||||||
|
switch (this.properties.layout.direction) {
|
||||||
|
.horizontal => size.cols += this.properties.layout.gap * (len - 1),
|
||||||
|
.vertical => size.rows += this.properties.layout.gap * (len - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.cols = @max(size.cols, this.properties.min_size.cols),
|
||||||
|
.rows = @max(size.rows, this.properties.min_size.rows),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle(this: *@This(), event: Event) !void {
|
pub fn handle(this: *@This(), event: Event) !void {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.resize => |size| resize: {
|
.resize => |size| resize: {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
//! Interface for Element's which describe the contents of a `Container`.
|
//! Interface for Element's which describe the contents of a `Container`.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const s = @import("size.zig");
|
const s = @import("size.zig");
|
||||||
|
const input = @import("input.zig");
|
||||||
|
|
||||||
const Container = @import("container.zig").Container;
|
const Container = @import("container.zig").Container;
|
||||||
const Cell = @import("cell.zig");
|
const Cell = @import("cell.zig");
|
||||||
|
const Mouse = input.Mouse;
|
||||||
const Position = s.Position;
|
const Position = s.Position;
|
||||||
const Size = s.Size;
|
const Size = s.Size;
|
||||||
|
|
||||||
@@ -82,9 +84,12 @@ pub fn Scrollable(Event: type) type {
|
|||||||
/// `Size` of the actual contents where the anchor and the size is
|
/// `Size` of the actual contents where the anchor and the size is
|
||||||
/// representing the size and location on screen.
|
/// representing the size and location on screen.
|
||||||
size: Size = .{},
|
size: Size = .{},
|
||||||
/// Anchor
|
/// `Size` of the `Container` content that is scrollable and mapped to
|
||||||
|
/// the *size* of the `Scrollable` `Element`.
|
||||||
|
container_size: Size = .{},
|
||||||
|
/// Anchor of the viewport of the scrollable `Container`.
|
||||||
anchor: Position = .{},
|
anchor: Position = .{},
|
||||||
/// The actual container, that is *scrollable*
|
/// The actual container, that is scrollable.
|
||||||
container: Container(Event),
|
container: Container(Event),
|
||||||
/// Enable horizontal scrolling. This also renders a scrollbar (along the bottom of the viewport).
|
/// Enable horizontal scrolling. This also renders a scrollbar (along the bottom of the viewport).
|
||||||
horizontal: bool = false,
|
horizontal: bool = false,
|
||||||
@@ -104,24 +109,42 @@ pub fn Scrollable(Event: type) type {
|
|||||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => try this.container.handle(event),
|
|
||||||
// TODO: emit `.resize` event for the container to set the size for the scrollable `Container`
|
// TODO: emit `.resize` event for the container to set the size for the scrollable `Container`
|
||||||
// - how would I determine the required or necessary `Size`?
|
// - how would I determine the required or necessary `Size`?
|
||||||
.resize => |size| {
|
.resize => |size| {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
// TODO: not just pass through the given size, but rather the size that is necessary for scrollable content
|
// TODO: not just pass through the given size, but rather the size that is necessary for scrollable content
|
||||||
try this.container.handle(.{ .resize = size });
|
const min_size = this.container.minSize();
|
||||||
|
this.container_size = .{
|
||||||
|
.anchor = size.anchor,
|
||||||
|
.cols = @max(min_size.cols, size.cols),
|
||||||
|
.rows = @max(min_size.rows, size.rows),
|
||||||
|
};
|
||||||
|
try this.container.handle(.{ .resize = this.container_size });
|
||||||
},
|
},
|
||||||
.mouse => |mouse| {
|
.mouse => |mouse| switch (mouse.button) {
|
||||||
std.log.debug("mouse event detected in scrollable element {any}", .{mouse.in(this.size)});
|
Mouse.Button.wheel_up => if (this.vertical) {
|
||||||
try this.container.handle(.{
|
this.anchor.row -|= 1;
|
||||||
|
},
|
||||||
|
Mouse.Button.wheel_down => if (this.vertical) {
|
||||||
|
const max_anchor_row = this.container_size.rows -| this.size.rows;
|
||||||
|
this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
|
||||||
|
},
|
||||||
|
Mouse.Button.wheel_left => if (this.horizontal) {
|
||||||
|
this.anchor.col -|= 1;
|
||||||
|
},
|
||||||
|
Mouse.Button.wheel_right => if (this.horizontal) {
|
||||||
|
const max_anchor_col = this.container_size.cols -| this.size.cols;
|
||||||
|
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
|
||||||
|
},
|
||||||
|
else => try this.container.handle(.{
|
||||||
.mouse = .{
|
.mouse = .{
|
||||||
.col = mouse.col + this.anchor.col,
|
.col = mouse.col + this.anchor.col,
|
||||||
.row = mouse.row + this.anchor.row,
|
.row = mouse.row + this.anchor.row,
|
||||||
.button = mouse.button,
|
.button = mouse.button,
|
||||||
.kind = mouse.kind,
|
.kind = mouse.kind,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
},
|
},
|
||||||
else => try this.container.handle(event),
|
else => try this.container.handle(event),
|
||||||
}
|
}
|
||||||
@@ -131,39 +154,41 @@ pub fn Scrollable(Event: type) type {
|
|||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
||||||
|
|
||||||
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows));
|
const container_size = this.container.size;
|
||||||
@memset(cells, .{});
|
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
||||||
{
|
{
|
||||||
const container_cells_const = try this.container.contents();
|
const container_cells_const = try this.container.contents();
|
||||||
defer this.container.allocator.free(container_cells_const);
|
defer this.container.allocator.free(container_cells_const);
|
||||||
const container_size = this.container.size;
|
|
||||||
std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
||||||
@memcpy(container_cells, container_cells_const);
|
@memcpy(container_cells, container_cells_const);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: render correct view port into the container_cells
|
||||||
for (this.container.elements.items) |*e| {
|
for (this.container.elements.items) |*e| {
|
||||||
const e_size = e.size;
|
const e_size = e.size;
|
||||||
const element_cells = try e.contents();
|
const element_cells = try e.contents();
|
||||||
defer e.allocator.free(element_cells);
|
defer e.allocator.free(element_cells);
|
||||||
|
|
||||||
const anchor = (@as(usize, e_size.anchor.row -| size.anchor.row) * @as(usize, size.cols)) + @as(usize, e_size.anchor.col -| size.anchor.col);
|
const anchor = (@as(usize, e_size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) + @as(usize, e_size.anchor.col -| container_size.anchor.col);
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
blk: for (0..e_size.rows) |row| {
|
blk: for (0..container_size.rows) |row| {
|
||||||
for (0..e_size.cols) |col| {
|
for (0..container_size.cols) |col| {
|
||||||
const cell = element_cells[idx];
|
const cell = element_cells[idx];
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
container_cells[anchor + (row * size.cols) + col].style = cell.style;
|
container_cells[anchor + (row * container_size.cols) + col].style = cell.style;
|
||||||
container_cells[anchor + (row * size.cols) + col].cp = cell.cp;
|
container_cells[anchor + (row * container_size.cols) + col].cp = cell.cp;
|
||||||
|
|
||||||
if (element_cells.len == idx) break :blk;
|
if (element_cells.len == idx) break :blk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = (@as(usize, this.anchor.row) * @as(usize, size.cols)) + @as(usize, this.anchor.col);
|
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
|
||||||
for (container_cells, 0..) |cell, idx| {
|
for (0..size.rows) |row| {
|
||||||
cells[anchor + idx] = cell;
|
for (0..size.cols) |col| {
|
||||||
|
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.container.allocator.free(container_cells);
|
this.container.allocator.free(container_cells);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,17 +60,34 @@ pub const Key = packed struct {
|
|||||||
/// ```zig
|
/// ```zig
|
||||||
/// switch (event) {
|
/// switch (event) {
|
||||||
/// .quit => break,
|
/// .quit => break,
|
||||||
/// .key => |key| {
|
/// .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit.set(),
|
||||||
/// // ctrl+c to quit
|
|
||||||
/// if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } }))
|
|
||||||
/// app.quit.set();
|
|
||||||
/// },
|
|
||||||
/// else => {},
|
/// else => {},
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn eql(this: @This(), other: @This()) bool {
|
pub fn eql(this: @This(), other: @This()) bool {
|
||||||
return std.meta.eql(this, other);
|
return std.meta.eql(this, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if the `Key` is an ascii character that can be printed to
|
||||||
|
/// the screen. This means that the code point of the `Key` is an ascii
|
||||||
|
/// character between 32 - 255 (with the exception of 127 = Delete) and no
|
||||||
|
/// modifiers (alt and/or ctrl) are used.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Get user input's from the .key event from the application event loop:
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// switch (event) {
|
||||||
|
/// .key => |key| if (key.isAscii()) try this.input.append(key.cp),
|
||||||
|
/// else => {},
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn isAscii(this: @This()) bool {
|
||||||
|
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
|
||||||
|
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
|
||||||
|
this.cp >= 128 and this.cp <= 255); // extended ascii codes
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// codepoints for keys
|
// codepoints for keys
|
||||||
|
|||||||
17
src/main.zig
17
src/main.zig
@@ -42,15 +42,13 @@ pub const HelloWorldText = packed struct {
|
|||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => log.debug(".init event", .{}),
|
.init => log.debug(".init event", .{}),
|
||||||
.key => |key| {
|
.key => |key| if (key.eql(.{ .cp = input.Space })) {
|
||||||
if (key.eql(.{ .cp = input.Space })) {
|
var next_color_idx = @intFromEnum(this.text_color);
|
||||||
var next_color_idx = @intFromEnum(this.text_color);
|
next_color_idx += 1;
|
||||||
next_color_idx += 1;
|
next_color_idx %= 17;
|
||||||
next_color_idx %= 17; // iterate over the first 16 colors (but exclude `.default` == 0)
|
if (next_color_idx == @intFromEnum(zterm.Color.default)) next_color_idx += 1;
|
||||||
if (next_color_idx == 0) next_color_idx += 1;
|
this.text_color = @enumFromInt(next_color_idx);
|
||||||
this.text_color = @enumFromInt(next_color_idx);
|
log.debug("Next color: {s}", .{@tagName(this.text_color)});
|
||||||
log.debug("Next color: {s}", .{@tagName(this.text_color)});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@@ -84,6 +82,7 @@ pub fn main() !void {
|
|||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
|
.min_size = .{ .rows = 100 },
|
||||||
}, .{});
|
}, .{});
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ pub const Size = packed struct {
|
|||||||
anchor: Position = .{},
|
anchor: Position = .{},
|
||||||
cols: u16 = 0,
|
cols: u16 = 0,
|
||||||
rows: u16 = 0,
|
rows: u16 = 0,
|
||||||
|
|
||||||
|
pub fn merge(this: @This(), other: @This()) Size {
|
||||||
|
return .{
|
||||||
|
.cols = this.cols + other.cols,
|
||||||
|
.rows = this.rows + other.rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Position = packed struct {
|
pub const Position = packed struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user