Skip to content

feat(ui): one proportional UI scale as window content zoom, density removed - #635

Merged
ThusMad merged 4 commits into
mainfrom
feat/ui-scale-window-zoom
Sep 21, 2026
Merged

ThusMad merged 4 commits into
mainfrom
feat/ui-scale-window-zoom

Conversation

@ThusMad

@ThusMad ThusMad commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

What & why

One proportional UI scale, applied as window content zoom; the Compact / Standard / Large density setting is removed.

Density shifted the whole application one MoonUI tier (Xs / Sm / Md). MoonUI draws each tier by hand — a toggle is 36×20 at Sm and 44×24 at Md, control heights step 20 → 24 → 32 — so stepping the app a tier substituted a different design rather than scaling the reviewed one, and the design's own mixture of tiers (Xs strips, Sm controls, Md dialogs) lost its proportions. The existing "UI zoom" slider fed MoonUI's token multipliers, which reach only the geometry that goes through tokens.ui(): raw pixel literals, persisted dock sizes and the inherited base widgets stayed put.

Now ui_scale (Settings → General, 50–200 %) is installed as MoonScale::zoom and every window's MoonRoot applies it through Window::set_content_zoom (Moonbot-Tech/MoonUI#77). The zoom folds into the window's scale factor, so every pixel, glyph and hitbox scales by construction and the design keeps its proportions at any factor. The tokens stay at the design's own size system: tier = Sm, font_delta = 3, ui = font = 1.0, spelled once as design::CONTROL_TIER / DESIGN_FONT_DELTA / BODY_TEXT / INPUT_SIZE.

The chart does not scale with the interface. chartdx sizes its render target by the frame's combined factor (the slot really is that many device pixels) and its own geometry — line widths, candle outlines, axis gutters, captions — by scale_factor / content_zoom, the platform DPI. Its text layer lays out in the chart's own logical pixels and crosses into GPUI's content pixels only in chartdx::text::content_px / chart_metrics; the overlays over chart geometry (action buttons, arbitrage names, filter headers, the volume menu) divide by the zoom in one place, chart_origin_logical. Every one of those edits is shared code above chartdx/backend.rs; the Direct3D 11, Metal and wgpu layers are untouched.

Notable decisions

  • UiDensity is deleted from moon-core, the settings page, the locales and the presentation mapping. A stored ui_density key is ignored on load (SettingsFile has no deny_unknown_fields) and dropped on the next save; it is not a reason to rewrite a file. Compact and Large users land on Standard at their stored zoom.
  • The tier resolvers in design.rs (button_tier, choice_tier, input_tier, tier_font_size, tier_text_step, tier_text_value, tier_font_base, tier_text_metrics) are gone; their call sites read the constants or the renamed text_metrics / body_font_base. checkbox_metrics(cx) became the CHECKBOX_METRICS constant, the stepper reserve in strategies/settings.rs keeps only the Sm ratio, and seven widget builders lost a context parameter they no longer read.
  • design::ui_px / ui_value stay as MoonUI's token adapter (identity while the tokens are at 1.0). Replacing their ~1200 call sites with px(v) is a mechanical follow-up kept out of this diff so the review sees the real change.
  • windowing::responsive_width reads viewport_size() (content space) instead of bounds() (screen space), so the narrow-layout thresholds compare against the width the row is actually laid out in. The chart's click series (double-click and close-residue detection, which is process-wide and therefore screen space) multiplies the press by the zoom before adding the window's screen origin.
  • The connections table's Endpoint column was budgeted for Large's 16 px body. Its basis and cap are now derived from BODY_TEXT and DESIGN_FONT_DELTA (21 IPv4 glyphs at the body size over the 1.3 text ratio; the cap adds 2 px), so the column stops taking name-column room it no longer needs.
  • The FireTest chart probe is built in platform space (bounds × zoom, factor ÷ zoom), so the Windows, Linux and macOS storms keep their math. A MOON_FIXTURE_SETTINGS hook (a settings.toml copied into the bench before the configuration is read, mirroring MOON_FIXTURE_LAYOUT) lets a scripted run open at a chosen scale.
  • theme_contract/density.rs is gone; the four of its contracts that still guard live invariants (no legacy font channel outside the documented mirror, no custom button metrics on the Settings and Analytics tabs, no literal MoonSize::Sm in chrome controls, toolbar strip text on the rendered body metrics) return as theme_contract/size_system.rs. The contract strings elsewhere follow the renamed helpers.
  • Two coordinate spaces meet at the chart and the second commit keeps them apart after two clean-context reviews: the pointer crosses content → device with the window's factor (ChartEngine::slot_scale_factor), the input container and the engine's own geometry take the chart-design factor (last_ppp), and the pane buttons, zone markers, logo and hover cards divide device pixels by the window's factor to land in content space. An engine unit test pins the two factors and the crossing. First-open window sizes and minimums (login, detached and chart windows, expert settings, report) are design pixels and multiply the content zoom in.

