Skip to content

Four remaining declarative-UI issues: Ghost emphasis, overlay width, shell head, dateTimeInput mode - #1038

Merged
Yaraslaut merged 5 commits into
masterfrom
feature/remaining-ui-issues-ghost-sizing-headrow
Oct 23, 2026
Merged

Yaraslaut merged 5 commits into
masterfrom
feature/remaining-ui-issues-ghost-sizing-headrow

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes #1026, closes #1028. Partially addresses #1024 and #1027 (both stay open — see below).

Four changes from the #1035 batch, each independently reviewable:

  • qt_quick: dateTimeInput has no calendar/date-picker widget, just a formatted text field #1024 (partial) — dateTimeInput() gains a DateTimeInputOptions overload, exposing mode (already fully supported by the document schema and interpreter, just never surfaced by the positional author wrapper). The issue's core ask — a calendar/date-picker in the Qt Quick backend — is not implemented here; MorphDateTimeInput.qml is unchanged.
  • ui::button has no small/inline emphasis for an action embedded in other content (a table cell, a disclosure) #1028 — ButtonEmphasis::Ghost: no border/fill, minimal padding, a faint neutral tint on hover/press — for an action embedded in other content (a table row's remove, a disclosure chevron) where full button chrome reads as a second control. Touches the enum, both document-schema emphasis arrays, both interpreter mapping chains, both author.hpp serializers, the Qt Quick leaves.cpp enum-to-int mapping (the one site that actually renders it), MorphButton.qml/MorphButtonBackground.qml (including the implicitWidth floor, which would otherwise keep a ghost button at the same width as a filled one), and the TUI fallback-to-plain-style path.
  • qt_quick: drawer's "grow to fit content" width is a circular binding that never actually grows for stretching content #1026 — Drawer and dialog gain an explicit-width sizing lever, breaking their circular width inference (a drawer always settles at its 30-unit floor; a dialog settles wherever the first measurement pass happened to land). The dialog fix needs two coordinated changes: the Popup's own width and contentColumn's tracking of popup.availableWidth, since contentItem's width is only re-asserted by Qt's resizeContent() from specific trigger points, not from a plain implicit-size change. Stretch sizing is refused on both node kinds at the two points that actually matter (a literal document value, and the shared mountKind convergence point for a computed expression or a direct C++ ui::dialog()/ui::drawer() call) — a stretching overlay placeholder would silently change how every sibling in its own parent layout is justified.
  • ui::screen has no way to mark part of its document as shell chrome, separate from the page body #1027 (partial) — The active tab's head (title + status badges + actions) now also renders in ShellFrame's own top bar, not just inside the page body. A new _headSlot sits beside the title with its own independent stretch (so it keeps claiming width even while the mounted row is invisible). Workspace::headRowForActive() exposes the same reactive row headRow() already builds for the in-body rendering — headRow's construction is split into a Row-returning helper both methods share, since a Node, once built, cannot be mutated further. shell.cpp re-mounts only on a genuine active-tab change, compared by the model's own tab id (not Workspace::active()'s index, which is a bar position, not an identity, and unsafe to compare across a close) — and resets the mount together with its comparison key ahead of _workspace, on every path that tears the workspace down directly rather than through its own model callback (the destructor, and buildFrame()'s rebuild — where a fresh Workspace instance restarts its own tab-id numbering from 1, which would otherwise collide with the pre-rebuild key and silently drop the head on the very screen a sign-in just opened).

Part (b) of #1027 (an optional second builder for arbitrary document-authored chrome content) is explicitly out of scope — this covers only the ScreenHead-shaped repro the issue reports. Trailing-edge action alignment inside the mounted head row is also deliberately not attempted: it would need a change to ui::spacer() that also affects the existing in-body rendering.

Design process

This batch went through six rounds of adversarial review (a second model instance independently re-verifying every claim against the live source — including executing qmllint against the proposed QML and tracing Qt's own C++ internals for the overlay-width circularity) before implementation started. The design document is at docs/superpowers/specs/2026-10-22-remaining-ui-issues-design.md (not part of this PR's diff — tracked separately).

