summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-14 00:13:18 +0900
committerAdam Malczewski <[email protected]>2026-06-14 00:13:18 +0900
commitf7df36c8f656652e8245d781b35cb3252f17bad2 (patch)
treed61d7f857c910070f1bc38359bad0b354b08774b /packages
parentc4939d736ff55fa48cff31a26333f4922430c3d8 (diff)
downloadunbox-f7df36c8f656652e8245d781b35cb3252f17bad2.tar.gz
unbox-f7df36c8f656652e8245d781b35cb3252f17bad2.zip
ext-stage-dock: full-height left rail — 288px wide, gradient, centered cards
Turn the card-hugging dock into a full-height left rail. - C++ (extension.cpp/dock_layout.hpp): the surface is now the full OUTPUT HEIGHT (hug reverted; content_height helpers/tests dropped) and kDockWidth widened 240 -> 288 (~20%). Cards unchanged at 224dp. set_visible still hides the rail when empty, so it only appears when there are minimized windows. - RCSS (dock.rcss): body.dock fills the surface (width:100% height:100%) as a flex COLUMN scroll container with a `horizontal-gradient(#000000ff #00000000)` decorator — fully-opaque black at the left screen edge fading to transparent across the wider rail. Cards centered both axes (align-items:center + a `div.rail` margin:auto wrapper that vertically centers when they fit and collapses gracefully — no flex-center+overflow strand — scrolling when they overflow). Scrollbars hidden (scrollbarvertical/horizontal size 0). - RML (dock.rml): a `div.rail` wrapper around the data-for slot list enables the center-or-scroll pattern. Accepted caveat (quick path): the full-height 288px surface consumes pointer/touch across the left strip while shown; the deferred input-transparent UiSurfaceSpec flag is the real fix. Real-seat verified via live RML/RCSS hot-reload. ext-stage-dock 2/2 green on build + build-asan.
Diffstat (limited to 'packages')
-rw-r--r--packages/ext-stage-dock/src/dock_layout.hpp89
-rw-r--r--packages/ext-stage-dock/src/extension.cpp121
-rw-r--r--packages/ext-stage-dock/tests/test_policy.cpp114
3 files changed, 69 insertions, 255 deletions
diff --git a/packages/ext-stage-dock/src/dock_layout.hpp b/packages/ext-stage-dock/src/dock_layout.hpp
index b02d819..d36dba7 100644
--- a/packages/ext-stage-dock/src/dock_layout.hpp
+++ b/packages/ext-stage-dock/src/dock_layout.hpp
@@ -2,16 +2,17 @@
#include <algorithm>
-// Pure decision core 2 — DOCK LAYOUT: the geometry mapping a reveal fraction +
-// slot count to on-screen rects for the stage dock. No wlroots / GL / RMLUi —
-// plain ints in, plain rects out, doctest-covered in tests/test_policy.cpp.
+// Pure decision core 2 — DOCK LAYOUT: the geometry of the stage-dock FRAME.
+// No wlroots / GL / RMLUi — plain ints in, plain rects out, doctest-covered in
+// tests/test_policy.cpp.
//
// Fork B division of labour: the RML/RCSS inside the dock document does the
-// in-dock slot FLOW (wrapping, styling); this core owns the dock FRAME (its
-// on-screen rect as the reveal slides it in from the left), the reveal OFFSET,
-// and the SCROLL/CAPACITY math (how many slots fit, total content height, and a
-// per-slot rect for callers that want compositor-side placement). All glue (c2/
-// d1) consumes these; this file calls nothing back.
+// in-dock slot FLOW (the full-height rail's flex centering, scrolling, card
+// styling); this core owns ONLY the dock FRAME — its on-screen rect as the
+// reveal slides it in from the left. (The card-stack capacity / content-height /
+// per-slot-rect math the earlier hug-the-cards sizing used now lives in the
+// RCSS, so those helpers were removed when the dock became a full-height rail.)
+// All glue consumes this; this file calls nothing back.
//
// Single wl_event_loop thread throughout.
@@ -29,24 +30,20 @@ struct Box {
};
// Static metrics of the dock on a given output. `output_w`/`output_h` are the
-// output's pixel size. `dock_width` is the revealed dock's on-screen width.
-// `slot_height` is one preview slot's height; `gap` the vertical space between
-// slots; `pad` the inner margin at the top (and conceptually all edges) of the
-// dock content. All in px.
+// output's pixel size. `dock_width` is the revealed dock's on-screen width. All
+// in px.
struct DockMetrics {
int output_w = 0;
int output_h = 0;
int dock_width = 320;
- int slot_height = 96;
- int gap = 8;
- int pad = 8;
};
// The dock's on-screen rect at reveal fraction f in [0,1]. The dock slides
// horizontally: at f=0 it is fully hidden off the left (x == -dock_width); at
// f=1 it is fully revealed (x == 0). x is monotonic non-decreasing in f. y/h
-// cover the full output height; w is always dock_width (the dock keeps its
-// width and translates — it does not grow). f is clamped to [0,1].
+// cover the FULL output height (the dock is a full-height left rail); w is
+// always dock_width (the dock keeps its width and translates — it does not
+// grow). f is clamped to [0,1].
[[nodiscard]] inline auto dock_box(const DockMetrics& m, double fraction) -> Box {
const double f = std::clamp(fraction, 0.0, 1.0);
// x goes from -dock_width (f=0) to 0 (f=1): x = -dock_width * (1 - f).
@@ -54,62 +51,4 @@ struct DockMetrics {
return Box{.x = x, .y = 0, .w = m.dock_width, .h = m.output_h};
}
-// One slot's full stride: its height plus the gap below it.
-[[nodiscard]] inline auto slot_stride(const DockMetrics& m) -> int {
- return m.slot_height + m.gap;
-}
-
-// How many slots fit in the revealed dock WITHOUT scrolling. The usable height
-// is the output height minus the top+bottom pad; each slot occupies
-// slot_height and slots are separated by `gap` (no gap after the last). Returns
-// 0 if nothing fits. Independent of `count` — this is pure capacity.
-[[nodiscard]] inline auto visible_slots(const DockMetrics& m) -> int {
- const int usable = m.output_h - 2 * m.pad;
- if (usable < m.slot_height || m.slot_height <= 0) {
- return 0;
- }
- // usable >= slot_height + k*(slot_height+gap) => k = (usable - slot_height) / stride
- const int stride = slot_stride(m);
- if (stride <= 0) {
- return 1; // degenerate gap+height; at least the one that fit above
- }
- return 1 + (usable - m.slot_height) / stride;
-}
-
-// The total content height needed to stack `count` slots: top pad + count slots
-// with `gap` between them + bottom pad (no trailing gap). 0 slots -> 0 (an empty
-// dock has no content). Used to clamp the scroll range.
-[[nodiscard]] inline auto content_height(const DockMetrics& m, int count) -> int {
- if (count <= 0) {
- return 0;
- }
- return 2 * m.pad + count * m.slot_height + (count - 1) * m.gap;
-}
-
-// The POSITIVE surface height that hugs `count` cards: content_height(count)
-// clamped to a strictly-positive minimum. The ui substrate REJECTS a surface
-// with non-positive geometry (create_surface/set_size return nullptr + log an
-// error), so the empty dock (count 0 -> content_height 0) must still be created
-// / resized at a positive size and merely hidden (set_visible(false)), never at
-// height 0. Returns max(content_height(count), 1): >= 1 for every count >= 0,
-// equal to content_height once there is at least one card. (Width never hits 0
-// in practice — dock_width is a fixed positive constant — but callers should
-// likewise guard it; this helper covers the height, which is the count-driven
-// dimension.)
-[[nodiscard]] inline auto surface_height(const DockMetrics& m, int count) -> int {
- return std::max(1, content_height(m, count));
-}
-
-// The on-screen rect of slot `i` (0-based) within the dock content, given the
-// current vertical `scroll` offset (px scrolled DOWN; 0 = top). The slot's
-// content-space top is pad + i*(slot_height+gap); subtracting `scroll` yields
-// its screen y. x is the inner pad; width is dock_width minus pad on both sides
-// (clamped to >= 0). A negative or off-screen y is returned as-is (the caller /
-// RCSS clips); this core does not cull.
-[[nodiscard]] inline auto slot_box(const DockMetrics& m, int i, int scroll) -> Box {
- const int content_y = m.pad + i * slot_stride(m);
- const int w = std::max(0, m.dock_width - 2 * m.pad);
- return Box{.x = m.pad, .y = content_y - scroll, .w = w, .h = m.slot_height};
-}
-
} // namespace unbox::ext_stage_dock::layout
diff --git a/packages/ext-stage-dock/src/extension.cpp b/packages/ext-stage-dock/src/extension.cpp
index b859a04..08e3800 100644
--- a/packages/ext-stage-dock/src/extension.cpp
+++ b/packages/ext-stage-dock/src/extension.cpp
@@ -9,6 +9,7 @@
#include <unbox/kernel/ui.hpp>
#include <unbox/kernel/wlr.hpp>
+#include <algorithm>
#include <cstddef>
#include <memory>
#include <stdexcept>
@@ -52,36 +53,12 @@ using Toplevel = ext_xdg_shell::Toplevel;
constexpr std::uint32_t kMinimizeKeysym = XKB_KEY_m; // 0x06d
constexpr std::uint32_t kMinimizeMods = WLR_MODIFIER_LOGO; // Super/LOGO
-// The dock width (revealed) in px — matches the dock_layout default. For c2 the
-// dock sits fully revealed (no reveal animation); d1 animates dock_box(f).
-constexpr int kDockWidth = 240;
-
-// Card-stack metrics, in px, that MIRROR the kDockRml RCSS so the surface rect
-// can be sized (via dock_layout::content_height) to hug the rendered card stack
-// rather than the full output height. dp == px (substrate dp-ratio is 1.0), so
-// these are the RCSS dp values:
-// kCardHeight — one div.slot's OUTER (border-box) height. The card IS the
-// preview now (a full-bleed `div.thumb` child carries the image() decorator
-// and the rounded `overflow:hidden` slot clips it; the title is OVERLAID
-// absolute at the bottom — neither child adds to the box height). The card is
-// a FIXED EXPLICIT box: `div.slot { width: 224dp; height: 140dp; }`. Both
-// dimensions MUST be explicit: the slot's only children are out-of-flow
-// (absolute), so an auto/`width:100%` box has no in-flow content and
-// COLLAPSED (decorators paint inside the box but contribute NO layout size) —
-// only a ~10dp sliver of the rounded edge rendered. The explicit 224dp ×
-// 140dp box fixes that. 224 = the dock inner width (240 dock_width - 2*8dp
-// body padding); 140 ≈ a 16:10 landscape card at 224 wide (224*10/16=140), a
-// sane thumbnail aspect. A fixed box keeps the surface-hugging math
-// deterministic; the thumb decorator's `cover center center` fit fills the
-// box (centered) for any source aspect without distortion (cropping overflow).
-// kCardGap — the inter-card vertical space (div.slot margin-bottom: 8dp).
-// kStripPad — the strip's inner top/bottom margin (body.dock padding: 8dp).
-// content_height(count) = 2*kStripPad + count*kCardHeight + (count-1)*kCardGap.
-// MUST stay in lockstep with the RCSS height/margin/padding AND with
-// tests/test_policy.cpp's expected hug heights.
-constexpr int kCardHeight = 140;
-constexpr int kCardGap = 8;
-constexpr int kStripPad = 8;
+// The dock width (revealed) in px. The rail is kDockWidth wide x the full output
+// height tall (dock_box). d1 animates the reveal via the body translateX in RCSS
+// (not by resizing the surface). 288 = the original 240 widened ~20% so the
+// horizontal gradient (dark left -> transparent right) extends farther right;
+// the cards stay 224dp (RCSS) left-aligned, so the extra width is to their right.
+constexpr int kDockWidth = 288;
// A minimized window's dock entry: the live Toplevel* borrow (valid until its
// unmapped event), the frozen Preview (owns the imported texture; null when the
@@ -300,13 +277,10 @@ private:
// -> the body slides back out; we DEFER set_visible(false) until the
// slide-out finishes (on_dock_settled, fired by RmlUi's transitionend
// through the existing event binding) so the close animation is seen.
- // The surface rect tracks the slot count: whenever there is at least one
- // slot we set_size the height to content_height(count) so the rect hugs the
- // card stack (brief §3 — minimize the input-capturing area). We do NOT
- // shrink to 0 the instant the dock empties: that would collapse the surface
- // mid slide-OUT and the close animation would not be seen. Instead we keep
- // the last non-empty height through the slide and shrink to 0 in
- // on_dock_settled (with set_visible(false)). The width is fixed (kDockWidth).
+ // The surface is a fixed full-height rail; its size never changes with the
+ // card count (the RCSS scrolls/centers the cards within it). Only visibility
+ // toggles: shown when there is >= 1 slot, hidden (after the slide-out) when
+ // empty so the rail is not always eating the left strip.
//
// No-op on the visual when the surface is null (no-GL backend); the model is
// still tracked, and slot_count()/the c2 invariants are unchanged.
@@ -316,13 +290,6 @@ private:
}
dock_surface_->dirty("slots");
- // Resize the rect to hug the cards while non-empty (growing OR shrinking
- // by one of several). When empty, defer the collapse to on_dock_settled.
- if (!slots_.empty()) {
- const int w = layout::dock_box(dock_metrics(), 1.0).w;
- dock_surface_->set_size(w, surface_height_for(slots_.size()));
- }
-
const bool want_open = !slots_.empty();
if (want_open == open_) {
return; // reveal state unchanged (e.g. minimize a 2nd window)
@@ -351,31 +318,34 @@ private:
// again-open dock: we re-check open_.
void on_dock_settled() {
if (dock_surface_ != nullptr && closing_ && !open_) {
+ // Slide-out finished and the dock is empty: hide the full-height rail
+ // so it stops compositing AND stops capturing input over the left
+ // strip. The surface keeps its full height (no resize) for the next
+ // reveal; only visibility toggled.
dock_surface_->set_visible(false);
- // Collapse the rect now that the slide-out is done and the surface is
- // hidden: a hidden empty dock captures NO input. Deferred to here (not
- // refresh_slots) so the card stack stays sized through the slide. The
- // height is the positive 1px placeholder (count==0), never 0.
- const int w = layout::dock_box(dock_metrics(), 1.0).w;
- dock_surface_->set_size(w, surface_height_for(slots_.size())); // count==0 -> 1px
}
closing_ = false;
}
// Create the dock UiSurface (overlay, left edge) and register all data
- // bindings BEFORE the first frame. The surface rect HUGS the card stack: its
- // width is the dock_box width, its height is content_height(slot count) —
- // NOT the full output height — because the substrate consumes pointer/touch
- // over the whole rect regardless of visual transparency, so a transparent
- // full-height strip would still eat clicks over the empty area (brief §3).
- // Height tracks the slot count via set_size in refresh_slots(); at create
- // the dock is empty so the height is a POSITIVE 1px placeholder (the
- // substrate rejects non-positive geometry — see surface_height_for) and the
- // surface is hidden (spec.visible=false) until the first slot. Null surface
- // (no-GL backend) is fine — we just skip it and the model is still tracked.
+ // bindings BEFORE the first frame. The surface is a FULL-HEIGHT left RAIL:
+ // kDockWidth (240) wide x the full OUTPUT HEIGHT tall, at the output's
+ // top-left, REGARDLESS of card count (the RCSS owns the in-rail flow:
+ // flex-column centering + overflow-y scroll). It is hidden (spec.visible =
+ // false) until the first slot, so the rail only appears when there are
+ // minimized windows (it does not always eat the left strip). set_size never
+ // changes the height afterwards — only visibility toggles.
+ //
+ // ACCEPTED CAVEAT (deferred): while shown, the full-height surface captures
+ // pointer/touch across the whole 240px left strip — windows under it there
+ // are not clickable. Width is kept minimal (240). The real fix is the
+ // deferred input-transparent UiSurfaceSpec flag (report change-req).
+ //
+ // Null surface (no-GL backend) is fine — we just skip it and the model is
+ // still tracked.
void create_dock_surface() {
const layout::DockMetrics m = dock_metrics();
- const layout::Box frame = layout::dock_box(m, 1.0); // fully revealed (c2)
+ const layout::Box frame = layout::dock_box(m, 1.0); // full-height rail
kernel::UiSurfaceSpec spec;
// External asset (RELATIVE to the asset root the orchestrator wires) so
@@ -385,7 +355,10 @@ private:
spec.x = frame.x;
spec.y = frame.y;
spec.width = frame.w;
- spec.height = surface_height_for(slots_.size()); // hug the card stack
+ // Full output height. Guard >= 1: the substrate rejects non-positive
+ // geometry, and on a backend with no output yet frame.h could be 0 (the
+ // dock is hidden until a slot exists anyway).
+ spec.height = std::max(1, frame.h);
spec.layer = kernel::SceneLayer::overlay;
spec.visible = false; // shown when slot count > 0
@@ -426,9 +399,8 @@ private:
// Dock metrics from the first output's size (queried via output_layout). On
// a backend with no output yet, falls back to 0x0 (the dock is hidden until
- // a slot exists anyway). The dock keeps its fixed width; its HEIGHT now hugs
- // the card stack (content_height), not the full output (input caveat, §3).
- // The card-stack dims (slot_height/gap/pad) mirror the kDockRml RCSS.
+ // a slot exists anyway). dock_box() turns these into the full-height rail
+ // rect (kDockWidth wide x output height tall, at the output top-left).
[[nodiscard]] auto dock_metrics() const -> layout::DockMetrics {
int ow = 0;
int oh = 0;
@@ -449,28 +421,9 @@ private:
m.output_w = ow;
m.output_h = oh;
m.dock_width = kDockWidth;
- // Card-stack dims mirror the kDockRml RCSS so content_height() yields the
- // surface height that hugs the rendered cards (see kCardHeight et al.).
- m.slot_height = kCardHeight;
- m.gap = kCardGap;
- m.pad = kStripPad;
return m;
}
- // The POSITIVE surface HEIGHT that hugs `count` cards: the strip's content
- // height (2*pad + count*card + (count-1)*gap) per dock_layout, CLAMPED to a
- // strictly-positive minimum (>= 1). The substrate REJECTS non-positive
- // geometry (create_surface/set_size return nullptr + log
- // "surface needs positive geometry"), so the EMPTY dock (count 0 ->
- // content_height 0) must be created/resized at a positive size and merely
- // hidden (set_visible(false)), never at height 0. The substrate consumes
- // input over the whole rect, so sizing height to the card stack (not the
- // full output) leaves the rest of the screen interactive (brief §3 caveat);
- // the empty-dock 1px placeholder is hidden, so it captures nothing.
- [[nodiscard]] auto surface_height_for(std::size_t count) const -> int {
- return layout::surface_height(dock_metrics(), static_cast<int>(count));
- }
-
const kernel::Manifest manifest_{
.id = "stage-dock",
.tier = kernel::Tier::standard,
diff --git a/packages/ext-stage-dock/tests/test_policy.cpp b/packages/ext-stage-dock/tests/test_policy.cpp
index ed80daa..b07739b 100644
--- a/packages/ext-stage-dock/tests/test_policy.cpp
+++ b/packages/ext-stage-dock/tests/test_policy.cpp
@@ -133,9 +133,7 @@ TEST_CASE("an inactive (non-edge) recognizer is inert") {
// ============================================================================
static auto metrics() -> lay::DockMetrics {
- return lay::DockMetrics{
- .output_w = 1920, .output_h = 1080, .dock_width = 300,
- .slot_height = 100, .gap = 10, .pad = 20};
+ return lay::DockMetrics{.output_w = 1920, .output_h = 1080, .dock_width = 300};
}
TEST_CASE("dock_box: f=0 fully off-screen left, f=1 flush at x==0") {
@@ -165,98 +163,22 @@ TEST_CASE("dock_box: x is monotonic non-decreasing in f and clamps outside [0,1]
CHECK(lay::dock_box(m, 2.0).x == 0); // clamped to f=1
}
-TEST_CASE("visible_slots: 0/1/many capacity") {
- // usable = 1080 - 40 = 1040; stride = 110; 1 + (1040-100)/110 = 1 + 8 = 9.
- CHECK(lay::visible_slots(metrics()) == 9);
-
- // Exactly one slot fits.
- lay::DockMetrics one{.output_w = 0, .output_h = 140, .dock_width = 300,
- .slot_height = 100, .gap = 10, .pad = 20};
- CHECK(lay::visible_slots(one) == 1); // usable 100 == slot_height
-
- // Nothing fits (usable < slot_height).
- lay::DockMetrics none{.output_w = 0, .output_h = 100, .dock_width = 300,
- .slot_height = 100, .gap = 10, .pad = 20};
- CHECK(lay::visible_slots(none) == 0); // usable 60 < 100
-}
-
-TEST_CASE("content_height: 0/1/many slots") {
- auto m = metrics(); // pad 20, slot 100, gap 10
- CHECK(lay::content_height(m, 0) == 0);
- CHECK(lay::content_height(m, 1) == 2 * 20 + 100); // 140, no gap
- CHECK(lay::content_height(m, 3) == 2 * 20 + 3 * 100 + 2 * 10); // 360
-}
-
-// The glue (src/extension.cpp) sizes the dock SURFACE rect to HUG the card stack
-// via content_height(card-stack metrics, slot count) instead of the full output
-// height — so the transparent strip captures input only over the cards (brief
-// §3: the substrate consumes input over the whole rect regardless of visual
-// transparency). These are the exact px values the surface height takes, with
-// the card-stack metrics that mirror the kDockRml RCSS (kCardHeight=140 — the
-// card IS the preview now, a fixed 16:10-ish box with the title OVERLAID (the
-// title no longer adds to the box height); kCardGap=8 inter-card margin;
-// kStripPad=8 body padding). Keep in lockstep with src/extension.cpp's
-// kCard*/kStripPad constants + dock_metrics().
-TEST_CASE("dock surface height hugs the card stack (content_height with RCSS card metrics)") {
- // Mirror src/extension.cpp: kCardHeight=140, kCardGap=8, kStripPad=8.
- lay::DockMetrics card{.output_w = 1920, .output_h = 1080, .dock_width = 240,
- .slot_height = 140, .gap = 8, .pad = 8};
- // Empty dock -> 0 content (but the SURFACE is clamped positive, below).
- CHECK(lay::content_height(card, 0) == 0);
- // One card -> 2*pad + card (no trailing gap).
- CHECK(lay::content_height(card, 1) == 2 * 8 + 140); // 156
- // Two cards -> +gap between them.
- CHECK(lay::content_height(card, 2) == 2 * 8 + 2 * 140 + 1 * 8); // 304
- // Many cards grow linearly and stay FAR under the full output height, so the
- // surface never spans the whole left edge (the hug-the-cards property).
- CHECK(lay::content_height(card, 4) == 2 * 8 + 4 * 140 + 3 * 8); // 600
- CHECK(lay::content_height(card, 4) < card.output_h); // < 1080
-}
-
-// REGRESSION GUARD (0-geometry boot bug): the ui substrate REJECTS a surface
-// with non-positive geometry ("surface needs positive geometry") and returns
-// nullptr, so the EMPTY dock must be created/resized at a POSITIVE height, not 0.
-// surface_height() — the helper the glue's surface_height_for() delegates to —
-// clamps content_height to >= 1, so create_surface/set_size are never called
-// with height 0. Cover EVERY count the glue can produce, especially the empty
-// case the headless test cannot distinguish from the substrate-null path.
-TEST_CASE("surface_height is ALWAYS positive (empty-dock 0-geometry guard)") {
- lay::DockMetrics card{.output_w = 1920, .output_h = 1080, .dock_width = 240,
- .slot_height = 140, .gap = 8, .pad = 8};
- // The empty dock: content_height is 0, but the surface height is clamped to 1.
- CHECK(lay::content_height(card, 0) == 0);
- CHECK(lay::surface_height(card, 0) == 1); // positive placeholder (hidden)
- // Once there is at least one card, surface_height == content_height (>0).
- CHECK(lay::surface_height(card, 1) == lay::content_height(card, 1));
- CHECK(lay::surface_height(card, 4) == lay::content_height(card, 4));
- // Never non-positive for any plausible count (incl. a negative/degenerate).
- for (int n = -2; n <= 20; ++n) {
- CHECK(lay::surface_height(card, n) >= 1);
+// FULL-HEIGHT RAIL: the dock surface is kDockWidth wide x the FULL OUTPUT HEIGHT
+// tall, REGARDLESS of card count (the RCSS owns the in-rail flex centering +
+// overflow scroll; the C++ no longer sizes the surface to the card stack). The
+// revealed frame the glue feeds to UiSurfaceSpec/set_position is dock_box(m, 1.0)
+// at x==0, full output height — this is what the glue's create_dock_surface uses
+// for spec.width/height. (The earlier hug-the-cards content_height/surface_height
+// helpers were removed with the rail change; the RCSS scrolls the overflow.)
+TEST_CASE("dock_box: revealed rail is dock_width x full output height, count-independent") {
+ // Various output heights -> the rail's h always equals output_h (never the
+ // card-stack content height); w always dock_width; revealed x == 0.
+ for (int oh : {600, 1080, 1440}) {
+ lay::DockMetrics m{.output_w = 1920, .output_h = oh, .dock_width = 288};
+ auto rail = lay::dock_box(m, 1.0);
+ CHECK(rail.x == 0);
+ CHECK(rail.y == 0);
+ CHECK(rail.w == 288);
+ CHECK(rail.h == oh); // FULL output height, independent of any card count
}
- // Even with degenerate (zeroed) metrics — defensive: still >= 1, never 0.
- lay::DockMetrics zero{};
- CHECK(lay::surface_height(zero, 0) >= 1);
- CHECK(lay::surface_height(zero, 3) >= 1);
-}
-
-TEST_CASE("slot_box: vertical stacking by stride, inset width") {
- auto m = metrics(); // pad 20, slot 100, gap 10, dock_width 300
- auto s0 = lay::slot_box(m, 0, 0);
- CHECK(s0.x == 20); // inner pad
- CHECK(s0.y == 20); // pad + 0*stride
- CHECK(s0.w == 300 - 2 * 20); // dock_width minus pad both sides = 260
- CHECK(s0.h == 100);
-
- auto s1 = lay::slot_box(m, 1, 0);
- CHECK(s1.y == 20 + 110); // pad + 1*stride(110) = 130
- auto s2 = lay::slot_box(m, 2, 0);
- CHECK(s2.y == 20 + 220); // 240
-}
-
-TEST_CASE("slot_box: scroll offset shifts slots up") {
- auto m = metrics();
- auto s2_unscrolled = lay::slot_box(m, 2, 0);
- auto s2_scrolled = lay::slot_box(m, 2, 150);
- CHECK(s2_scrolled.y == s2_unscrolled.y - 150);
- CHECK(s2_scrolled.x == s2_unscrolled.x); // scroll is vertical only
}