diff --git a/src/game/root.zig b/src/game/root.zig new file mode 100644 index 0000000..52916ff --- /dev/null +++ b/src/game/root.zig @@ -0,0 +1 @@ +pub const Text = @import("text.zig").Text; diff --git a/src/game/text.zig b/src/game/text.zig new file mode 100644 index 0000000..0ec6bfa --- /dev/null +++ b/src/game/text.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const grome = @import("../root.zig"); +const style = grome.view.style; + +const Writer = std.Io.Writer; + +const Widget = grome.view.Widget; +const WidgetError = grome.view.WidgetError; +const Cell = grome.view.Cell; +const Block = grome.container.Block; +const SubMatrix = grome.container.SubMatrix; +const Style = style.Style; + +pub const Text = struct { + // TODO: This should be a matrix instead and we should add the ability + // to turn raw text into a Matrix(Cell) + text: [1024]Cell, + text_width: usize, + widget: Widget, + + pub fn init(text: []const u8, s: ?Style) @This() { + // TODO: this is bad, but I want to see it work. + var cells: [1024]Cell = undefined; + + for (0..text.len) |x| { + cells[x] = Cell{ + .char = [_]u8{text[x]} ** 8, + .width = 1, + .style = s, + }; + } + + return .{ + .text = cells, + .text_width = text.len, + .widget = .{ + .geometryFn = @This().geometry, + .renderFn = @This().render, + }, + }; + } + + pub fn geometry(widget: *const Widget) Block { + const self: *const @This() = @fieldParentPtr("widget", widget); + return Block{ + .loc = .{ .x = 0, .y = 0 }, + .size = .{ .height = 1, .width = self.text_width }, + }; + } + + pub fn render(widget: *Widget, frame: *SubMatrix(Cell)) WidgetError!void { + const self: *@This() = @fieldParentPtr("widget", widget); + var row = frame.rowable.row(0) catch return WidgetError.UnknownError; + const ending = @min(self.text_width, row.len); + @memcpy(row[0..ending], self.text[0..ending]); + } +}; diff --git a/src/main.zig b/src/main.zig index 589ae02..9091655 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,11 +1,13 @@ const std = @import("std"); const grome = @import("root.zig"); +const style = grome.view.style; -const Io = std.Io; +const Duration = std.Io.Duration; +const Clock = std.Io.Clock; +const Writer = std.Io.File.Writer; const Rect = grome.container.Rect; -const DoubleBuffer = grome.view.DoubleBuffer; -const Duration = Io.Duration; -const Clock = Io.Clock; +const Text = grome.game.Text; +const Viewport = grome.view.Viewport; pub fn main(init: std.process.Init) !void { // This is appropriate for anything that lives as long as the process. @@ -13,39 +15,27 @@ pub fn main(init: std.process.Init) !void { // Initialize the stdout writer var stdout_buffer: [1024]u8 = undefined; - var stdout_file_writer: Io.File.Writer = .init(.stdout(), init.io, &stdout_buffer); + var stdout_file_writer: Writer = .init(.stdout(), init.io, &stdout_buffer); var w = &stdout_file_writer.interface; // alright lets play with the matrix - const size = Rect{ .height = 70, .width = 120 }; - var double_buf = try DoubleBuffer.init(arena, size); - defer double_buf.deinit(arena); - try double_buf.hard_refresh(w); + const size = Rect{ + .height = 70, + .width = 120, + }; + + var viewport = try Viewport.init(arena, size); + defer viewport.deinit(arena); + + var text = Text.init("hello Grome", style.Red); + try viewport.add_widget(arena, &text.widget); + + try viewport.hard_refresh(w); + try w.flush(); while (true) { - var frame = double_buf.frame(.{ .loc = .{ .x = 0, .y = 10 }, .size = .{ .height = 1, .width = 120 } }); - const row = try frame.rowable.row(0); - var fill_next: bool = false; - var unfilled: bool = true; - - for (row) |*cell| { - if (cell.char[0] == '>') { - fill_next = true; - cell.char[0] = ' '; - } else { - if (fill_next) { - fill_next = false; - cell.char[0] = '>'; - unfilled = false; - } - } - } - if (unfilled) { - row[0].char[0] = '>'; - } - - try double_buf.refresh(w); + try viewport.refresh(w); try w.flush(); - try init.io.sleep(Duration.fromMilliseconds(10), Clock.real); + try init.io.sleep(Duration.fromMilliseconds(30), Clock.real); } } diff --git a/src/root.zig b/src/root.zig index 8376d09..342f33d 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const container = @import("container/root.zig"); pub const view = @import("view/root.zig"); +pub const game = @import("game/root.zig"); test { std.testing.refAllDecls(@This()); diff --git a/src/view/cell.zig b/src/view/cell.zig index 03fa7b6..17018c4 100644 --- a/src/view/cell.zig +++ b/src/view/cell.zig @@ -1,17 +1,31 @@ const std = @import("std"); const mem = std.mem; +const view = @import("root.zig"); +const style = view.style; + +const Writer = std.Io.Writer; +const Style = style.Style; // Cell is a single cell in the [DoubleBuffer]. pub const Cell = struct { char: [8]u8, width: usize, + style: ?Style, /// eql returns pub fn eql(self: *const Cell, b: *const Cell) bool { return mem.eql(u8, &self.char, &b.char) and self.width == b.width; } - pub fn render(self: *const @This(), w: *std.Io.Writer) !void { + pub fn render(self: *const @This(), w: *Writer) !void { + if (self.style) |*s| { + try s.render(w); + } + _ = try w.write(self.char[0..self.width]); + + if (self.style) |_| { + try style.Reset.render(w); + } } }; diff --git a/src/view/double_buffer.zig b/src/view/double_buffer.zig index 4eaaa0b..d11822c 100644 --- a/src/view/double_buffer.zig +++ b/src/view/double_buffer.zig @@ -23,10 +23,18 @@ pub const DoubleBuffer = struct { back: Matrix(Cell), pub fn init(gpa: Allocator, size: Rect) !DoubleBuffer { - const a = try Matrix(Cell).init(gpa, .{ .char = [_]u8{' '} ** 8, .width = 1 }, size); + const a = try Matrix(Cell).init(gpa, .{ + .char = [_]u8{' '} ** 8, + .width = 1, + .style = null, + }, size); errdefer a.deinit(gpa); - const b = try Matrix(Cell).init(gpa, .{ .char = [_]u8{' '} ** 8, .width = 1 }, size); + const b = try Matrix(Cell).init(gpa, .{ + .char = [_]u8{' '} ** 8, + .width = 1, + .style = null, + }, size); return .{ .front = a, @@ -60,18 +68,19 @@ pub const DoubleBuffer = struct { for (back_row) |cell| { _ = try cell.render(w); } - _ = try w.write("\n"); + _ = try w.write("\x1b[1E"); } + _ = try w.write("\x1b[H"); + self.swap(); } pub fn refresh(self: *@This(), w: *std.Io.Writer) !void { - _ = try w.write("\x1b[H"); - var back_iter = self.back.row_iterator(); var front_iter = self.front.row_iterator(); var skip_lines: usize = 0; + var rendered: bool = false; while (true) { var back_row = back_iter.next() orelse break; @@ -88,15 +97,18 @@ pub const DoubleBuffer = struct { for (back_row[x..]) |*cell| { try cell.render(w); } - _ = try w.write("\n"); + _ = try w.write("\x1b[1E"); skip_lines = 0; + rendered = true; break; } skip_lines += 1; } - _ = try w.write("\x1b[?25l"); + if (rendered) { + _ = try w.write("\x1b[H"); + } self.swap(); } }; diff --git a/src/view/root.zig b/src/view/root.zig index 2bb80ee..0b588c6 100644 --- a/src/view/root.zig +++ b/src/view/root.zig @@ -1,5 +1,12 @@ +const widget = @import("widget.zig"); + +pub const style = @import("style.zig"); + pub const DoubleBuffer = @import("double_buffer.zig").DoubleBuffer; pub const Cell = @import("cell.zig").Cell; +pub const Widget = widget.Widget; +pub const WidgetError = widget.WidgetError; +pub const Viewport = @import("viewport.zig").Viewport; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/view/style.zig b/src/view/style.zig new file mode 100644 index 0000000..accc877 --- /dev/null +++ b/src/view/style.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +const Writer = std.Io.Writer; + +/// Style is a specific style which will write out to the writer. +pub const Style = struct { + escape: []const u8, + + pub fn render(self: @This(), w: *Writer) !void { + _ = try w.write(self.escape); + } +}; + +/// Reset is a special case that will reset all styles +pub const Reset = Style{ .escape = "\x1b[0m" }; +pub const Red = Style{ .escape = "\x1b[31m" }; diff --git a/src/view/viewport.zig b/src/view/viewport.zig index 16b77d1..17e2195 100644 --- a/src/view/viewport.zig +++ b/src/view/viewport.zig @@ -1,8 +1,56 @@ const std = @import("std"); const grome = @import("../root.zig"); +const view = @import("root.zig"); -const DoubleBuffer = grome.draw.DoubleBuffer; +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; + +const DoubleBuffer = view.DoubleBuffer; +const Widget = view.Widget; +const Rect = grome.container.Rect; pub const Viewport = struct { + /// buffer holds the double buffer which will actually write things to + /// the screen. buffer: DoubleBuffer, + + /// widgets define the widgets which will be rendered to the double + /// buffer + widgets: ArrayList(*Widget), + + pub fn init(gpa: Allocator, size: Rect) !@This() { + return .{ + .buffer = try DoubleBuffer.init(gpa, size), + .widgets = .empty, + }; + } + + pub fn hard_refresh(self: *@This(), w: *Writer) !void { + try self.render(); + try self.buffer.hard_refresh(w); + } + + pub fn deinit(self: *@This(), gpa: Allocator) void { + self.buffer.deinit(gpa); + self.widgets.deinit(gpa); + } + + pub fn render(self: *@This()) !void { + // loop through all the widgets and render them + for (self.widgets.items) |widget| { + const b = widget.geometry(); + var frame = self.buffer.frame(b); + try widget.render(&frame); + } + } + + pub fn refresh(self: *@This(), w: *Writer) !void { + try self.render(); + try self.buffer.refresh(w); + } + + pub fn add_widget(self: *@This(), gpa: Allocator, widget: *Widget) !void { + return self.widgets.append(gpa, widget); + } }; diff --git a/src/view/widget.zig b/src/view/widget.zig index e2ed6bb..12f7549 100644 --- a/src/view/widget.zig +++ b/src/view/widget.zig @@ -2,7 +2,38 @@ const std = @import("std"); const mem = std.mem; const grome = @import("../root.zig"); +const view = @import("root.zig"); + +const Block = grome.container.Block; +const SubMatrix = grome.container.SubMatrix; +const Cell = view.Cell; + +/// WidgetError is an error returned when doing things +/// and stuff +pub const WidgetError = error{ + UnknownError, +}; /// A widget is an intrusive interface that allows the implementor to draw /// a specific item to a frame. -pub const Widget = struct {}; +pub const Widget = struct { + /// geometryFn returns a Block, which specifies the location and size of the + /// submatrix which will be passed into the render function + geometryFn: *const fn (*const @This()) Block, + + /// renderFn passes in the SubMatrix which can be written to... + /// neat + renderFn: *const fn (*@This(), frame: *SubMatrix(Cell)) WidgetError!void, + + /// geometry is the driver for geometryFn for the Widget interface. + /// you must create the geometryFn. + pub fn geometry(self: *const @This()) Block { + return self.geometryFn(self); + } + + /// render is the driver for renderFn for the Widget interface. + /// you must create the renderFn. + pub fn render(self: *@This(), frame: *SubMatrix(Cell)) WidgetError!void { + return self.renderFn(self, frame); + } +};