diff options
| author | user <user@clank> | 2026-08-25 14:23:33 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-25 14:23:33 +0200 |
| commit | b744a90859bf248bfbea97f990668301b5fa9537 (patch) | |
| tree | f1b717507bc24161b7ab41def644289f9457470e /src | |
| parent | placeholder webbie (diff) | |
| download | slosh-b744a90859bf248bfbea97f990668301b5fa9537.tar.gz slosh-b744a90859bf248bfbea97f990668301b5fa9537.tar.xz slosh-b744a90859bf248bfbea97f990668301b5fa9537.zip | |
compact: shared borders instead of gaps (D23)
One knob, `compact true`: panes pack flush against 1-cell divider lines
that meet in real junctions, one frame rings the tab, and a pane's title
rides the line above it -- a boundary costs one cell where the default
look spends four. The lines are strokes: each write says which edges of
its cell it needs, unions with what is there, and never clobbers text,
so ├ ┬ ┼ fall out of paint order and the focus ring recolours without
inventing arms. The divider *is* the gap (eff_gap, one answer for the
layout, split_fits and the resize clamp), so gap hits, hover hints and
corner drags work unchanged. Interior edges give up click-to-split --
one cell cannot hold two verbs -- and a pane's status row overlays its
last content row. Floats, zoom and the D6 flatten keep classic frames
through place_hidden: zero new tree cases.
Underneath it, mode or no mode: a border drag now moves the boundary on
that side, converted on the first motion so the press that never moves
keeps its old meaning -- the handle's click still splits, the rim's is
still deliberately nothing (drag.rim). Which is also the fix for gap 0,
where flush panes leave no gap cell to grab and the resize gesture had
silently vanished with it.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 84 | ||||
| -rw-r--r-- | src/app_draw.c | 460 | ||||
| -rw-r--r-- | src/app_layout.c | 61 | ||||
| -rw-r--r-- | src/app_ui.c | 5 | ||||
| -rw-r--r-- | src/config.c | 5 |
5 files changed, 516 insertions, 99 deletions
@@ -457,7 +457,9 @@ void draw_min_bar(app_t *a, screen_t *s) { void draw_status_line(app_t *a, screen_t *s) { if (!CFG.status_line || s->rows < 3) return; - uint16_t y = (uint16_t)(s->rows - CFG.gap - 1); + /* Compact rides the very bottom row: the gap it floats on is air, and + * compact has none. */ + uint16_t y = (uint16_t)(s->rows - (CFG.compact ? 0 : CFG.gap) - 1); uint16_t x = CFG.status_pad; uint16_t right = (uint16_t)(s->cols > CFG.status_pad ? s->cols - CFG.status_pad : 0); @@ -933,6 +935,37 @@ static void drag_edge(app_t *a, node_t *sp, size_t i, int cells) { layout(a); } +/* The split boundary a pane's side sits on, walking up as far as it takes: + * the pane's parent may divide the other way, and the boundary to the right + * of a pane can belong to a split any number of levels up. False for a side + * on the outer edge, where there is nothing to drag against. + * + * This is what lets a *border* drag resize. With `gap 0` the panes are flush + * and there is no gap cell to grab — gap_rect answers false, so no resize + * handle is ever registered — and the border is all there is. The border was + * already a button (a click splits toward it); motion turning the same press + * into a boundary drag gives the two gestures one target, and a press that + * never moves still splits exactly as it did. */ +static bool boundary_for_side(node_t *leaf, char side, node_t **sp_out, + size_t *edge_out) { + split_dir_t dir = side_dir(side); + bool before = side == 'l' || side == 't'; + for (node_t *n = leaf; n && n->parent; n = n->parent) { + node_t *p = n->parent; + if (p->dir != dir) continue; /* divides the other way: keep climbing */ + size_t i = 0; + while (i < p->nkids && p->kids[i] != n) i++; + if (i >= p->nkids) return false; + if (before ? i > 0 : i + 1 < p->nkids) { + *sp_out = p; + *edge_out = before ? i - 1 : i; + return true; + } + /* At this split's extreme edge: the boundary, if any, is further up. */ + } + return false; +} + /* Focus follows the mouse, but never at the cost of what you were doing. * * Hovering may not: steal focus mid-chord (the prefix is held), reach past an @@ -1024,30 +1057,31 @@ static void do_action(app_t *a, const char *action, const input_event_t *ev) { a->drag.moved = false; const char *sep = on_name ? NULL : strchr(action + 6, ':'); a->drag.side = sep && sep[1] ? sep[1] : 0; + a->drag.rim = false; /* the top row's handle really does split */ app_focus_pane(a, id); } return; } - /* The rim of an edge: hover arms the guide, and a press does nothing at all. - * Deliberately inert and deliberately explicit -- this is the whole point of - * the handle, and a silent fall-through to whatever comes next would be a - * thin place for it to live. Starting no drag also means the release path - * finds nothing to split, which is what stops the accidents. + /* The rim of an edge: hover arms the guide, and a *click* does nothing at + * all. Deliberately inert and deliberately explicit -- this is the whole + * point of the handle, and a silent fall-through to whatever comes next + * would be a thin place for it to live. + * + * A press that then moves is a different gesture: the rim arms the same + * border drag the handle does, marked as the rim's, so motion can turn it + * into a boundary drag while its release stays the click that splits + * nothing. Grabbing anywhere on a border and pulling moves the boundary, + * which is the only way to move one when `gap 0` leaves no gap to grab. * * On a float the whole border is a resize grab instead: a float has no * guide to arm and no split to guard, and the rim being inert was about * accidents that rearrange a layout — resizing the thing you grabbed is * the accident-free reading of the same gesture. */ - if (strncmp(action, "brim:", 5) == 0) { - uint32_t id = (uint32_t)strtoul(action + 5, NULL, 10); - node_t *fn = pane_by_id(a, id); - if (fn && fn->floating && ev->maction == MOUSE_PRESS) - float_resize_press(a, fn, ev); - return; - } - if (strncmp(action, "border:", 7) == 0) { - uint32_t id = (uint32_t)strtoul(action + 7, NULL, 10); - const char *colon = strchr(action + 7, ':'); + if (strncmp(action, "brim:", 5) == 0 || strncmp(action, "border:", 7) == 0) { + bool rim = action[1] == 'r'; /* brim */ + const char *rest = action + (rim ? 5 : 7); + uint32_t id = (uint32_t)strtoul(rest, NULL, 10); + const char *colon = strchr(rest, ':'); char side = colon && colon[1] ? colon[1] : 'r'; if (ev->maction == MOUSE_PRESS) { node_t *fn = pane_by_id(a, id); @@ -1058,6 +1092,7 @@ static void do_action(app_t *a, const char *action, const input_event_t *ev) { a->drag.kind = DRAG_BORDER; a->drag.src = id; a->drag.side = side; + a->drag.rim = rim; a->drag.moved = false; a->drag.x = ev->mx; a->drag.y = ev->my; @@ -1586,6 +1621,21 @@ void app_event(app_t *a, const input_event_t *ev) { if (a->drag.kind != DRAG_NONE) { if (ev->maction == MOUSE_MOTION) { a->drag.moved = true; + /* A border press that moves is a boundary drag, when the side has a + * boundary. Converted on the first motion rather than at the press, + * so a press that never moves is still the click that splits. */ + if (a->drag.kind == DRAG_BORDER) { + node_t *bn = pane_by_id(a, a->drag.src); + node_t *bsp = NULL; + size_t bedge = 0; + if (bn && !bn->floating && + boundary_for_side(bn, a->drag.side, &bsp, &bedge)) { + a->drag.kind = DRAG_EDGE; + a->drag.c_nv = a->drag.c_nh = 0; + a->drag.src = bsp->id; + a->drag.edge = bedge; + } + } if (a->drag.kind == DRAG_SELECT) { node_t *n = pane_by_id(a, a->drag.src); if (n) @@ -1685,7 +1735,7 @@ void app_event(app_t *a, const input_event_t *ev) { } else if (ev->maction == MOUSE_RELEASE) { /* A press that never moved is a click, and a click on an edge * splits toward it. */ - if (!a->drag.moved && a->drag.side && + if (!a->drag.moved && a->drag.side && !a->drag.rim && (a->drag.kind == DRAG_BORDER || a->drag.kind == DRAG_TITLE)) { node_t *n = pane_by_id(a, a->drag.src); if (n) { diff --git a/src/app_draw.c b/src/app_draw.c index 04e8264..1a46a46 100644 --- a/src/app_draw.c +++ b/src/app_draw.c @@ -21,6 +21,159 @@ /* ---- drawing ------------------------------------------------------------ */ +/* ---- compact: shared borders ------------------------------------------- + * + * Compact packs panes flush against 1-cell divider lines and rings the tab + * with one outer frame, instead of giving every pane its own border across a + * gap. The lines are drawn as *strokes*: each write says which of the cell's + * four edges it needs (up/down/left/right), reads what is already there, and + * writes the union — so a divider running into the outer frame makes a ├ and + * two dividers crossing make a ┼ without anyone computing junctions. A cell + * holding anything that is not a line (a title, a button) is left alone, so + * a re-stroke — the focused pane's ring in the frame colour — can run over a + * finished row and recolour exactly the lines. */ + +enum { BX_U = 1, BX_D = 2, BX_L = 4, BX_R = 8 }; + +/* The glyph for a set of edge bits. Corners — exactly one arm each way — + * follow `rounded`, the same choice the classic frame makes; a tee or a + * cross has no rounded form. */ +static const char *box_glyph(uint8_t bits) { + switch (bits) { + case BX_L | BX_R: return "\u2500"; /* ─ */ + case BX_U | BX_D: return "\u2502"; /* │ */ + case BX_D | BX_R: return CFG.rounded ? "\u256d" : "\u250c"; /* ┌ */ + case BX_D | BX_L: return CFG.rounded ? "\u256e" : "\u2510"; /* ┐ */ + case BX_U | BX_R: return CFG.rounded ? "\u2570" : "\u2514"; /* └ */ + case BX_U | BX_L: return CFG.rounded ? "\u256f" : "\u2518"; /* ┘ */ + case BX_U | BX_D | BX_R: return "\u251c"; /* ├ */ + case BX_U | BX_D | BX_L: return "\u2524"; /* ┤ */ + case BX_D | BX_L | BX_R: return "\u252c"; /* ┬ */ + case BX_U | BX_L | BX_R: return "\u2534"; /* ┴ */ + case BX_U | BX_D | BX_L | BX_R: return "\u253c"; /* ┼ */ + case BX_U: return "\u2575"; /* ╵ */ + case BX_D: return "\u2577"; /* ╷ */ + case BX_L: return "\u2574"; /* ╴ */ + case BX_R: return "\u2576"; /* ╶ */ + default: return " "; + } +} + +/* And back: the edge bits of what a cell already holds, or 0 for anything + * that is not one of our lines. The dashed variants count — a drop-target's + * dashes are still the boundary they replaced — and both corner styles do, + * so `rounded` cannot confuse the union. */ +static uint8_t box_bits_of(const cell_t *c) { + if (!c || c->len != 3) return 0; + static const struct { + const char *g; + uint8_t bits; + } tab[] = { + {"\u2500", BX_L | BX_R}, + {"\u2502", BX_U | BX_D}, + {"\u2504", BX_L | BX_R}, + {"\u2506", BX_U | BX_D}, + {"\u250c", BX_D | BX_R}, + {"\u256d", BX_D | BX_R}, + {"\u2510", BX_D | BX_L}, + {"\u256e", BX_D | BX_L}, + {"\u2514", BX_U | BX_R}, + {"\u2570", BX_U | BX_R}, + {"\u2518", BX_U | BX_L}, + {"\u256f", BX_U | BX_L}, + {"\u251c", BX_U | BX_D | BX_R}, + {"\u2524", BX_U | BX_D | BX_L}, + {"\u252c", BX_D | BX_L | BX_R}, + {"\u2534", BX_U | BX_L | BX_R}, + {"\u253c", BX_U | BX_D | BX_L | BX_R}, + {"\u2575", BX_U}, + {"\u2577", BX_D}, + {"\u2574", BX_L}, + {"\u2576", BX_R}, + }; + for (size_t i = 0; i < sizeof tab / sizeof *tab; i++) + if (memcmp(c->text, tab[i].g, 3) == 0) return tab[i].bits; + return 0; +} + +/* One stroke into one cell: union with whatever line is there, and never + * clobber anything that is not a line — which is what lets a focus re-stroke + * run over a row that already carries a title. `create` false additionally + * refuses blank cells: a junction extension and a focus ring only ever join + * or recolour lines this pass drew, and must not leave stray stumps where + * there is no line to meet — the rows the minimised bar owns, say. */ +static void stroke(screen_t *s, uint16_t x, uint16_t y, uint8_t bits, + color_t fg, uint16_t attrs, bool create) { + cell_t *c = screen_at(s, x, y); + if (!c) return; + uint8_t have = box_bits_of(c); + if (!have && !(create && c->len == 1 && c->text[0] == ' ')) return; + screen_text(s, x, y, box_glyph((uint8_t)(have | bits)), fg, NO_COLOR, attrs); +} + +/* The ring one cell outside `r`: the four lines a compact pane sits inside. + * Drawn (`create`) for the tab area it is the outer frame; re-stroked for + * the focused pane it *recolours* its stretch of the shared lines — junction + * glyphs included — and adds no arms of its own: any bit the ring would add + * is an arm pointing at a line that is not there, which is exactly the state + * (a pane sitting on the minimised bar's rows) where nothing should be + * drawn. */ +static void stroke_ring(screen_t *s, rect_t r, color_t fg, uint16_t attrs, + bool create) { + if (!r.x || !r.y || !r.w || !r.h) return; + uint16_t x0 = (uint16_t)(r.x - 1), x1 = (uint16_t)(r.x + r.w); + uint16_t y0 = (uint16_t)(r.y - 1), y1 = (uint16_t)(r.y + r.h); + for (uint16_t x = x0; x <= x1; x++) { + uint8_t h = + create ? (uint8_t)((x > x0 ? BX_L : 0) | (x < x1 ? BX_R : 0)) : 0; + stroke(s, x, y0, h, fg, attrs, create); + stroke(s, x, y1, h, fg, attrs, create); + } + for (uint16_t y = y0; y <= y1; y++) { + uint8_t v = + create ? (uint8_t)((y > y0 ? BX_U : 0) | (y < y1 ? BX_D : 0)) : 0; + stroke(s, x0, y, v, fg, attrs, create); + stroke(s, x1, y, v, fg, attrs, create); + } +} + +/* Is this pane drawn compact? The mode is global but the treatment is not: + * a float is an overlay and keeps the classic frame that lifts it off the + * page, and a flattened or zoomed tab is showing one pane on its own — + * nothing is packed against anything, so the pane keeps its own edges. */ +static bool compact_pane(app_t *a, const node_t *leaf) { + return CFG.compact && !leaf->floating && !leaf->collapsed && !a->flattened && + a->ntabs && !cur(a)->zoom; +} + +/* Does this side of the rect sit on the tab's outer frame? Interior sides + * are shared dividers — resize handles their whole length — and only the + * outer frame keeps the click-to-split border verbs. Geometric on purpose: + * when the minimised bar eats the bottom rows, the panes above it touch no + * line, and offering a split target on the bar's cells would be a lie. */ +static bool edge_outer(app_t *a, rect_t r, char side) { + rect_t ar = app_tab_area(a); + switch (side) { + case 'l': return r.x == ar.x; + case 'r': return (uint16_t)(r.x + r.w) == (uint16_t)(ar.x + ar.w); + case 't': return r.y == ar.y; + default: return (uint16_t)(r.y + r.h) == (uint16_t)(ar.y + ar.h); + } +} + +/* A plain rule goes dashed, and anything else — a junction, a title, a blank + * — stays: how a compact pane's stretch of the shared lines says "you could + * drop here" without redrawing glyphs its neighbours also own. */ +static void dash_rule(screen_t *s, uint16_t x, uint16_t y, color_t fg, + uint16_t attrs) { + cell_t *c = screen_at(s, x, y); + if (!c || c->len != 3) return; + if (memcmp(c->text, "\u2500", 3) == 0) + screen_text(s, x, y, "\u2504", fg, NO_COLOR, attrs); + else if (memcmp(c->text, "\u2502", 3) == 0) + screen_text(s, x, y, "\u2506", fg, NO_COLOR, attrs); +} + /* A pane's own status line and buttons, drawn in its bottom frame row. * * Two things end up here, because they are the same thing: what a *live* pane @@ -139,23 +292,32 @@ static uint16_t split_handle_len(uint16_t span) { /* One side's handle. False when the side is too short to hold one, which is a * pane `split_fits` is about to refuse anyway. The top row is not here: its * placement depends on what the title and the buttons left, so draw_frame does - * it once those are known. */ -static bool split_handle(const node_t *leaf, char side, rect_t *out) { + * it once those are known. + * + * A compact pane's edges are the shared lines one cell outside its rect, so + * `cf` moves the handle out there; the span is the pane's own stretch of the + * line, junction cells excluded by construction since they sit beyond it. */ +static bool split_handle(const node_t *leaf, char side, bool cf, rect_t *out) { rect_t r = leaf->rect; - if (r.w < 4 || r.h < 4) return false; + if (cf ? (r.w < 4 || r.h < 3 || !r.x || !r.y) : (r.w < 4 || r.h < 4)) + return false; bool vert = side == 'l' || side == 'r'; /* The span leaves the corners out: a corner is where two gaps cross and is a * resize target already. */ - uint16_t span = vert ? (uint16_t)(r.h - 2) : (uint16_t)(r.w - 2); + uint16_t span = + vert ? (uint16_t)(r.h - (cf ? 0 : 2)) : (uint16_t)(r.w - (cf ? 0 : 2)); if (span < 3) return false; uint16_t len = split_handle_len(span); uint16_t off = (uint16_t)((span - len) / 2); - if (vert) - *out = (rect_t){side == 'l' ? r.x : (uint16_t)(r.x + r.w - 1), - (uint16_t)(r.y + 1 + off), 1, len}; - else - *out = - (rect_t){(uint16_t)(r.x + 1 + off), (uint16_t)(r.y + r.h - 1), len, 1}; + if (vert) { + uint16_t bx = + side == 'l' ? (cf ? (uint16_t)(r.x - 1) : r.x) + : (cf ? (uint16_t)(r.x + r.w) : (uint16_t)(r.x + r.w - 1)); + *out = (rect_t){bx, (uint16_t)(r.y + (cf ? 0 : 1) + off), 1, len}; + } else { + uint16_t by = cf ? (uint16_t)(r.y + r.h) : (uint16_t)(r.y + r.h - 1); + *out = (rect_t){(uint16_t)(r.x + (cf ? 0 : 1) + off), by, len, 1}; + } return true; } @@ -242,7 +404,8 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { if (!split_fits(leaf, side_dir(side))) return; rect_t r = leaf->rect; - if (r.w < 4 || r.h < 4) return; + bool cf = compact_pane(a, leaf); + if (cf ? (r.w < 4 || r.h < 3) : (r.w < 4 || r.h < 4)) return; uint16_t x1 = (uint16_t)(r.x + r.w - 1), y1 = (uint16_t)(r.y + r.h - 1); color_t hi = GUIDE; @@ -262,15 +425,21 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { split_rim_action(leaf, side, rim, sizeof rim); split_handle_action(leaf, side, act, sizeof act); if (side == 'l' || side == 'r') { - uint16_t bx = side == 'l' ? r.x : x1; - for (uint16_t y = (uint16_t)(r.y + 1); y < y1; y++) { + uint16_t bx = side == 'l' ? (cf ? (uint16_t)(r.x - 1) : r.x) + : (cf ? (uint16_t)(r.x + r.w) : x1); + uint16_t ylo = (uint16_t)(r.y + (cf ? 0 : 1)); + uint16_t yhi = cf ? (uint16_t)(r.y + r.h) : y1; + for (uint16_t y = ylo; y < yhi; y++) { const char *own = hit_test(&s->hits, bx, y); if (own && (strcmp(own, rim) == 0 || strcmp(own, act) == 0)) screen_text(s, bx, y, "\u2503", hi, NO_COLOR, ATTR_BOLD); } } else { - uint16_t by = side == 't' ? r.y : y1; - for (uint16_t x = (uint16_t)(r.x + 1); x < x1; x++) { + uint16_t by = side == 't' ? (cf ? (uint16_t)(r.y - 1) : r.y) + : (cf ? (uint16_t)(r.y + r.h) : y1); + uint16_t xlo = (uint16_t)(r.x + (cf ? 0 : 1)); + uint16_t xhi = cf ? (uint16_t)(r.x + r.w) : x1; + for (uint16_t x = xlo; x < xhi; x++) { const char *own = hit_test(&s->hits, x, by); if (own && (strcmp(own, rim) == 0 || strcmp(own, act) == 0)) screen_text(s, x, by, "\u2501", hi, NO_COLOR, ATTR_BOLD); @@ -537,7 +706,14 @@ static void draw_float_guide(app_t *a, screen_t *s, node_t *leaf) { static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { rect_t r = leaf->rect; - if (r.w < 3 || r.h < 3) return; + /* Compact wears no frame of its own: the lines were drawn by + * draw_compact_lines and this function only dresses them — the title, the + * buttons and the hits go on the shared line *above* the rect, which is + * why a compact pane needs a row and a column to its left to exist. */ + bool cf = compact_pane(a, leaf); + if (cf ? (r.w < 3 || !r.h || !r.x || !r.y) : (r.w < 3 || r.h < 3)) return; + uint16_t ty = cf ? (uint16_t)(r.y - 1) : r.y; /* the row the title rides */ + bool top_outer = !cf || edge_outer(a, r, 't'); bool focused = leaf == cur(a)->focus; bool drop_target = a->drag.kind == DRAG_TITLE && a->drag.target == leaf->id && a->drag.src != leaf->id; @@ -560,27 +736,27 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { const char *bl = CFG.rounded ? "╰" : "└", *br = CFG.rounded ? "╯" : "┘"; uint16_t x1 = (uint16_t)(r.x + r.w - 1), y1 = (uint16_t)(r.y + r.h - 1); - screen_text(s, r.x, r.y, tl, fg, NO_COLOR, attrs); - screen_text(s, x1, r.y, tr, fg, NO_COLOR, attrs); - screen_text(s, r.x, y1, bl, fg, NO_COLOR, attrs); - screen_text(s, x1, y1, br, fg, NO_COLOR, attrs); - for (uint16_t x = (uint16_t)(r.x + 1); x < x1; x++) { - screen_text(s, x, r.y, hbar, fg, NO_COLOR, attrs); - screen_text(s, x, y1, hbar, fg, NO_COLOR, attrs); - } - for (uint16_t y = (uint16_t)(r.y + 1); y < y1; y++) { - screen_text(s, r.x, y, vbar, fg, NO_COLOR, attrs); - screen_text(s, x1, y, vbar, fg, NO_COLOR, attrs); - } + if (!cf) { + screen_text(s, r.x, r.y, tl, fg, NO_COLOR, attrs); + screen_text(s, x1, r.y, tr, fg, NO_COLOR, attrs); + screen_text(s, r.x, y1, bl, fg, NO_COLOR, attrs); + screen_text(s, x1, y1, br, fg, NO_COLOR, attrs); + for (uint16_t x = (uint16_t)(r.x + 1); x < x1; x++) { + screen_text(s, x, r.y, hbar, fg, NO_COLOR, attrs); + screen_text(s, x, y1, hbar, fg, NO_COLOR, attrs); + } + for (uint16_t y = (uint16_t)(r.y + 1); y < y1; y++) { + screen_text(s, r.x, y, vbar, fg, NO_COLOR, attrs); + screen_text(s, x1, y, vbar, fg, NO_COLOR, attrs); + } - /* The padding ring between the border and the contents, painted blank - * rather than left alone. A tiled pane sits on a cleared screen, so leaving - * it unpainted used to look the same by accident — but a float sits on - * whatever was composited under it, and every unpainted cell lets that show - * through. Blank-with-no-colour is exactly what screen_clear leaves, so a - * tiled pane looks as it always did, and with no padding configured the - * ring is empty and this writes nothing. */ - { + /* The padding ring between the border and the contents, painted blank + * rather than left alone. A tiled pane sits on a cleared screen, so + * leaving it unpainted used to look the same by accident — but a float + * sits on whatever was composited under it, and every unpainted cell lets + * that show through. Blank-with-no-colour is exactly what screen_clear + * leaves, so a tiled pane looks as it always did, and with no padding + * configured the ring is empty and this writes nothing. */ rect_t c = leaf->content; for (uint16_t y = (uint16_t)(r.y + 1); y < y1; y++) for (uint16_t x = (uint16_t)(r.x + 1); x < x1; x++) { @@ -590,12 +766,27 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { } screen_text(s, x, y, " ", NO_COLOR, NO_COLOR, 0); } + } else if (drag_target || drop_target) { + /* The dashed drop-candidate border, said on shared lines: each cell of + * this pane's ring that is still a plain rule goes dashed (in DROP_C for + * the pane under the pointer). Junctions are left alone — they belong to + * the neighbours too, and a dashed tee is not a glyph. */ + uint16_t rx0 = (uint16_t)(r.x - 1), rx1 = (uint16_t)(r.x + r.w); + uint16_t ry0 = ty, ry1 = (uint16_t)(r.y + r.h); + for (uint16_t x = rx0; x <= rx1; x++) { + dash_rule(s, x, ry0, fg, attrs); + dash_rule(s, x, ry1, fg, attrs); + } + for (uint16_t y = ry0; y <= ry1; y++) { + dash_rule(s, rx0, y, fg, attrs); + dash_rule(s, rx1, y, fg, attrs); + } } /* A pane that rang, marked just inside its corner: the same place on every * pane, whatever its title is doing. */ if (CFG.bell_indicator && pane_bell(leaf->pane) && r.w > 4) - screen_text(s, (uint16_t)(r.x + 1), r.y, CFG.bell_mark, BELL_C, NO_COLOR, + screen_text(s, (uint16_t)(r.x + 1), ty, CFG.bell_mark, BELL_C, NO_COLOR, ATTR_BOLD); /* The frame's top row is the drag handle, all of it. Its *split* is a handle @@ -603,11 +794,16 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { * than here: the title, the buttons and the scroll indicator all anchor * wherever the config puts them, a centred title lands exactly where a * centred handle would want to be, and this row's rule is that the title - * wins. So the handle is put where they are not, once they are all placed. */ - { + * wins. So the handle is put where they are not, once they are all placed. + * + * Not when the row is a shared divider (compact, interior top edge): that + * line is the boundary between this pane and the one above, and its free + * cells stay the resize handle they are. The pane is still dragged — by + * its name, which registers its own hit below. */ + if (top_outer) { char action[48]; snprintf(action, sizeof action, "title:%u", leaf->id); - hit_add(&s->hits, r.x, r.y, r.w, 1, action); + hit_add(&s->hits, r.x, ty, r.w, 1, action); } /* No split button. The border *is* the button -- or the middle of it is: @@ -657,12 +853,12 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { uint16_t bw = (uint16_t)(mw + 1); if (bx < r.x + 4 + bw) break; uint16_t px = (uint16_t)(bx - bw + 1); - bool hot = ptr_on(a, px, r.y, bw, 1); - screen_text(s, px, r.y, cell, hot ? PANE_BTN_HOVER : PANE_BTN, NO_COLOR, + bool hot = ptr_on(a, px, ty, bw, 1); + screen_text(s, px, ty, cell, hot ? PANE_BTN_HOVER : PANE_BTN, NO_COLOR, hot ? ATTR_BOLD : 0); char action[48]; snprintf(action, sizeof action, "%s:%u", btns[i].verb, leaf->id); - hit_add(&s->hits, px, r.y, bw, 1, action); + hit_add(&s->hits, px, ty, bw, 1, action); bx = (uint16_t)(px - 1); has_btn = true; btn_x = px; @@ -671,7 +867,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { /* One blank between the rule and the first button, so the group is not * welded to the frame the way a title without its space would be. */ if (has_btn && btn_x > (uint16_t)(r.x + 1)) { - screen_text(s, (uint16_t)(btn_x - 1), r.y, " ", PANE_BTN, NO_COLOR, 0); + screen_text(s, (uint16_t)(btn_x - 1), ty, " ", PANE_BTN, NO_COLOR, 0); btn_x = (uint16_t)(btn_x - 1); avail = (uint16_t)(avail > 1 ? avail - 1 : 0); } @@ -685,7 +881,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { * The rim is a target rather than nothing so that hovering anywhere on an * edge can still show you where the button is. Take it away and the handle * becomes a thing you have to already know about. */ - { + if (!cf) { char action[48]; snprintf(action, sizeof action, "brim:%u:l", leaf->id); hit_add(&s->hits, r.x, (uint16_t)(r.y + 1), 1, (uint16_t)(r.h - 2), action); @@ -695,13 +891,42 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { hit_add(&s->hits, r.x, y1, r.w, 1, action); for (const char *side = "lrb"; *side; side++) { rect_t h; - if (!split_handle(leaf, *side, &h)) continue; + if (!split_handle(leaf, *side, false, &h)) continue; + split_handle_action(leaf, *side, action, sizeof action); + hit_add(&s->hits, h.x, h.y, h.w, h.h, action); + } + } else { + /* Only the outer frame keeps the border verbs. An interior side is a + * shared divider: its whole length is already the resize handle the gap + * used to be, and a one-cell line cannot honestly hold two verbs — the + * keyboard still splits anything. */ + char action[48]; + uint16_t xl = (uint16_t)(r.x - 1), xr = (uint16_t)(r.x + r.w); + uint16_t yb = (uint16_t)(r.y + r.h); + if (edge_outer(a, r, 'l')) { + snprintf(action, sizeof action, "brim:%u:l", leaf->id); + hit_add(&s->hits, xl, r.y, 1, r.h, action); + } + if (edge_outer(a, r, 'r')) { + snprintf(action, sizeof action, "brim:%u:r", leaf->id); + hit_add(&s->hits, xr, r.y, 1, r.h, action); + } + if (edge_outer(a, r, 'b')) { + snprintf(action, sizeof action, "brim:%u:b", leaf->id); + hit_add(&s->hits, r.x, yb, r.w, 1, action); + } + for (const char *side = "lrb"; *side; side++) { + rect_t h; + if (!edge_outer(a, r, *side)) continue; + if (!split_handle(leaf, *side, true, &h)) continue; split_handle_action(leaf, *side, action, sizeof action); hit_add(&s->hits, h.x, h.y, h.w, h.h, action); } } - draw_pane_status(a, s, leaf, fg, focused); + /* A compact pane's status rides its bottom content row and must go over + * the composed contents, so draw_cb calls it after the pane does. */ + if (!cf) draw_pane_status(a, s, leaf, fg, focused); /* Scroll position, when there is one: compact, right-aligned, and clickable * to get back to the bottom. Budgeted after the button and before the title, @@ -723,10 +948,10 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { if (iw + 2 < avail) { uint16_t ix = (uint16_t)((has_btn ? btn_x : x1) - iw); uint16_t drawn = - screen_text(s, ix, r.y, ind, SCROLL_FG, SCROLL_BG, ATTR_BOLD); + screen_text(s, ix, ty, ind, SCROLL_FG, SCROLL_BG, ATTR_BOLD); char action[48]; snprintf(action, sizeof action, "scrollbottom:%u", leaf->id); - hit_add(&s->hits, ix, r.y, drawn, 1, action); + hit_add(&s->hits, ix, ty, drawn, 1, action); right_lo = ix; avail = (uint16_t)(avail - iw); } @@ -775,7 +1000,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { * is being typed, so a half-finished rename can never be mistaken for what * the pane is actually called. */ uint16_t drawn = screen_text( - s, tx, r.y, buf, + s, tx, ty, buf, editing ? RENAME_FG : (focused ? TITLE_FOCUS : TITLE_IDLE), editing ? RENAME_BG : NO_COLOR, editing || focused ? ATTR_BOLD : 0); @@ -792,7 +1017,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { if (drawn) { char action[48]; snprintf(action, sizeof action, "panetitle:%u", leaf->id); - hit_add(&s->hits, tx, r.y, drawn, 1, action); + hit_add(&s->hits, tx, ty, drawn, 1, action); has_title = true; title_lo = tx; title_hi = (uint16_t)(tx + drawn - 1); @@ -809,7 +1034,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { * the widest run the row still owns and, between equals, the one nearest the * middle. A row with nothing left offers no upward split, and its other three * edges are unaffected. */ - { + if (top_outer) { uint16_t lo = (uint16_t)(r.x + 1); if (CFG.bell_indicator && pane_bell(leaf->pane) && r.w > 4) lo++; struct run { @@ -863,7 +1088,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { if (found) { char action[48]; split_handle_action(leaf, 't', action, sizeof action); - hit_add(&s->hits, best_x, r.y, best_w, 1, action); + hit_add(&s->hits, best_x, ty, best_w, 1, action); } } } @@ -1422,7 +1647,28 @@ static void draw_cb(node_t *n, void *ud) { * is not decoration and must not be dimmed along with the thing it is * offered on. */ shade_leaf(d->a, d->s, n); - shade_chrome(d->a, d->s, n, n->rect, &n->content); + /* A compact pane's chrome is the ring of shared line it sits inside — one + * cell out on every side — rather than the rect's own edge; the state + * passes (a bell's flash, a theme's chrome chain) land there instead. The + * shared cells belong to the neighbours too, so a chain that recolours an + * *unfocused* frame will also touch the focused pane's stretch of a shared + * line: two owners is what sharing means. */ + rect_t chrome_r = n->rect; + if (compact_pane(d->a, n) && chrome_r.x && chrome_r.y) { + chrome_r.x--; + chrome_r.y--; + chrome_r.w = (uint16_t)(chrome_r.w + 2); + chrome_r.h = (uint16_t)(chrome_r.h + 2); + } + shade_chrome(d->a, d->s, n, chrome_r, &n->content); + /* Over the contents and under the guides: the status row of a compact pane + * overlays its last content row — there is no bottom border to carry it — + * so it has to go on after the pane composed and shaded. */ + if (compact_pane(d->a, n)) { + bool focused = n == cur(d->a)->focus; + draw_pane_status(d->a, d->s, n, focused ? FRAME_FOCUS : FRAME_IDLE, + focused); + } draw_split_guide(d->a, d->s, n); draw_float_guide(d->a, d->s, n); draw_drop_zones(d->a, d->s, n); @@ -1446,7 +1692,7 @@ void draw_tab_strip(app_t *a, screen_t *s) { * that is true. Worked out once rather than per tab. */ bool dragging_pane = a->drag.kind == DRAG_TITLE && a->drag.moved; uint16_t x = CFG.status_pad; - uint16_t y = CFG.gap; + uint16_t y = CFG.compact ? 0 : CFG.gap; /* compact spends no row on air */ /* Right side first, so a long tab list can never eat the indicators — the * same budgeting rule as the split button and the OSC buttons. */ @@ -1718,6 +1964,15 @@ static bool corner_uses(const corner_t *c, uint32_t id, size_t edge) { * Dotted while available, doubled and bold while engaged: the same grammar the * split guide already uses, where dashes are a possibility and weight is a * commitment. */ +/* A hint cell is painted only if the boundary still owns it. In classic + * layouts a gap holds nothing else, so this refuses nothing; on a compact + * divider the pane below has its title and buttons on the same line, and + * ruling dots through a name would be the top-row mistake all over again. */ +static bool edge_owns(screen_t *s, uint16_t x, uint16_t y) { + const char *o = hit_test(&s->hits, x, y); + return o && strncmp(o, "edge:", 5) == 0; +} + static void draw_resize_hint(app_t *a, screen_t *s, node_t *split, size_t idx, rect_t gapr) { bool active = a->drag.kind == DRAG_EDGE && a->drag.src == split->id && @@ -1744,11 +1999,13 @@ static void draw_resize_hint(app_t *a, screen_t *s, node_t *split, size_t idx, if (split->dir == SPLIT_COLS) { uint16_t x = (uint16_t)(gapr.x + gapr.w / 2); for (uint16_t y = gapr.y; y < gapr.y + gapr.h; y++) - screen_text(s, x, y, "\u250a", RESIZE_C, NO_COLOR, attrs0); + if (edge_owns(s, x, y)) + screen_text(s, x, y, "\u250a", RESIZE_C, NO_COLOR, attrs0); } else { uint16_t y = (uint16_t)(gapr.y + gapr.h / 2); for (uint16_t x = gapr.x; x < gapr.x + gapr.w; x++) - screen_text(s, x, y, "\u2508", RESIZE_C, NO_COLOR, attrs0); + if (edge_owns(s, x, y)) + screen_text(s, x, y, "\u2508", RESIZE_C, NO_COLOR, attrs0); } return; } @@ -1782,17 +2039,21 @@ static void draw_resize_hint(app_t *a, screen_t *s, node_t *split, size_t idx, if (split->dir == SPLIT_COLS) { uint16_t x = (uint16_t)(gapr.x + gapr.w / 2); for (uint16_t y = gapr.y; y < gapr.y + gapr.h; y++) - screen_text(s, x, y, active ? "\u2551" : "\u250a", RESIZE_C, NO_COLOR, - attrs); - screen_text(s, x, (uint16_t)(gapr.y + gapr.h / 2), "\u21d4", RESIZE_C, - NO_COLOR, ATTR_BOLD); + if (edge_owns(s, x, y)) + screen_text(s, x, y, active ? "\u2551" : "\u250a", RESIZE_C, NO_COLOR, + attrs); + uint16_t ay = (uint16_t)(gapr.y + gapr.h / 2); + if (edge_owns(s, x, ay)) + screen_text(s, x, ay, "\u21d4", RESIZE_C, NO_COLOR, ATTR_BOLD); } else { uint16_t y = (uint16_t)(gapr.y + gapr.h / 2); for (uint16_t x = gapr.x; x < gapr.x + gapr.w; x++) - screen_text(s, x, y, active ? "\u2550" : "\u2508", RESIZE_C, NO_COLOR, - attrs); - screen_text(s, (uint16_t)(gapr.x + gapr.w / 2), y, "\u21d5", RESIZE_C, - NO_COLOR, ATTR_BOLD); + if (edge_owns(s, x, y)) + screen_text(s, x, y, active ? "\u2550" : "\u2508", RESIZE_C, NO_COLOR, + attrs); + uint16_t ax = (uint16_t)(gapr.x + gapr.w / 2); + if (edge_owns(s, ax, y)) + screen_text(s, ax, y, "\u21d5", RESIZE_C, NO_COLOR, ATTR_BOLD); } } @@ -1811,21 +2072,82 @@ void draw_node(app_t *a, screen_t *s, node_t *n) { return; } - /* The gap between two children is the boundary you can drag. It is drawn as - * nothing, but it is a real target, derived from the rects the children were - * just given rather than recomputed from the config. */ + /* The gap between two children is the boundary you can drag. In classic + * layouts it is drawn as nothing; compact draws the divider line through it + * (draw_compact_lines). Either way it is a real target, derived from the + * rects the children were just given rather than recomputed from the + * config, and registered *before* the children so that a compact pane's + * title and buttons — which live on this very line — win their own cells. + * The hover hint is painted by draw_resize_hints once every pane has + * claimed what is its. */ for (size_t i = 0; i + 1 < n->nkids; i++) { rect_t gapr; if (!gap_rect(n, i, &gapr)) continue; char action[48]; snprintf(action, sizeof action, "edge:%u:%zu", n->id, i); hit_add(&s->hits, gapr.x, gapr.y, gapr.w, gapr.h, action); - draw_resize_hint(a, s, n, i, gapr); } for (size_t i = 0; i < n->nkids; i++) draw_node(a, s, n->kids[i]); } +/* The hover hints for every boundary, painted after the panes so the + * ownership question each cell asks (`edge_owns`) is answered by the + * finished hit list. */ +void draw_resize_hints(app_t *a, screen_t *s) { + if (!a->ntabs || !cur(a)->root) return; + struct gapinfo g[64]; + size_t n = collect_gaps(cur(a)->root, g, 64, 0); + for (size_t i = 0; i < n; i++) + draw_resize_hint(a, s, g[i].sp, g[i].i, g[i].r); +} + +/* The compact mode's lines, drawn before the panes so their titles land on + * top: one frame ring round the tab area, a divider through every gap — each + * extended one stroke into whatever perpendicular line it meets, which is + * what makes the junctions — and the focused pane's ring re-stroked in the + * frame colour, junction glyphs included, so focus reads off the lines the + * way it always has. */ +void draw_compact_lines(app_t *a, screen_t *s) { + if (!CFG.compact || !a->ntabs || !cur(a)->root) return; + if (a->flattened || cur(a)->zoom) return; + rect_t ar = app_tab_area(a); + if (!ar.x || !ar.y) return; /* no room for a ring: no lines to share */ + stroke_ring(s, ar, FRAME_IDLE, 0, true); + + /* The dividers' own cells first, their end-extensions after: an end only + * ever joins a line that is already there — the ring, or a perpendicular + * divider — which is what makes the junction glyphs, and what keeps a + * divider that stops short (at the minimised bar's rows) from leaving a + * stump in cells that hold no line. */ + struct gapinfo g[64]; + size_t n = collect_gaps(cur(a)->root, g, 64, 0); + for (size_t i = 0; i < n; i++) { + rect_t r = g[i].r; + if (g[i].sp->dir == SPLIT_COLS) + for (uint16_t y = r.y; y < r.y + r.h; y++) + stroke(s, r.x, y, BX_U | BX_D, FRAME_IDLE, 0, true); + else + for (uint16_t x = r.x; x < r.x + r.w; x++) + stroke(s, x, r.y, BX_L | BX_R, FRAME_IDLE, 0, true); + } + for (size_t i = 0; i < n; i++) { + rect_t r = g[i].r; + if (g[i].sp->dir == SPLIT_COLS) { + if (r.y) stroke(s, r.x, (uint16_t)(r.y - 1), BX_D, FRAME_IDLE, 0, false); + stroke(s, r.x, (uint16_t)(r.y + r.h), BX_U, FRAME_IDLE, 0, false); + } else { + if (r.x) stroke(s, (uint16_t)(r.x - 1), r.y, BX_R, FRAME_IDLE, 0, false); + stroke(s, (uint16_t)(r.x + r.w), r.y, BX_L, FRAME_IDLE, 0, false); + } + } + + node_t *f = cur(a)->focus; + if (f && f->kind == NODE_LEAF && !f->floating && !f->minimized && + !f->hidden && !f->collapsed && f->rect.x && f->rect.y) + stroke_ring(s, f->rect, FRAME_FOCUS, 0, false); +} + /* The cell of shade a float casts on whatever it covers: `gap_aspect` * columns beside and one row below, offset so the light reads as coming from * the top left. The same dim pass the scrim is — a shadow is a colour pass diff --git a/src/app_layout.c b/src/app_layout.c index 6618a2a..8ba4a3f 100644 --- a/src/app_layout.c +++ b/src/app_layout.c @@ -21,6 +21,16 @@ /* ---- layout: a pure function of the tree and the rect ------------------- */ +/* The space between two siblings, which every piece of gap arithmetic — the + * layout, the fit check, the resize clamp — must agree on. Compact packs + * panes against 1-cell divider lines, so the "gap" is exactly the divider, + * whichever axis: `gap` and `gap_aspect` are about air, and compact has + * none. */ +uint16_t eff_gap(split_dir_t dir) { + if (CFG.compact) return 1; + return dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; +} + /* Every leaf under a node, in tree order. */ size_t collect_leaves(node_t *n, node_t **out, size_t cap, size_t k) { if (!n || k >= cap) return k; @@ -201,12 +211,24 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) { * rect too small for a frame gets neither, and the pane takes the whole * thing. Horizontal padding is aspect-corrected, like the gap: the config's * numbers are rows, so a square-looking ring is one number rather than an - * arithmetic problem. */ - bool framed = r.w >= 3 && r.h >= 3; - uint16_t left = framed ? 1 + CFG.pad_left * CFG.gap_aspect : 0; - uint16_t right = framed ? 1 + CFG.pad_right * CFG.gap_aspect : 0; - uint16_t top = framed ? 1 + CFG.pad_top : 0; - uint16_t bottom = framed ? 1 + CFG.pad_bottom : 0; + * arithmetic problem. + * + * A compact tiled pane spends no border cells at all: the lines live + * outside its rect, between the rects, so content is the rect minus + * padding only. Floats keep the classic frame — they are overlays, and an + * overlay needs its own edge — and so does everything laid out through + * the flatten/zoom paths (`place_hidden`), where the pane on show is not + * packed against anything. */ + bool classic = !CFG.compact || n->floating || ctx->place_hidden; + bool framed = classic && r.w >= 3 && r.h >= 3; + uint16_t bord = framed ? 1 : 0; + bool pads = framed || !classic; + uint16_t left = + (uint16_t)(bord + (pads ? CFG.pad_left * CFG.gap_aspect : 0)); + uint16_t right = + (uint16_t)(bord + (pads ? CFG.pad_right * CFG.gap_aspect : 0)); + uint16_t top = (uint16_t)(bord + (pads ? CFG.pad_top : 0)); + uint16_t bottom = (uint16_t)(bord + (pads ? CFG.pad_bottom : 0)); n->content = (rect_t){ .x = (uint16_t)(r.x + left), .y = (uint16_t)(r.y + top), @@ -238,8 +260,7 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) { return; } - uint16_t gap = - n->dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; + uint16_t gap = eff_gap(n->dir); uint16_t total = n->dir == SPLIT_COLS ? r.w : r.h; /* Does every child clear the floor? If not, this node collapses — a local @@ -363,7 +384,18 @@ static void ensure_tiled(app_t *a) { * and the outer gap. Factored so the float edits can ask the same question * the layout answers, rather than a second copy of this arithmetic. */ static rect_t tab_area(app_t *a) { - uint16_t gx = (uint16_t)(CFG.gap * CFG.gap_aspect), gy = CFG.gap; + /* Compact reserves exactly the outer frame: one cell a side, drawn as one + * ring around the whole tab rather than a border per pane. The ring is + * also where the top row of panes put their titles, so it is not optional + * air the way the gap is. */ + uint16_t gx, gy; + if (CFG.compact) { + gx = a->cols >= 3 ? 1 : 0; + gy = a->rows >= 3 ? 1 : 0; + } else { + gx = (uint16_t)(CFG.gap * CFG.gap_aspect); + gy = CFG.gap; + } uint16_t top = (uint16_t)(gy + STRIP_ROWS); return (rect_t){.x = gx, .y = top, @@ -374,6 +406,11 @@ static rect_t tab_area(app_t *a) { : 1)}; } +/* The same rect for the draw passes: the compact frame ring and the outer + * split affordances are drawn against it, and recomputing it there would be a + * second opinion about where the layout put everything. */ +rect_t app_tab_area(app_t *a) { return tab_area(a); } + void layout(app_t *a) { if (!a->ntabs) return; /* Re-derived below, like everything else about a layout pass: whether this @@ -618,8 +655,7 @@ bool split_fits(node_t *leaf, split_dir_t dir) { uint16_t floor_ = dir == SPLIT_COLS ? CFG.min_split_cols : CFG.min_split_rows; uint16_t hard = dir == SPLIT_COLS ? MIN_PANE_COLS : MIN_PANE_ROWS; if (floor_ < hard) floor_ = hard; - uint16_t gap = - dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; + uint16_t gap = eff_gap(dir); size_t k; uint16_t total; @@ -1531,8 +1567,7 @@ void transfer_weight(node_t *from, node_t *to, int amount) { node_t *sp = from->parent; if (sp && sp->nkids >= 2) { uint16_t floor_ = sp->dir == SPLIT_COLS ? MIN_PANE_COLS : MIN_PANE_ROWS; - uint16_t gap = - sp->dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; + uint16_t gap = eff_gap(sp->dir); uint16_t span = sp->dir == SPLIT_COLS ? sp->rect.w : sp->rect.h; uint16_t gaps = (uint16_t)(gap * (sp->nkids - 1)); uint16_t avail = span > gaps ? (uint16_t)(span - gaps) : span; diff --git a/src/app_ui.c b/src/app_ui.c index d9e231c..d03ddd4 100644 --- a/src/app_ui.c +++ b/src/app_ui.c @@ -1004,7 +1004,12 @@ void app_compose(app_t *a, screen_t *s) { layout(a); find_corners(a); if (CFG.status_bar) draw_tab_strip(a, s); + /* Compact's shared lines go down first, so every pane's title and buttons + * land on top of them; the hover hints come after the panes, because each + * hint cell asks the finished hit list whether the boundary still owns it. */ + draw_compact_lines(a, s); draw_node(a, s, cur(a)->root); + draw_resize_hints(a, s); draw_corners(a, s); draw_min_bar(a, s); /* The floats, over everything tiled and under the modals below: painted diff --git a/src/config.c b/src/config.c index a306314..5d9d688 100644 --- a/src/config.c +++ b/src/config.c @@ -829,6 +829,7 @@ void config_defaults(config_t *c) { c->gap = 1; c->gap_aspect = 2; c->pad_top = c->pad_right = c->pad_bottom = c->pad_left = 0; + c->compact = false; c->rounded = true; c->title_align = ALIGN_CENTER; c->title_inset = 2; @@ -1558,6 +1559,8 @@ char *config_render(const config_t *c) { cb_add(&b, "padding %u %u %u %u // top right bottom left\n", c->pad_top, c->pad_right, c->pad_bottom, c->pad_left); cb_add(&b, "rounded %s\n", yesno(c->rounded)); + cb_add(&b, "compact %s // shared 1-cell borders instead of gaps\n", + yesno(c->compact)); cb_add(&b, "title_align \"%s\"\n", c->title_align == ALIGN_LEFT ? "left" : c->title_align == ALIGN_RIGHT ? "right" @@ -1827,6 +1830,7 @@ static const char *const KNOWN_TOP[] = { "bell_indicator", "bell_mark", "close_mark", + "compact", "dim_unfocused", "double_click_ms", "word_separators", @@ -2060,6 +2064,7 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, } } c->rounded = kdl_arg_bool(kdl_child(root, "rounded"), 0, c->rounded); + c->compact = kdl_arg_bool(kdl_child(root, "compact"), 0, c->compact); c->status_bar = kdl_arg_bool(kdl_child(root, "status_bar"), 0, c->status_bar); c->status_line = kdl_arg_bool(kdl_child(root, "status_line"), 0, c->status_line); |