Known limitations

  • Linux is compile-unverified here (no toolchain on this machine); no platform-specific code is added, and the macOS probe job compiles the Metal backend.
  • The tour page was not regenerated locally (no Python here); no tour content references the removed iface.density* keys, so the Tour job is expected to stay green.
  • Follow-up (separate PR): replace design::ui_px(cx, v) with px(v).

Issues

Open issues were searched for density / zoom / scale / compact: none covers this change. #608 (chart scale hotkeys in the trade window) is unrelated.

How to verify

cargo fmt --all -- --check
cargo clippy -p moon-core -p moon-ui-gpui --all-targets    # exit 0, no new warnings in changed lines
cargo test --workspace                                      # moon-chart 140, moon-core 2055 (+ the flaky module below), moon-ui-gpui 1987 unit + 348 contract

Live, on the fixture bench (moonterminal.exe --fixture --debug-script chart-smoke) at 100 % and, with MOON_FIXTURE_SETTINGS naming a settings.toml holding ui_scale = 1.5, at 150 %: every behavioural stage passes at both scales — chart probe, mouse storm over the chart, static-text storm, command-error contract, tool windows open and dedup, Root overlay contract, live locale switch, price scale 50 % → 20 % → Auto. The run's final verdict is arrival_flash pulse_per_chart 3.3 < 5.0; a pristine origin/main build fails identically on this machine (3.2), so that is the flash pacing measurement on this hardware, not this change.

cargo test --workspace stops in moon-core on update::discovery::tests (a local scripted HTTP server): a different test of that module fails on each run, including on a pristine origin/main worktree, so it is pre-existing flakiness unrelated to config; the crates' own suites are green.

Builds on Moonbot-Tech/MoonUI#77, which is merged; the lock is re-pinned to that MoonUI revision (e4f2d4e9, only the MoonUI packages move), and cargo check -p moon-ui-gpui --locked passes with no local override.

…emoved

The Compact / Standard / Large density setting shifted the whole application
one MoonUI tier. MoonUI draws each tier by hand, so a tier step substituted a
different design rather than scaling the reviewed one, and the design's own
mixture of tiers lost its proportions. The UI zoom slider fed the token
multipliers, which reach only the geometry that goes through `tokens.ui()`.

Now `ui_scale` (50-200 %) is installed as `MoonScale::zoom`; every window's
`MoonRoot` applies it as content zoom, so every pixel, glyph and hitbox scales
by construction. The tokens stay at the design's size system, spelled once as
`design::CONTROL_TIER` / `DESIGN_FONT_DELTA` / `BODY_TEXT` / `INPUT_SIZE`;
the tier resolvers, `UiDensity`, its settings row, locale keys and the
density-migration contracts are gone.

The chart keeps device density: `chartdx` sizes its render target by the
combined factor and its own geometry and text by `scale_factor /
content_zoom`, the text layer crosses into content pixels only at the GPUI
boundary, and the overlays over chart geometry divide by the zoom in
`chart_origin_logical`. All of it is shared code above the three GPU
backends. `responsive_width` and the click series read content and screen
space as they should; the FireTest probe is built in platform space and a
`MOON_FIXTURE_SETTINGS` hook lets the bench open at a chosen scale.

