summaryrefslogtreecommitdiff
path: root/src/draw.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/draw.zig')
-rw-r--r--src/draw.zig187
1 files changed, 96 insertions, 91 deletions
diff --git a/src/draw.zig b/src/draw.zig
index 7d91ad31..7cfe7554 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -28,7 +28,7 @@ const Pane = panes.Pane;
const Text = panes.Text;
const Rect = layout.Rect;
const Region = @import("surface.zig").Region;
-const TagKind = @import("tag_layer.zig").Kind;
+const TagKind = @import("Layer.zig").Kind;
const pdf_enabled = panes.Pdf.enabled;
const wordBoundsAtCol = pardes.wordBoundsAtCol;
const BOX_H = pardes.BOX_H;
@@ -40,8 +40,6 @@ const MAX_PANES = pardes.MAX_PANES;
const MAX_COLS = pardes.MAX_COLS;
const MAX_TAG_ROWS = pardes.MAX_TAG_ROWS;
const NOTICE_LAYER_BASE = pardes.NOTICE_LAYER_BASE;
-const TAG_LINE_LAYER_BASE = pardes.TAG_LINE_LAYER_BASE;
-const HEADER_LINE_LAYER_BASE = pardes.HEADER_LINE_LAYER_BASE;
/// Where every part of the frame goes, decided once before anything is
/// painted: the page; each pane's grip, tag, body, rail (with its thumb)
@@ -172,7 +170,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// never set.
for (s.cells) |*c| c.hover = false;
for (&s.body_layers) |*layer| layer.rows = 0;
- for (&s.tag_layers) |*layer| layer.cols = 0;
+ for (&s.tag_layers) |*layer| layer.rows = 0;
s.nimages = 0;
s.npanel_tracks = 0;
s.previous_cells = &.{};
@@ -505,11 +503,9 @@ pub fn paintHeaders(p: *Pardes, s: *Surface, arena: std.mem.Allocator) !void {
tagline.revealHeader(p);
if (s.region(.workspace_tag, Region.none)) |region| {
const top = if (p.header_focus and p.header_column == null) p.header_top else 0;
- for (0..region.rect.h) |line| {
- const index = if (line == 0) MAX_PANES + MAX_COLS else HEADER_LINE_LAYER_BASE + MAX_COLS * (MAX_TAG_ROWS - 1) + line - 1;
- try renderHeaderLayer(p, arena, index, .workspace, 0, @intCast(line), .{ .x = region.rect.x, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w, .h = TOPBAR_H }, modal.lineSlice(tagline.topbar(p), line + top), chrome.tag_bg, chrome.tag_fg);
- joinHeaderLine(s, &s.tag_layers[index], chrome.tag_fg);
- }
+ const layer = &s.tag_layers[MAX_PANES + MAX_COLS];
+ try renderHeaderLayer(p, arena, layer, .workspace, 0, region.rect, tagline.topbar(p), top, chrome.tag_bg, chrome.tag_fg);
+ joinHeaderRows(s, layer, chrome.tag_fg);
}
const column_rows = p.columnBarHeight();
if (column_rows > 0) for (0..p.ncol) |c| {
@@ -531,23 +527,21 @@ pub fn paintHeaders(p: *Pardes, s: *Surface, arena: std.mem.Allocator) !void {
s.fill(p.col_x[c] + grip, region.rect.y, inset - grip, column_rows, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
}
const top = if (p.header_focus and p.header_column == c) p.header_top else 0;
- for (0..column_rows) |line| {
- const index = if (line == 0) MAX_PANES + c else HEADER_LINE_LAYER_BASE + c * (MAX_TAG_ROWS - 1) + line - 1;
- try renderHeaderLayer(p, arena, index, .column, @intCast(c), @intCast(line), .{ .x = region.rect.x, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w, .h = COLBAR_H }, modal.lineSlice(tagline.columnTag(p, c), line + top), bg, fg);
- if (p.col_x[c] < s.cols) joinHeaderLine(s, &s.tag_layers[index], fg);
- // Laid out at its full width; only shown short of the rail.
- s.tag_layers[index].viewport.w -= region.clip;
- }
+ const layer = &s.tag_layers[MAX_PANES + c];
+ try renderHeaderLayer(p, arena, layer, .column, @intCast(c), region.rect, tagline.columnTag(p, c), top, bg, fg);
+ if (p.col_x[c] < s.cols) joinHeaderRows(s, layer, fg);
+ // Laid out at its full width; only shown short of the rail.
+ layer.viewport.w -= region.clip;
};
}
-/// Copy a header line's layer into the grid under it, and its caret.
-fn joinHeaderLine(s: *Surface, layer: *const pardes.TagLayer, fg: [3]u8) void {
+/// Copy a header's layer into the grid under it, row by row, and its caret.
+fn joinHeaderRows(s: *Surface, layer: *const pardes.Layer, fg: [3]u8) void {
const v = layer.viewport;
const w = @min(v.w, s.cols -| v.x);
- joinCells(s, v.x, v.y, layer.cells[0..layer.cols], 0, w, .{ .rgb = fg });
+ for (0..layer.rows) |line| joinCells(s, v.x, v.y + @as(u16, @intCast(line)), layer.cells[line * layer.cols ..][0..layer.cols], 0, w, .{ .rgb = fg });
if (layer.cursor) |caret| {
- if (caret.col < v.w) s.cursor = .{ .x = v.x + caret.col, .y = v.y, .bar = caret.bar };
+ if (caret.x < v.w) s.cursor = .{ .x = v.x + caret.x, .y = v.y + caret.y, .bar = caret.bar };
}
}
@@ -600,77 +594,85 @@ fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct {
return .{ .lo = start + basename, .hi = start + name.len };
}
-/// One row of a header or a notice band as a tag layer: `text` is line
-/// `line` of what it shows.
-fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: TagKind, id: u16, line: u16, viewport: Rect, text: []const u8, bg: [3]u8, fg: [3]u8) !void {
+/// A header or a notice band as a tag layer: `viewport.h` rows, line `top`
+/// of `text` and on, laid out at the tagline's pitch -- as wide as its
+/// widest line and never narrower than its viewport.
+fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, kind: TagKind, id: u16, viewport: Rect, text: []const u8, top: usize, bg: [3]u8, fg: [3]u8) !void {
_ = arena;
- const layer = &p.surface.tag_layers[index];
- if (viewport.w == 0) {
- layer.cols = 0;
+ if (viewport.w == 0 or viewport.h == 0) {
+ layer.rows = 0;
return;
}
- const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), panes.File.displayWidth(text) + 1)));
- if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
+ var widest: usize = 0;
+ for (0..viewport.h) |line| widest = @max(widest, panes.File.displayWidth(modal.lineSlice(text, line + top)));
+ const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), widest + 1)));
+ const count = @as(usize, columns) * viewport.h;
+ if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count);
layer.kind = kind;
layer.id = id;
- layer.line = line;
layer.serial = if (kind == .column) p.col_serial[id] else 0;
layer.viewport = viewport;
layer.cols = columns;
+ layer.rows = viewport.h;
+ layer.context_rows = 0;
layer.bg = bg;
layer.cursor = null;
layer.slide = 0;
layer.fade = 1;
- var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells };
// A notice band carries a PANE id, not a column index, so it must not
// answer to the column being edited -- that would scroll and select
// its text with the column tag's offsets and give it a second caret.
const editing: ?*Text = if (p.header_focus and switch (kind) {
.workspace => p.header_column == null,
.column => p.header_column == id,
- .pane, .notice => false,
+ .pane, .notice, .body => false,
}) tagline.focusedHeader(p) else null;
const scroll = if (editing != null) p.header_scroll else 0;
- surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
- _ = surface.print(0, 0, columns, text[panes.File.rawAtDisplay(text, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
- // The word under the pointer: on a header even while it is edited, but
- // not on a notice (a message is not a command line), nor on a column a
- // carried column's pointer is aiming at a place over.
- const hovered = p.pointer_inside and kind != .notice and !(kind == .column and p.drag == .column_move);
- if (hovered) {
- const tag_hit = p.pointer_tag_hit;
- const col: ?usize = if (tag_hit) |pointer|
- (if (pointer.kind == kind and pointer.id == id and pointer.line == line) pointer.col else null)
- else if (p.hover_row == viewport.y and p.hover_col >= viewport.x and p.hover_col - viewport.x < viewport.w)
- p.hover_col - viewport.x
- else
- null;
- if (col) |hover| if (wordBoundsAtCol(text, panes.File.rawAtDisplay(text, hover + scroll))) |bounds| {
- var cell = panes.File.rawDisplayCol(text, bounds.lo) -| scroll;
- const end = panes.File.rawDisplayCol(text, bounds.hi) -| scroll;
- while (cell < end and cell < columns) : (cell += 1) {
- surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
- surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
- }
- };
- }
- if (editing) |t| {
- if (tagline.headerSelection(t, try edit.flatSurface(p, t), line + p.header_top)) |span| {
- var col = panes.File.rawDisplayCol(text, span.lo) -| scroll;
- const end = panes.File.rawDisplayCol(text, span.hi) -| scroll;
- while (col < end and col < columns) : (col += 1) {
- surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
- surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ for (0..viewport.h) |index| {
+ const line: u16 = @intCast(index);
+ const shown = modal.lineSlice(text, index + top);
+ var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells[index * columns ..][0..columns] };
+ surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ _ = surface.print(0, 0, columns, shown[panes.File.rawAtDisplay(shown, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
+ // The word under the pointer: on a header even while it is edited,
+ // but not on a notice (a message is not a command line), nor on a
+ // column a carried column's pointer is aiming at a place over.
+ const hovered = p.pointer_inside and kind != .notice and !(kind == .column and p.drag == .column_move);
+ if (hovered) {
+ const tag_hit = p.pointer_tag_hit;
+ const col: ?usize = if (tag_hit) |pointer|
+ (if (pointer.kind == kind and pointer.id == id and pointer.line == line) pointer.col else null)
+ else if (p.hover_row == viewport.y + line and p.hover_col >= viewport.x and p.hover_col - viewport.x < viewport.w)
+ p.hover_col - viewport.x
+ else
+ null;
+ if (col) |hover| if (wordBoundsAtCol(shown, panes.File.rawAtDisplay(shown, hover + scroll))) |bounds| {
+ var cell = panes.File.rawDisplayCol(shown, bounds.lo) -| scroll;
+ const end = panes.File.rawDisplayCol(shown, bounds.hi) -| scroll;
+ while (cell < end and cell < columns) : (cell += 1) {
+ surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
+ };
+ }
+ if (editing) |t| {
+ if (tagline.headerSelection(t, try edit.flatSurface(p, t), line + p.header_top)) |span| {
+ var col = panes.File.rawDisplayCol(shown, span.lo) -| scroll;
+ const end = panes.File.rawDisplayCol(shown, span.hi) -| scroll;
+ while (col < end and col < columns) : (col += 1) {
+ surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
}
+ const caret = panes.File.rawDisplayCol(shown, @min(@as(usize, @intCast(@max(0, t.cur_col))), shown.len)) -| scroll;
+ const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
+ if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .x = @intCast(caret), .y = line, .bar = t.mode == .insert };
}
- const caret = panes.File.rawDisplayCol(text, @min(@as(usize, @intCast(@max(0, t.cur_col))), text.len)) -| scroll;
- const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
- if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
}
}
-/// A pane's tag rows, painted once into their layers, a layer to a line,
-/// and copied into the grid; the grid's caret where the layer has it.
+/// A pane's tag rows, painted once into their layer, a row to a line, and
+/// copied into the grid; the grid's caret where the layer has it.
fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool) !void {
const capacity = p.tagCapacity(r.w -| TAG_TEXT_INSET);
// Too narrow for a layer: only the grid's gap cell.
@@ -692,23 +694,26 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r
var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = rows, .cells = storage };
try paintPaneTag(p, &temporary, arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = rows }, id, active, capacity);
const tag_fg: Color = .{ .rgb = if (active and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg };
- // A layer to a line: the first in the pane's own slot, the rest in the
- // slots kept for the lines of taller tags.
+ // One layer, a row to each line the tag shows.
+ const layer = &s.tag_layers[id];
+ const count = @as(usize, columns) * rows;
+ if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count);
+ layer.kind = .pane;
+ layer.id = @intCast(id);
+ layer.serial = pane.serial;
+ layer.viewport = .{ .x = region.rect.x + TAG_GAP, .y = region.rect.y, .w = region.rect.w - TAG_GAP - region.clip, .h = rows };
+ layer.cols = columns;
+ layer.rows = rows;
+ layer.context_rows = 0;
+ layer.bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
+ layer.slide = 0;
+ layer.fade = 1;
+ layer.cursor = if (p.header_focus) null else if (temporary.cursor) |cursor|
+ .{ .x = cursor.x - TAG_TEXT_INSET, .y = cursor.y, .bar = cursor.bar }
+ else
+ null;
for (0..rows) |line| {
- const layer = &s.tag_layers[if (line == 0) id else TAG_LINE_LAYER_BASE + id * (MAX_TAG_ROWS - 1) + line - 1];
- if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
- @memcpy(layer.cells, storage[line * pitch + TAG_TEXT_INSET ..][0..columns]);
- layer.kind = .pane;
- layer.id = @intCast(id);
- layer.serial = pane.serial;
- layer.line = @intCast(line);
- layer.viewport = .{ .x = region.rect.x + TAG_GAP, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w - TAG_GAP - region.clip, .h = BOX_H };
- layer.cols = columns;
- layer.bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
- layer.cursor = if (p.header_focus) null else if (temporary.cursor) |cursor|
- (if (cursor.y == line) .{ .col = cursor.x - TAG_TEXT_INSET, .bar = cursor.bar } else null)
- else
- null;
+ @memcpy(layer.cells[line * columns ..][0..columns], storage[line * pitch + TAG_TEXT_INSET ..][0..columns]);
// The grid's row: the gap cell and the text, as far as the pane.
joinCells(s, region.rect.x, region.rect.y + @as(u16, @intCast(line)), storage[line * pitch ..][0..pitch], config.GUTTER, region.rect.w, tag_fg);
}
@@ -765,9 +770,8 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
const line = try arena.alloc(u8, pad + kept.len);
@memset(line[0..pad], ' ');
@memcpy(line[pad..], kept);
- const index = NOTICE_LAYER_BASE + id * Pane.Notices.max + i;
- try renderHeaderLayer(p, arena, index, .notice, @intCast(id), 0, region.rect, line, bg, fg);
- const layer = &s.tag_layers[index];
+ const layer = &s.tag_layers[NOTICE_LAYER_BASE + id * Pane.Notices.max + i];
+ try renderHeaderLayer(p, arena, layer, .notice, @intCast(id), region.rect, line, 0, bg, fg);
layer.slide = motion.slide;
layer.fade = motion.alpha;
// The caret rides the band, where the prompt's glyphs are.
@@ -775,7 +779,7 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
const dropped = shown - panes.File.displayWidth(kept);
if (typed < dropped) break :caret;
- layer.cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.input.mode == .insert };
+ layer.cursor = .{ .x = @intCast(pad + typed - dropped), .y = 0, .bar = pane.input.mode == .insert };
}
// The grid shows the chip's last cells of the band, where the text ends
// (all of it where the tagline's pitch is the grid's).
@@ -785,7 +789,7 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
// no ink of its own (the layer prints it as spaces in the band's ink).
for (skip..@min(pad, skip + chip)) |col| s.at(region.rect.x + @as(u16, @intCast(col - skip)), region.rect.y).style.fg = .default;
if (layer.cursor) |caret| {
- if (caret.col >= skip) s.cursor = .{ .x = region.rect.x + caret.col - @as(u16, @intCast(skip)), .y = region.rect.y, .bar = caret.bar };
+ if (caret.x >= skip) s.cursor = .{ .x = region.rect.x + caret.x - @as(u16, @intCast(skip)), .y = region.rect.y, .bar = caret.bar };
}
// Laid out at its full width; only shown short of the rail.
layer.viewport.w -= region.clip;
@@ -960,10 +964,11 @@ test "place puts every part where the painters and layers put it, in every layou
try std.testing.expectEqual(FontRole.tagline, s.at(grip.rect.x, grip.rect.y).style.font_role);
const tag = s.region(.tag, id).?;
try std.testing.expectEqual(Rect{ .x = r.x + config.GUTTER, .y = p.tagTop(pane, r), .w = r.w - config.GUTTER, .h = pane.tag_rows }, tag.rect);
- if (r.w > TAG_TEXT_INSET) for (0..pane.tag_rows) |line| {
- const layer = &s.tag_layers[if (line == 0) id else TAG_LINE_LAYER_BASE + id * (MAX_TAG_ROWS - 1) + line - 1];
- try std.testing.expectEqual(Rect{ .x = r.x + TAG_TEXT_INSET, .y = p.tagTop(pane, r) + @as(u16, @intCast(line)), .w = r.w - TAG_TEXT_INSET, .h = BOX_H }, layer.viewport);
- };
+ // One layer, a row to each line of the tag.
+ if (r.w > TAG_TEXT_INSET) {
+ try std.testing.expectEqual(Rect{ .x = r.x + TAG_TEXT_INSET, .y = p.tagTop(pane, r), .w = r.w - TAG_TEXT_INSET, .h = pane.tag_rows }, s.tag_layers[id].viewport);
+ try std.testing.expectEqual(pane.tag_rows, s.tag_layers[id].rows);
+ }
if (pane.collapsed) {
try std.testing.expect(s.region(.body, id) == null);
try std.testing.expect(s.region(.rail, id) == null);