summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui/gui.zig88
-rw-r--r--src/layout.zig17
-rw-r--r--src/macos.zig2
-rw-r--r--src/macos/Sources/PardesView.swift5
-rw-r--r--src/pardes.zig60
-rw-r--r--src/web.zig2
6 files changed, 111 insertions, 63 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 7f28124c..48087d0b 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1809,6 +1809,8 @@ const Gui = struct {
pointer_cell: ?MouseCell = null,
capture: bool = false,
+ /// Pixels the capture holds past the grid (PARDES_TEST_PAD).
+ capture_pad: u32 = 0,
capture_dir: []const u8 = "",
capture_tex: ?*c.SDL_GPUTexture = null,
capture_tex_w: u32 = 0,
@@ -2196,7 +2198,9 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
if (test_mode) {
const cols = envU16(env, "PARDES_TEST_COLS") orelse 80;
const rows = envU16(env, "PARDES_TEST_ROWS") orelse 24;
- _ = c.SDL_SetWindowSize(window, @intCast(cols * cell_w), @intCast(rows * cell_h));
+ // PARDES_TEST_PAD: extra pixels past the grid, as a real window has.
+ const pad = envU16(env, "PARDES_TEST_PAD") orelse 0;
+ _ = c.SDL_SetWindowSize(window, @intCast(cols * cell_w + pad), @intCast(rows * cell_h + pad));
_ = c.SDL_SyncWindow(window);
}
@@ -2284,6 +2288,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.atlas_stage = atlas_stage,
.glyphs = std.AutoHashMap(GlyphKey, Slot).init(gpa),
.capture = test_mode,
+ .capture_pad = if (test_mode) envU16(env, "PARDES_TEST_PAD") orelse 0 else 0,
.capture_dir = capture_dir orelse "",
.soft_present = soft_present,
.transparent = config.gui_transparent,
@@ -4909,8 +4914,8 @@ fn renderFrame(
var sh: u32 = 0;
var target: *c.SDL_GPUTexture = undefined;
if (g.capture) {
- sw = @as(u32, surface.cols) * g.cell_w;
- sh = @as(u32, surface.rows) * g.cell_h;
+ sw = @as(u32, surface.cols) * g.cell_w + g.capture_pad;
+ sh = @as(u32, surface.rows) * g.cell_h + g.capture_pad;
if (sw == 0 or sh == 0) {
command_consumed = true;
_ = c.SDL_SubmitGPUCommandBuffer(cmd);
@@ -5360,18 +5365,14 @@ fn bottomTaglinePresent(surface: *const pardes.Surface) bool {
return false;
}
-test "tagline bands face the topbar rule and Tagbottom faces the window edge" {
+test "tagline bands center in their cells and Tagbottom faces the window edge" {
const cell_h: u32 = 20;
const tagline_h: u32 = 16;
const canvas_h: f32 = 200;
- const border = topbarPaneBorderPixels(cell_h, tagline_h);
- try std.testing.expectEqual(@as(u32, 1), border);
- try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h, true));
- try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(0, canvas_h, cell_h, tagline_h, false));
- try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h, true));
- try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(1, canvas_h, cell_h, tagline_h, false));
- try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h, true));
- try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h, true));
+ // Workspace, column and pane rows alike: text lines up across bands.
+ for ([_]u16{ 0, 1, 2, 5, 8 }) |row|
+ try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(row, canvas_h, cell_h, tagline_h));
+ try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h));
}
fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell {
@@ -5502,7 +5503,10 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
// it comes out from under whatever is above it.
const slide = layer.slide * bh;
const top = @as(f32, @floatFromInt(layer.viewport.y)) * bh;
- emitInstance(g, instances, next.*, 0, layer.viewport.y, .{ .w = right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, &background, old_layer, false, page);
+ // A window is rarely a whole number of cells wide: a band that ends at
+ // the last column runs on through the leftover pixels to the edge.
+ const band_right = if (win_w - right < bw) @max(right, win_w) else right;
+ emitInstance(g, instances, next.*, 0, layer.viewport.y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, &background, old_layer, false, page);
if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
next.* += 1;
for (0..tagLayerCellCount(g, layer)) |col| {
@@ -5578,7 +5582,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
if (left + @as(f32, @floatFromInt(col)) * width >= right) break;
const cell = &layer.cells[index * layer.cols + col];
const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == row else false;
- const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height, true))) else 0);
+ const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height))) else 0);
emitInstance(g, instances, next.*, 0, 0, .{ .w = width, .h = height, .x_off = left + @as(f32, @floatFromInt(col)) * width, .y_off = glyph_top }, win_w, win_h, track, role, cell, old_layer, cursor, page);
clipBodyInstance(&instances[next.*], bottom, win_h);
clipInstanceRight(&instances[next.*], right, win_w);
@@ -5628,6 +5632,10 @@ fn paneGripCell(layers: []const pardes.TagLayer, cells: []const pardes.Cell, col
return null;
}
+fn anchorInset(g: *const Gui, row: u16, win_h: f32) u32 {
+ return taglineBandOffset(row, win_h, g.cell_h, g.tagline_height);
+}
+
fn emitSurfaceCell(
g: *Gui,
core: ?*const pardes.Pardes,
@@ -5657,19 +5665,27 @@ fn emitSurfaceCell(
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, cell, old_layer, is_cursor, page);
+ // An anchor starts at its column's rule: its left edge is inset by
+ // its top margin so the band frames it on the left as above and
+ // below, and its mark moves right by half that to stay centered.
+ const inset: u32 = if (core) |p| if (std.mem.indexOfScalar(u16, p.col_x[0..p.ncol], col -| (if (grip) |indicator| indicator.offset else 0)) != null) anchorInset(g, row, win_h) else 0 else 0;
if (grip) |indicator| {
const cached = cachedGlyph(g, cellCodepoint(indicator.cell), .tagline, GlyphDecoration.fromCell(indicator.cell), true);
const slot = cached orelse g.space_slot;
- const offset: u32 = if (cached != null) @as(u32, indicator.offset) * g.cell_w else 0;
+ const offset: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, indicator.offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0;
const resolved = resolveCell(g, indicator.cell, .tagline, false, page);
instances[next.*].fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255;
instances[next.*].fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255;
instances[next.*].fb = @as(f32, @floatFromInt(resolved.fg[2])) / 255;
- instances[next.*].u0 = @as(f32, @floatFromInt(slot.u + offset)) / atlas_w;
+ instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + offset) / atlas_w;
instances[next.*].v0 = @as(f32, @floatFromInt(slot.v)) / atlas_h;
instances[next.*].v1 = @as(f32, @floatFromInt(slot.v + g.tagline_height)) / atlas_h;
}
instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w;
+ if (inset > 0 and (grip == null or grip.?.offset == 0)) {
+ instances[next.*].x0 += @as(f32, @floatFromInt(inset)) / win_w * 2;
+ instances[next.*].u0 += @as(f32, @floatFromInt(inset)) / atlas_w;
+ }
next.* += 1;
return;
}
@@ -5690,7 +5706,8 @@ test "small tagline fonts leave no background gutters between adjacent tags" {
var instances: [6]CellInstance = undefined;
var next: u32 = 0;
for (0..3) |row| {
- emitSurfaceCell(&g, core, &instances, &next, 0, @intCast(row), fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default));
+ // Column 1: past the anchor's inset edge at the column's left.
+ emitSurfaceCell(&g, core, &instances, &next, 1, @intCast(row), fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default));
const base = instances[row * 2];
const ink = instances[row * 2 + 1];
try std.testing.expectApproxEqAbs(@as(f32, 20.0 / 480.0 * 2), base.y0 - base.y1, 0.0001);
@@ -5700,6 +5717,13 @@ test "small tagline fonts leave no background gutters between adjacent tags" {
try std.testing.expectApproxEqAbs(@as(f32, 10) / atlas_w, ink.u1 - ink.u0, 0.0001);
if (row > 0) try std.testing.expectApproxEqAbs(instances[(row - 1) * 2].y1, base.y0, 0.0001);
}
+ // Column 0 is an anchor's edge: its ink is inset by the band's margin
+ // (2 px here) and its underlay still covers the whole cell.
+ next = 0;
+ emitSurfaceCell(&g, core, &instances, &next, 0, 1, fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default));
+ try std.testing.expectApproxEqAbs(@as(f32, 10.0 / 800.0 * 2), instances[0].x1 - instances[0].x0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, 8.0 / 800.0 * 2), instances[1].x1 - instances[1].x0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, 8) / atlas_w, instances[1].u1 - instances[1].u0, 0.0001);
next = 0;
emitSurfaceCell(&g, null, &instances, &next, 0, 2, fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, true, false, Ground.opaqueRgb(bg_default));
try std.testing.expectEqual(@as(u32, 2), next);
@@ -5730,7 +5754,7 @@ fn emitInstance(
else
layout.h;
const band_offset: f32 = if (resolved.role == .tagline)
- @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height, true))
+ @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height))
else
0;
const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h + band_offset;
@@ -6562,7 +6586,7 @@ fn addCursorBar(
const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w;
const height = std.math.clamp(visual_height, 1.0, layout.h);
const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h +
- @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height), true)));
+ @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height))));
builder.addRect(
x0,
y0,
@@ -6662,7 +6686,7 @@ fn buildOverlay(
const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
const x = left + @as(f32, @floatFromInt(cursor.col)) * width;
if (x >= right) continue;
- const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height, true)));
+ const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height)));
builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[cursor.col], .tagline)));
};
if (transient_on) appendTouchOverlay(g, &builder, debug_on);
@@ -6683,7 +6707,7 @@ fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pard
if (cellCodepoint(cell) != ' ') occupied = i + 1;
}
const editing = p.topbar_col != null and p.header_column == null;
- const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height, true));
+ const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height));
const region = if (occupied >= surface.cols) null else pet.lane(
occupied,
@floatFromInt(g.tagline_width),
@@ -6810,7 +6834,10 @@ fn appendPaneChrome(
} else true;
if (tag_visible) {
const rule_y = @as(f32, @floatFromInt(tag_y + @as(u16, if (core.settings.tag_bottom) 0 else 1))) * layout.h;
- builder.addRect(x0, rule_y - @as(f32, if (core.settings.tag_bottom) 0 else 1), x1, rule_y + @as(f32, if (core.settings.tag_bottom) 1 else 0), overlayRgb(chrome.border));
+ // Like the band above it, the rule of a pane at the right edge
+ // runs on through the pixels past the last whole cell.
+ const rule_x1 = if (builder.win_w - x1 < layout.w) @max(x1, builder.win_w) else x1;
+ builder.addRect(x0, rule_y - @as(f32, if (core.settings.tag_bottom) 0 else 1), rule_x1, rule_y + @as(f32, if (core.settings.tag_bottom) 1 else 0), overlayRgb(chrome.border));
}
// Do not paint over resize previews or debug text occupying the rail.
@@ -6827,12 +6854,13 @@ fn appendPaneChrome(
}
if (row != body_end) continue;
const y0 = @as(f32, @floatFromInt(body_y)) * layout.h;
- const y1 = @as(f32, @floatFromInt(body_end)) * layout.h;
+ const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h;
+ const y1 = if (body_end == surface.rows and builder.win_h - body_y1 < layout.h) @max(body_y1, builder.win_h) else body_y1;
builder.addRect(x0, y0, x0 + layout.w, y1, page);
appendSquareRail(builder, x0, y0, y1, layout.w, false, overlayRgb(chrome.scroll_track));
if (thumb_start) |first| {
const thumb_y0 = @as(f32, @floatFromInt(first)) * layout.h;
- const thumb_y1 = @as(f32, @floatFromInt(thumb_end)) * layout.h;
+ const thumb_y1 = if (thumb_end == body_end) y1 else @as(f32, @floatFromInt(thumb_end)) * layout.h;
appendSquareRail(builder, x0, thumb_y0, thumb_y1, layout.w, true, overlayRgb(chrome.scroll_thumb));
}
}
@@ -6854,8 +6882,8 @@ fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLay
.rgb => |rgb| rgb,
else => continue,
};
- const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w;
- const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height, true))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
+ const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, builder.win_h) / 2));
+ const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
const half_w = @max(1, @min(3, layout.w - 2));
builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
@@ -6875,7 +6903,9 @@ test "column grips stay unmarked while pane marks retain physical centering" {
var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 200, .win_h = 80 };
appendTagGrips(&g, &surface, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
try std.testing.expectEqual(@as(usize, 12), builder.len);
- try std.testing.expectApproxEqAbs(@as(f32, 57.0 / 200.0 * 2.0 - 1.0), vertices[0].x, 0.0001);
+ // Centered in the anchor, whose left edge is inset by the band's offset
+ // (8 px on this last row): the mark moves right by half of it.
+ try std.testing.expectApproxEqAbs(@as(f32, 61.0 / 200.0 * 2.0 - 1.0), vertices[0].x, 0.0001);
}
fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w: f32, thumb: bool, color: OverlayColor) void {
@@ -6885,7 +6915,9 @@ fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w:
fn appendColumnSpines(core: *const pardes.Pardes, rows: u16, layout: CellLayout, builder: *OverlayBuilder) void {
const y0 = @as(f32, @floatFromInt(pardes.TOPBAR_H)) * layout.h;
- const y1 = @as(f32, @floatFromInt(rows)) * layout.h;
+ const grid_y1 = @as(f32, @floatFromInt(rows)) * layout.h;
+ // Down through the pixels past the last whole row, as tags run right.
+ const y1 = if (builder.win_h - grid_y1 < layout.h) @max(grid_y1, builder.win_h) else grid_y1;
if (y1 <= y0) return;
// A single boundary spans all panes, including their tags. Its left edge
// agrees with the square scroll markers and existing move-box hit area.
diff --git a/src/layout.zig b/src/layout.zig
index c6cb7503..8db05a07 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -723,6 +723,23 @@ pub fn columnMoveTarget(p: *const Pardes, source: usize, x: u16) usize {
return insertion;
}
+/// Share out the width of columns `left` and `left + 1` so the left one is
+/// `left_width` cells, neither below MINW. A border drag and a column grip
+/// dropped inside its own place both end here.
+pub fn setColumnPairWidth(p: *Pardes, left: usize, left_width: u64) void {
+ if (left + 1 >= p.ncol) return;
+ const combined: u64 = p.col_w[left] + p.col_w[left + 1];
+ // Too narrow for both to keep MINW: any split would squeeze one away.
+ if (combined < 2 * @as(u64, config.MINW)) return;
+ const width = std.math.clamp(left_width, config.MINW, @max(@as(u64, config.MINW), combined -| config.MINW));
+ const pair = p.col_weight[left] + p.col_weight[left + 1];
+ if (pair <= 1 or combined == 0) return;
+ const numerator = @as(u128, pair) * width + combined / 2;
+ const share = std.math.clamp(@as(u64, @intCast(numerator / combined)), 1, pair - 1);
+ p.col_weight[left] = share;
+ p.col_weight[left + 1] = pair - share;
+}
+
pub fn reorderColumn(p: *Pardes, from: usize, to: usize) void {
if (from >= p.ncol or to >= p.ncol or from == to) return;
const ids = p.col_panes[from];
diff --git a/src/macos.zig b/src/macos.zig
index c359eb3f..3ff8bdfa 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -951,7 +951,7 @@ export fn pardes_gui_tagline_font_percent() u8 {
export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
pardes.turn.wake();
defer pardes.turn.rest();
- return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, if (state) |*st| st.core.settings.workspace_tag else true);
+ return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h);
}
export fn pardes_topbar_pane_border_px(cell_h: u32, tagline_h: u32) u32 {
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index 5ad09c38..70f467e3 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -523,9 +523,8 @@ private struct TaglineMetrics {
/// Where this band sits inside row `row`, measured down from the row's top.
/// The rule belongs to the core (`pardes_tagline_band_offset`) so that this
- /// shell and the SDL one cannot disagree about it: row zero and the first
- /// pane-tag row meet at a shared rule, a Tagbottom band on the last row
- /// faces the window edge, and everything between is centred.
+ /// shell and the SDL one cannot disagree about it: every band is centred,
+ /// except a Tagbottom band on the last row, which faces the window edge.
func top(row: Int, canvasHeight: CGFloat) -> CGFloat {
CGFloat(pardes_tagline_band_offset(
UInt16(clamping: row), Float(canvasHeight * scale), cellPixels, bandPixels)) / scale
diff --git a/src/pardes.zig b/src/pardes.zig
index fae14f47..ca2c34a4 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -232,19 +232,12 @@ pub const BOX_H: u16 = 1;
pub const TAG_GAP: u16 = 1;
pub const TAG_TEXT_INSET: u16 = config.GUTTER + TAG_GAP;
-pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32, workspace: bool) u32 {
+pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
const spare = cell_h -| tagline_h;
- const border = topbarPaneBorderPixels(cell_h, tagline_h);
const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h))))));
- if (!workspace) {
- // No workspace row: nothing above row 0 to share a rule with, so
- // every band centers in its cell the way the middle rows always
- // have. The anchor boxes this lets the hosts draw stay symmetric.
- if (@as(u32, row) + 1 >= rows) return spare;
- return spare / 2;
- }
- if (row == 0) return spare - border / 2;
- if (row == TOPBAR_H) return border - border / 2;
+ // Every band centers in its cell, so text and anchors sit at the same
+ // height in the workspace, column and pane tags alike. Only the last row
+ // faces the window edge (Tagbottom).
if (@as(u32, row) + 1 >= rows) return spare;
return spare / 2;
}
@@ -5160,7 +5153,9 @@ const Drag = union(enum) {
border_v: struct { left_col: usize, cur_x: u16, corner: ?struct { col: usize, idx: usize } = null, cur_y: u16 = 0 },
border_h: struct { col: usize, top_idx: usize, cur_y: u16 },
move: struct { id: usize, cur_x: u16, cur_y: u16 },
- column_move: struct { column: usize, serial: u32, cur_x: u16 },
+ /// `grab`: how far into the grip it was pressed, so the column's left
+ /// edge lands at `cur_x - grab`. A release still on the grip moves nothing.
+ column_move: struct { column: usize, serial: u32, cur_x: u16, grab: u16 = 0 },
tag: struct { id: usize },
/// chorded: a 1-2/1-3 cut/paste chord fired during this left drag —
/// the drag's own release is then inert
@@ -12022,7 +12017,7 @@ pub const Pardes = struct {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
if (m.button == config.select_button) {
p.header_drag = false;
- p.drag = .{ .column_move = .{ .column = column, .serial = p.col_serial[column], .cur_x = mcol } };
+ p.drag = .{ .column_move = .{ .column = column, .serial = p.col_serial[column], .cur_x = mcol, .grab = mcol - p.col_x[column] } };
}
return;
}
@@ -12482,20 +12477,10 @@ pub const Pardes = struct {
.border_v => |d| {
const c = d.left_col;
const old_seam = p.col_x[c] + p.col_w[c] -| 1;
- if (c + 1 < p.ncol and d.cur_x != old_seam) {
- const combined: u64 = p.col_w[c] + p.col_w[c + 1];
- // the handle is the left pane's LAST column (inclusive), so
- // the new width includes it — a no-drag click changes nothing
- var nl: u64 = (d.cur_x + 1) -| p.col_x[c];
- nl = std.math.clamp(nl, config.MINW, @max(@as(u64, config.MINW), combined -| config.MINW));
- const pair = p.col_weight[c] + p.col_weight[c + 1];
- if (pair > 1 and combined > 0) {
- const numerator = @as(u128, pair) * nl + combined / 2;
- const left = std.math.clamp(@as(u64, @intCast(numerator / combined)), 1, pair - 1);
- p.col_weight[c] = left;
- p.col_weight[c + 1] = pair - left;
- }
- }
+ // the handle is the left pane's LAST column (inclusive), so
+ // the new width includes it — a no-drag click changes nothing
+ if (c + 1 < p.ncol and d.cur_x != old_seam)
+ layout.setColumnPairWidth(p, c, (d.cur_x + 1) -| p.col_x[c]);
if (d.corner) |k| {
if (d.cur_y != seamRowOf(p, k.col, k.idx))
layout.applyRowSplit(p, k.col, k.idx, d.cur_y);
@@ -12503,9 +12488,15 @@ pub const Pardes = struct {
},
.border_h => |d| if (d.cur_y != seamRowOf(p, d.col, d.top_idx))
layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y),
- .column_move => |d| {
- if (layout.columnBySerial(p, d.serial)) |column|
- layout.reorderColumn(p, column, layout.columnMoveTarget(p, column, d.cur_x));
+ .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| {
+ const target = layout.columnMoveTarget(p, column, d.cur_x);
+ if (target != column) {
+ layout.reorderColumn(p, column, target);
+ } else if (column > 0 and (d.cur_x < p.col_x[column] or d.cur_x >= p.col_x[column] + config.GUTTER)) {
+ // Dropped short of another column's place: as in acme,
+ // the column's left edge moves to where it was dropped.
+ layout.setColumnPairWidth(p, column - 1, (d.cur_x -| d.grab) -| p.col_x[column - 1]);
+ }
},
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
@@ -14181,6 +14172,15 @@ pub const Pardes = struct {
}
var row: u16 = p.topBarHeight();
while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
+ } else if (source > 0 and source < p.ncol and (d.cur_x < p.col_x[source] or d.cur_x >= p.col_x[source] + config.GUTTER) and
+ p.col_w[source - 1] + p.col_w[source] >= 2 * config.MINW)
+ {
+ // Where the left edge would land: the border drag's rail.
+ const lo = p.col_x[source - 1] + config.MINW;
+ const hi = (p.col_x[source] + p.col_w[source]) -| config.MINW;
+ const x = @min(std.math.clamp(d.cur_x -| d.grab, lo, @max(lo, hi)), s.cols -| 1);
+ var row: u16 = p.topBarHeight();
+ while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
}
},
.move => |d| if (d.cur_x < s.cols) {
diff --git a/src/web.zig b/src/web.zig
index 7325af54..b5a992ae 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -376,7 +376,7 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const WebCell {
}
export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
- return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, true);
+ return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h);
}
export fn pardes_body_layer_value(index: u32, field: u32) u32 {