Requires MoonUI with `Window::set_content_zoom` (Moonbot-Tech/MoonUI#77).
…r zoom

Two clean-context reviews of the scale change. The chart-input half had the
engine's factor (device pixels per chart-design pixel, UI zoom excluded) where
the window's (device per content pixel) belongs, so at any zoom but 100 % the
crosshair, drags and presses landed at `1 / zoom` of the pointer and the corner
overlays and hover cards sat `zoom` times too far from the pane:

- `chart_local_from_window_pos` crosses with `slot_scale_factor()`, the
  window's factor as the last frame saw it;
- the six input handlers hand the container `last_ppp`, the chart-design
  factor its pane layout and thresholds are drawn with;
- the pane close, pin, lock and broom buttons, the zone markers, the logo and
  the news, warning and trade hover cards divide device pixels by the window's
  factor to land in content space.

Also from the reviews: first-open window sizes and minimums (login, detached
and chart windows, expert settings, report) are design pixels and now multiply
the content zoom in; four contracts that still guard the size system return as
`theme_contract/size_system.rs`; stale "density" wording in comments reads
"control tier"; a tautological assertion is dropped. An engine test pins the
two factors and the pointer crossing.
@Alena-Selezneva

Copy link
Copy Markdown
Contributor

Два замечания по правке.

Плотность юзеры получили четыре дня назад. Compact / Standard / Large приехали в v0.45.3 (15.09) и были в анонсе релиза вместе с масштабом 75–150 %; тем же обновлением им уже один раз сбросили прежний «размер шрифта» на стандартную плотность и 100 %. Кто за эти дни успел поставить себе Compact или Large, на ближайшем автообновлении молча уедет обратно на Standard — второй сброс интерфейса за неделю. Менять в коде нечего: ползунок теперь берёт 50–200 %, и обе привычки восстанавливаются масштабом, причём пропорционально, чего смена tier'а как раз не давала. Но в примечания к релизу и в чат это стоит положить строкой, иначе первыми об этом напишут юзеры — и напишут не «стало пропорционально», а «размер сам поменялся».

Устаревший комментарий на поле, которое поменяло смысл. panels/chart/mod.rs:308 про last_ppp по-прежнему говорит "Last window scale factor recorded during rendering", хотя после render.rs:51 это платформенный фактор без zoom. От него считаются все пороги попадания на графике: figures/mod.rs:359 fig_hit_threshold, trade.rs:1363 (ORDER_PIN_DRAG_ARM_PX), trade.rs:690, news.rs:233 / warn.rs:242 (in_mark_row). В самих местах использования вы это расписали подробно, здесь осталось старое описание — а читать при следующей правке хитбоксов будут именно его.

…zoom

# Conflicts:
#	docs/ARCHITECTURE.md
#	docs/FIRETEST.md
@Alena-Selezneva

Copy link
Copy Markdown
Contributor

Re-read after the update — the change itself is the same one I read before, so just two things about the code.

The stale doc on last_ppp is still there, and it now carries more weight. panels/chart/mod.rs:308 still says "Last window scale factor recorded during rendering", while since render.rs:51 the field is the platform factor with the content zoom divided out. This is no longer only what figures/mod.rs:359, trade.rs:690, trade.rs:1363 and in_mark_row in news.rs:233 / warn.rs:242 derive their hit thresholds from: render_input.rs now takes let sf = this.last_ppp; in all six input handlers (199, 284, 513, 551, 685, 879), each with its own three-line comment explaining which of the two factors this is. The field's own doc is the one place that still says the old thing, and it is what the next person touching hitboxes reads first.

A doc comment lost its function in chartdx/text/mod.rs. The new content_px block was inserted directly under draw_sized_text_run's existing one-liner, so content_px now opens with "Draw the volume scale and cursor readout with their explicit size and weight." (line 224) and draw_sized_text_run at line 248 is left with no doc at all.

The release-notes point from the last read still stands: whoever writes them should say in one line that the density choice is gone and Compact/Large users land on Standard at their stored zoom — that is the second interface reset in a week for anyone who picked one, and they will otherwise report it as "the size changed by itself".

@ThusMad
ThusMad force-pushed the feat/ui-scale-window-zoom branch from 6b7e43e to 38e95f5 Compare September 21, 2026 09:28
@Alena-Selezneva

Copy link
Copy Markdown
Contributor

Third read. The only new commit is the merge from origin/main — the change itself has not moved (the six input handlers still read let sf = this.last_ppp;, now at render_input.rs:200/284/513/551/685/879), so both points from my last comment are still open:

  • panels/chart/mod.rs:308 still documents last_ppp as "Last window scale factor recorded during rendering", while since render.rs:51 it is the platform factor with the content zoom divided out. That field is what the six handlers above and the hit thresholds in figures/mod.rs, trade.rs, news.rs:in_mark_row / warn.rs:in_mark_row derive from; its own doc is the one place still saying the old thing, and it is what the next person touching hitboxes reads first.
  • chartdx/text/mod.rs:224 — the new content_px block still sits under draw_sized_text_run's old one-liner, so content_px opens with "Draw the volume scale and cursor readout with their explicit size and weight." and draw_sized_text_run at line 248 has no doc at all.

Release notes point still stands too: one line saying the density choice is gone and Compact/Large users land on Standard at their stored zoom. That is the second interface reset in a week for anyone who picked one, and they will otherwise report it as "the size changed by itself".

MoonUI #77 is merged: `Window::set_content_zoom`, `MoonScale::zoom` and the
device-pixel centring of the toggle thumb, checkbox mark and radio dot. This
branch calls that API, so the committed lock has to name a revision that has
it; the previous pin (1767a249) does not compile here.

Only the 23 packages sourced from the MoonUI repository move, 1767a249 ->
e4f2d4e9. `cargo fetch --locked` and `cargo check -p moon-ui-gpui --locked`
pass with no local override.
@Alena-Selezneva

Copy link
Copy Markdown
Contributor

Fourth read. The only new commit is c0f595f1, and the lock moves exactly the MoonUI packages from 1767a249 to e4f2d4e9 — no other package, version or source line changes, which is the same subject .github/scripts/assert-only-moonui-moved.sh compares. The code itself has not moved, so the earlier points are still open:

  • panels/chart/mod.rs:308 still documents last_ppp as "Last window scale factor recorded during rendering", while apply_slot_geometry now stores sf / zoom there — the platform factor. That field is what the chart's hit thresholds derive from, and its own doc is the one place still describing the old meaning.
  • chartdx/text/mod.rs:224content_px still opens with draw_sized_text_run's old one-liner ("Draw the volume scale and cursor readout…"), and draw_sized_text_run at line 248 has no doc at all.

And the release-notes line still stands: say that the density choice is gone and that Compact/Large users land on Standard at their stored zoom. For anyone who picked one this is the second interface reset in a week, and without that line they report it as "the size changed by itself".

@ThusMad
ThusMad merged commit 05971ad into main Sep 21, 2026
8 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

Development

Successfully merging this pull request may close these issues.

2 participants