Test plan

  • morph_tests (core, 3133/3134 — the one "failure" is default-sink-coverage, an intentional negative-path assertion)
  • morph_tui_tests (TUI, 338/339 — the one failure, test_tui_fields.cpp's year-10000 date formatting, pre-dates this branch and is unrelated to any file touched here)
  • morph_qt_quick_tests (headless Qt Quick, 139/141 — the two failures, test_qt_quick_theme.cpp/test_qt_quick_containers.cpp font-family detection, are offscreen-platform font-availability issues pre-dating this branch)
  • morph_client_tests (30/30 relevant — one failure in test_sign_in.cpp is an unescaped-Windows-backslash-in-JSON bug in a test fixture, pre-dating this branch, unrelated to anything touched here)
  • clang-format clean on every changed file
  • New, targeted tests added per issue: document-schema round-trips, mount-layer backend dumps, author-side JSON serialization, a TUI bold/plain-style assertion, a real Qt Quick rendering assertion (ghost button width/color), two real Qt Quick drawer/dialog Fixed-width assertions, and a Workspace/ShellFrame head-row-in-two-places assertion.

🤖 Generated with Claude Code

@Yaraslaut
Yaraslaut force-pushed the feature/remaining-ui-issues-ghost-sizing-headrow branch 2 times, most recently from 06068e6 to a8ad908 Compare October 22, 2026 22:20
Yaraslaut and others added 4 commits October 23, 2026 01:12
dateTimeInput() was a positional function with no way to set mode,
even though the document schema and interpreter already fully support
it. Add DateTimeInputOptions, matching TextInputOptions/
FilePickerOptions's own pattern, alongside the existing positional
overload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A ghost button has no border or fill, minimal (space.1) padding, and a
faint neutral tint on hover/press, for an action embedded in other
content (a table row's remove, a disclosure chevron) where full button
chrome reads as a second control beside it rather than part of it.

Touches the enum, both document-schema emphasis arrays, both
interpreter string-to-enum chains, both author.hpp enum-to-string
chains, the Qt Quick enum-to-QML-int mapping (the one site that
actually renders the feature), MorphButton.qml/
MorphButtonBackground.qml (including the implicitWidth floor, which
would otherwise keep every ghost button at the same width as a filled
one), and the TUI fallback-to-plain-style path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ar inference (#1026)

MorphDrawer.qml and MorphDialog.qml both infer their width from their
own content's implicit size, which reports back roughly the width the
content was just given rather than what it actually needs -- a
drawer settles at its 30-unit floor, a dialog settles wherever the
first measurement pass happened to land. A document's own `sizing`
now wins outright when given: Fixed on the drawer binds Drawer.width
directly; on the dialog, both the Popup's own width and
contentColumn's tracking of popup.availableWidth must change together,
since contentItem's width is only re-asserted by Qt's own
resizeContent() from specific trigger points, not from a plain change
of its implicit size.

Both overlays' root is only a zero-sized placeholder for the real
on-screen overlay; without an explicit Layout.preferredWidth/Height
override, the inherited MorphWidget binding would make that invisible
placeholder claim real space in whatever layout holds the node.

Stretch sizing is refused on both node kinds -- not silently
ignored -- since a stretching overlay placeholder would change how
every sibling in its own parent layout is justified. Covers the
document's own literal check and, in one place, both the document's
computed-expression path and a C++ author calling ui::dialog()/
ui::drawer() directly, since both converge at the same mountKind
overload before either backend sees the result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e body (#1027)

A tab's title + primary-actions head sits above its screen today, but
that row is inside the page the workspace's tabs sit on, which cannot
reach the window's own top bar -- the issue's repro.
ShellFrame gains a dedicated _headSlot beside the title, built in the
constructor like every other top-bar child, with its own independent
stretch so it keeps claiming width even while the mounted row is
invisible (a screen with no head). Workspace::headRowForActive()
exposes the same reactive head Node headRow() already builds for the
in-body rendering, read directly off the model's own active tab id --
splitting headRow()'s construction into a Row-returning helper both
methods share, since a Node, once built, cannot be mutated further.

shell.cpp mounts it, re-mounting only on a genuine active-tab change
(compared by tab id, not Workspace::active()'s own index, which is a
bar position and not stable across a close) -- the mounted Node is
itself reactive, so in-tab status/action changes update without a
re-mount. The mount and its comparison key reset together, ahead of
_workspace, on every path that tears the workspace down directly
rather than through its own model callback: the destructor and
buildFrame()'s own rebuild (where a fresh Workspace instance restarts
its tab-id numbering from 1, which would otherwise collide with the
pre-rebuild key and silently drop the head on the very screen a sign-in
just opened).

Trailing-edge action alignment inside the row is deliberately not
attempted here: pushing the row's own spacer to stretch would need a
change to ui::spacer() that also affects the existing in-body
rendering, out of scope for a change that only adds a new mount point.
Part (b) of the issue -- an optional second builder for arbitrary
document-authored chrome content -- is explicitly out of scope; this
covers only the ScreenHead-shaped repro the issue actually reports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Yaraslaut
Yaraslaut force-pushed the feature/remaining-ui-issues-ghost-sizing-headrow branch from 103eb96 to 8fd3e4e Compare October 22, 2026 23:22
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.37500% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/ui/interpreter.hpp 0.00% 2 Missing and 2 partials ⚠️
include/morph/ui/document.hpp 83.33% 0 Missing and 3 partials ⚠️
include/morph/ui/author.hpp 83.33% 2 Missing ⚠️
include/morph/ui/mount.hpp 92.30% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Yaraslaut

Copy link
Copy Markdown
Member Author

CI status: every check is green except Linux / gcc-release, which fails on a pre-existing -Werror=maybe-uninitialized false positive from GCC 15 in tests/test_ui_interpreter_detail.cpp (morph::ui::detail::engaged, interpreter.hpp:1807) — a file and function this PR does not touch. Confirmed master's own latest CI run (39845171669) fails with the byte-identical error, even after #1037's "fix the red master CI" merged. Not something this PR can or should work around.

Also note: this branch originally conflicted with #1037 in scripts/branch_partial_allowlist.json — both PRs independently corrected the same stale line-number hints into document.hpp (for unrelated reasons: #1037 for its own fix, this PR because checkOverlaySizing's extraction shifted two default: labels). Resolved via rebase; clang-coverage now passes with the merged line numbers.

…d() test

GCC 15.2 at -O3 loses the variant's index once a list's make_shared is
inlined into the test, and reports engaged()'s string branch as reading
an uninitialized length on a value that holds a list. Neither naming the
values nor rewriting engaged() as a switch on kind() changes that, so the
suppression is scoped to the one test case. This fails the gcc-release
leg on master too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
@Yaraslaut

Copy link
Copy Markdown
Member Author

The gcc-release failure is now fixed in this PR (b7890fc), so the earlier comment about it is out of date. The -Wmaybe-uninitialized warning at test_ui_interpreter_detail.cpp:229-230 is a GCC 15 false positive: at -O3, once a list's make_shared is inlined, GCC loses track of which variant alternative is held. It also fails on master.

Reproduced locally with GCC 15.2 at -O3 -DNDEBUG, using CI's warning flags, in the fastcache-fleet-toolchain image: unmodified master at f13c29a gives the same two errors and exit 1. Naming the values in the test didn't help, and neither did rewriting engaged() as a switch on kind(). A -Wmaybe-uninitialized suppression scoped to that one test case compiles cleanly (exit 0). The full CI run for this push is still pending.

🤖 Generated with Claude Code

@Yaraslaut
Yaraslaut merged commit 65fe24f into master Oct 23, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant