From 6f82b4f72c687cba631408085e0be7b3ac4d1c17 Mon Sep 17 00:00:00 2001 From: kirillDevPro <113171057+kirillDevPro@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:44:56 +0200 Subject: [PATCH 1/2] feat(chrome): proportional type for prose, mono kept for every value Every window and dock panel pins Geist Mono at its render root, so a Settings description, a dialog sentence or a toolbar caption reads in the same face as a price. Monospace prose is wider, harder to scan and looks unfinished next to the tables it explains. Apply one rule per element: a value the user compares across rows or reads as a figure (table cell, number, coin, core or strategy name, log line, axis label, timestamp, hotkey) stays mono; a label, sentence, heading or control caption takes the UI face. Settings, Login and the order-edit dialog flip their root and re-pin every value; Strategies, Analytics, the tuner and every table panel keep a mono root and flip prose per element. Every width that is measured for a flipped string is measured in the same face it renders in, through the new `ui_body_text_width` / `ui_caption_text_width` partners in design.rs. Window titles, dock tab labels, status-bar items, tooltips, dropdown triggers and group-box titles stay mono: MoonUI forces the face there and exposes no prop. A locale string that welds a label to a figure stays mono as a whole. No locale value changes. Claude-Session: https://claude.ai/code/session_01RaQ2tnAyr4pyiAiwGktPBv --- .../src/analytics/calendar/mod.rs | 8 + .../src/analytics/profit_monitor/mod.rs | 1 + .../src/analytics/profit_monitor/settings.rs | 3 + .../src/analytics/profit_monitor/table.rs | 4 + crates/moon-ui-gpui/src/analytics/render.rs | 1 + .../moon-ui-gpui/src/analytics/summary/mod.rs | 5 + crates/moon-ui-gpui/src/analytics/toolbar.rs | 135 +++---- .../src/analytics/tuner/coins/mod.rs | 69 ++-- .../src/analytics/tuner/coins/picker.rs | 1 + .../src/analytics/tuner/filter/mod.rs | 17 +- .../src/analytics/tuner/list/mod.rs | 30 +- .../src/analytics/tuner/list/table.rs | 4 + .../moon-ui-gpui/src/analytics/tuner/save.rs | 9 + .../src/analytics/tuner/shared.rs | 3 + .../moon-ui-gpui/src/analytics/tuner/shell.rs | 42 ++- .../src/analytics/tuner/time/grid.rs | 1 + .../src/analytics/tuner/time/mod.rs | 4 + crates/moon-ui-gpui/src/chrome/quiet.rs | 9 +- .../src/chrome/terminal_chrome.rs | 6 + crates/moon-ui-gpui/src/controls/toolbar.rs | 20 +- crates/moon-ui-gpui/src/design.rs | 16 + .../src/panels/assets/settings.rs | 6 +- .../moon-ui-gpui/src/panels/assets/table.rs | 3 + .../moon-ui-gpui/src/panels/assets/wallets.rs | 16 +- crates/moon-ui-gpui/src/panels/common.rs | 4 +- .../src/panels/core_status/config_popup.rs | 2 +- .../src/panels/core_status/interactions.rs | 4 + .../src/panels/core_status/problems.rs | 1 + crates/moon-ui-gpui/src/panels/detects/mod.rs | 1 + crates/moon-ui-gpui/src/panels/news/mod.rs | 5 +- .../src/panels/order_edit/body.rs | 48 ++- crates/moon-ui-gpui/src/settings/badges.rs | 12 +- crates/moon-ui-gpui/src/settings/common.rs | 2 + crates/moon-ui-gpui/src/settings/general.rs | 8 +- .../moon-ui-gpui/src/settings/hotkeys/tab.rs | 34 +- .../src/settings/import_preview.rs | 12 + crates/moon-ui-gpui/src/settings/render.rs | 5 +- crates/moon-ui-gpui/src/settings/security.rs | 10 + crates/moon-ui-gpui/src/settings/storage.rs | 65 ++-- crates/moon-ui-gpui/src/strategies/params.rs | 8 +- .../moon-ui-gpui/src/strategies/sections.rs | 22 +- .../moon-ui-gpui/src/strategies/settings.rs | 27 +- .../moon-ui-gpui/src/strategies/tree/mod.rs | 7 + .../src/strategies/tree/pane_cache.rs | 7 +- .../moon-ui-gpui/src/strategies/versions.rs | 4 +- crates/moon-ui-gpui/src/window/login.rs | 9 +- .../tests/theme_contract/analytics.rs | 6 +- .../tests/theme_contract/fonts.rs | 339 ++++++++++++++++++ .../moon-ui-gpui/tests/theme_contract/main.rs | 1 + .../tests/theme_contract/shell.rs | 17 +- 50 files changed, 867 insertions(+), 206 deletions(-) create mode 100644 crates/moon-ui-gpui/tests/theme_contract/fonts.rs diff --git a/crates/moon-ui-gpui/src/analytics/calendar/mod.rs b/crates/moon-ui-gpui/src/analytics/calendar/mod.rs index 6af21df7..bc6d6012 100644 --- a/crates/moon-ui-gpui/src/analytics/calendar/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/calendar/mod.rs @@ -611,6 +611,8 @@ impl AnalyticsView { // Plain buttons preserve the navigation actions' independent disabled states. Keep the // divider inside their group so it cannot wrap onto a line by itself. let nav = design::chrome_section(cx) + // Prev/next and the current-period readout are navigation chrome, not data. + .font_family(design::ui_font()) .child(design::chrome_divider(cx, p)) .child(nav_btn( "cal-prev", @@ -620,6 +622,11 @@ impl AnalyticsView { )) .child( div() + // The prev/next buttons around it are chrome, but this readout is the current + // PERIOD -- a date, which the rule keeps mono. The grid below it renders its + // dates mono too, and a header in the other face would disagree with the very + // cells it labels. + .font_family(design::mono()) .text_size(design::t_body(cx)) .font_weight(FontWeight::SEMIBOLD) .text_color(moon(p.text)) @@ -657,6 +664,7 @@ impl AnalyticsView { div() .min_w_0() .truncate() + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .text_color(moon(p.text)) diff --git a/crates/moon-ui-gpui/src/analytics/profit_monitor/mod.rs b/crates/moon-ui-gpui/src/analytics/profit_monitor/mod.rs index f6e778f2..a0286371 100644 --- a/crates/moon-ui-gpui/src/analytics/profit_monitor/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/profit_monitor/mod.rs @@ -1537,6 +1537,7 @@ fn auto_status( .id("profit-monitor-auto-status") .flex_none() .gap(design::ui_px(cx, 6.0)) + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(palette.text_muted)) .tooltip(crate::panels::common::text_tooltip(label.clone())) diff --git a/crates/moon-ui-gpui/src/analytics/profit_monitor/settings.rs b/crates/moon-ui-gpui/src/analytics/profit_monitor/settings.rs index 353b5190..a23bf1ba 100644 --- a/crates/moon-ui-gpui/src/analytics/profit_monitor/settings.rs +++ b/crates/moon-ui-gpui/src/analytics/profit_monitor/settings.rs @@ -447,9 +447,12 @@ fn settings_content( cx: &App, ) -> AnyElement { // Chrome belongs to MoonPopover; a second surface here would double the popup's background. + // Every title, caption, and checkbox label here is prose, so the popup flips to the UI face + // on its own root rather than inheriting the monitor root's mono. v_flex() .id("profit-monitor-settings-popup") .w_full() + .font_family(design::ui_font()) .gap(design::ui_px(cx, 8.0)) .child( h_flex() diff --git a/crates/moon-ui-gpui/src/analytics/profit_monitor/table.rs b/crates/moon-ui-gpui/src/analytics/profit_monitor/table.rs index dea56ec1..b10956d8 100644 --- a/crates/moon-ui-gpui/src/analytics/profit_monitor/table.rs +++ b/crates/moon-ui-gpui/src/analytics/profit_monitor/table.rs @@ -61,6 +61,7 @@ pub(super) fn centered_message(message: String, palette: MoonPalette, cx: &App) .flex() .items_center() .justify_center() + .font_family(design::ui_font()) .text_color(moon(palette.text_muted)) .text_size(design::t_body(cx)) .child(message) @@ -84,6 +85,7 @@ pub(super) fn centered_alert(title: String, detail: String, cx: &App) -> AnyElem .items_center() .justify_center() .px(design::ui_px(cx, 20.0)) + .font_family(design::ui_font()) .child(MoonAlert::error("profit-monitor-error", detail).title(title)) .into_any_element() } @@ -151,12 +153,14 @@ pub(super) fn split_body( .text_align(TextAlign::Center) .child( div() + .font_family(design::ui_font()) .text_color(moon(palette.text)) .child(t!("profit_monitor.split_title").to_string()), ) .child( div() .max_w(design::ui_px(cx, 560.0)) + .font_family(design::ui_font()) .text_color(moon(palette.text_muted)) .child(t!("profit_monitor.split_detail").to_string()), ) diff --git a/crates/moon-ui-gpui/src/analytics/render.rs b/crates/moon-ui-gpui/src/analytics/render.rs index 691e3e08..80246361 100644 --- a/crates/moon-ui-gpui/src/analytics/render.rs +++ b/crates/moon-ui-gpui/src/analytics/render.rs @@ -168,6 +168,7 @@ impl Render for AnalyticsView { .bg(moon(p.panel_high)) .border_1() .border_color(moon(p.border)) + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .text_color(moon(p.text_soft)) .child(t!("common.loading").to_string()), diff --git a/crates/moon-ui-gpui/src/analytics/summary/mod.rs b/crates/moon-ui-gpui/src/analytics/summary/mod.rs index d0ba7a3b..6c8beac6 100644 --- a/crates/moon-ui-gpui/src/analytics/summary/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/summary/mod.rs @@ -679,6 +679,7 @@ fn kpi( .border_color(moon(p.border)) .child( div() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_soft)) .child(label.to_string()), @@ -721,6 +722,7 @@ fn chart_card_ex( .flex_1() .min_w_0() .truncate() + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .child(title), @@ -744,6 +746,7 @@ fn chart_card_ex( .child(head) .child( div() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .mb(design::ui_px(cx, 6.0)) @@ -874,6 +877,7 @@ fn top_card( div() .px(design::ui_px(cx, 12.0)) .py(design::ui_px(cx, 8.0)) + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .child(title), @@ -1153,6 +1157,7 @@ fn insights_card(d: &Summary, p: MoonPalette, cx: &Context) -> im div() .px(design::ui_px(cx, 12.0)) .py(design::ui_px(cx, 8.0)) + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .child(t!("analytics.insights").to_string()), diff --git a/crates/moon-ui-gpui/src/analytics/toolbar.rs b/crates/moon-ui-gpui/src/analytics/toolbar.rs index 037490d4..08cbe54d 100644 --- a/crates/moon-ui-gpui/src/analytics/toolbar.rs +++ b/crates/moon-ui-gpui/src/analytics/toolbar.rs @@ -452,74 +452,79 @@ impl AnalyticsView { } // MoonButton's custom size has no horizontal padding, so give each localized title // measured breathing room while retaining a useful click target for short labels. - let tab_width = (design::ui_text_width(cx, &title, 10.5, 400.0, true) + let tab_width = (design::ui_text_width(cx, &title, 10.5, 400.0, false) + design::ui_value(cx, 20.0)) .max(design::ui_value(cx, 72.0)); + // Wrapped in its own font_family: the tab title is a label, while the toolbar row it + // sits in (the analytics root stays mono) later carries the core NAME, which is data. row = row.child( - MoonButton::new(t.id()) - .variant(if on { - MoonButtonVariant::Blue - } else { - MoonButtonVariant::Ghost - }) - .size(MoonButtonSize::Custom { - height: 24.0, - radius: design::R_BUTTON_BASE, - font_size: 10.5, - line_height: 13.0, - gap: 5.0, - }) - .width(tab_width) - .selected(on) - .label(title) - .on_click(cx.listener(move |this, _, window, cx| { - if this.tab != t { - this.tab = t; - this.backend.update(cx, |b, _| { - b.ui_session.analytics.tab = t; - }); - // Each tab remembers its OWN time window: re-sync the - // period bar and the "from"/"to" fields to the active tab. - this.sync_period_pickers(window, cx); - // The new tab's time window differs from the one `data` - // was built for → reload, or the strategy list and the - // summary would show another tab's period. reload() also - // pulls the active tab's secondary data (tuner/profile). - let period_changed = match t { - Tab::Summary => this.active_period() != this.data_period, - Tab::Strategies => { - this.active_period() != this.strategy_data_period - } - Tab::Calendar => false, - }; - let base_dirty = match t { - Tab::Summary => this.data_dirty, - Tab::Strategies => this.strategy_dirty, - Tab::Calendar => false, - }; - if period_changed { - this.reload(cx); - } else if matches!(t, Tab::Summary | Tab::Strategies) && base_dirty { - // A hidden base view can lag a generation while Calendar alone - // refreshes. Catch it up on entry without destructive scope - // invalidation, which would erase tuner drafts. - this.request_report_refresh(RefreshUrgency::User, true, cx); - } else { - // Tab-entry catch-up uses the report gate so it cannot overlap - // an automatic full-period scan already in flight. - if t == Tab::Strategies { - this.request_axis_if_stale(this.strat_mode, cx); - } - if t == Tab::Calendar - && (this.cal_days.data().is_none() || this.cal_dirty) + div().font_family(design::ui_font()).child( + MoonButton::new(t.id()) + .variant(if on { + MoonButtonVariant::Blue + } else { + MoonButtonVariant::Ghost + }) + .size(MoonButtonSize::Custom { + height: 24.0, + radius: design::R_BUTTON_BASE, + font_size: 10.5, + line_height: 13.0, + gap: 5.0, + }) + .width(tab_width) + .selected(on) + .label(title) + .on_click(cx.listener(move |this, _, window, cx| { + if this.tab != t { + this.tab = t; + this.backend.update(cx, |b, _| { + b.ui_session.analytics.tab = t; + }); + // Each tab remembers its OWN time window: re-sync the + // period bar and the "from"/"to" fields to the active tab. + this.sync_period_pickers(window, cx); + // The new tab's time window differs from the one `data` + // was built for → reload, or the strategy list and the + // summary would show another tab's period. reload() also + // pulls the active tab's secondary data (tuner/profile). + let period_changed = match t { + Tab::Summary => this.active_period() != this.data_period, + Tab::Strategies => { + this.active_period() != this.strategy_data_period + } + Tab::Calendar => false, + }; + let base_dirty = match t { + Tab::Summary => this.data_dirty, + Tab::Strategies => this.strategy_dirty, + Tab::Calendar => false, + }; + if period_changed { + this.reload(cx); + } else if matches!(t, Tab::Summary | Tab::Strategies) && base_dirty { + // A hidden base view can lag a generation while Calendar alone + // refreshes. Catch it up on entry without destructive scope + // invalidation, which would erase tuner drafts. this.request_report_refresh(RefreshUrgency::User, true, cx); + } else { + // Tab-entry catch-up uses the report gate so it cannot overlap + // an automatic full-period scan already in flight. + if t == Tab::Strategies { + this.request_axis_if_stale(this.strat_mode, cx); + } + if t == Tab::Calendar + && (this.cal_days.data().is_none() || this.cal_dirty) + { + this.request_report_refresh(RefreshUrgency::User, true, cx); + } } + cx.notify(); } - cx.notify(); - } - })) - .render(), + })) + .render(), + ), ); } // Keep the selector widths and their internal gaps together. One additional gap belongs to @@ -863,6 +868,7 @@ impl AnalyticsView { .whitespace_nowrap() .child( div() + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .text_color(moon(p.text_soft)) .child(lbl.to_string()), @@ -1225,6 +1231,7 @@ impl AnalyticsView { .child( div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(custom_label.clone()), @@ -1241,14 +1248,14 @@ impl AnalyticsView { // Each `date_field` draws its caption at `design::t_body(cx)`, so measure at the same // unscaled base rather than a second guessed size. let date_captions_w = - design::ui_text_width(cx, &from_lbl, design::base_text(cx), 400.0, true) - + design::ui_text_width(cx, &to_lbl, design::base_text(cx), 400.0, true); + design::ui_text_width(cx, &from_lbl, design::base_text(cx), 400.0, false) + + design::ui_text_width(cx, &to_lbl, design::base_text(cx), 400.0, false); // `date_field`'s own `h_flex().gap_1()` between its caption and picker — GPUI's // `rems(0.25)`, at the window's rem size, which this app never overrides from GPUI's // default `px(16.)`. One gap per field, not scaled by the Font slider. let date_field_gaps_w = f32::from(rems(0.25).to_pixels(px(16.0))) * 2.0; let custom_group_w = 1.0 - + design::ui_text_width(cx, &custom_label, 10.5, 400.0, true) + + design::ui_text_width(cx, &custom_label, 10.5, 400.0, false) + field_w * 2.0 + date_captions_w + date_field_gaps_w diff --git a/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs b/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs index 1ca7cc3f..76102e02 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs @@ -136,21 +136,25 @@ impl AnalyticsView { let chip = |id: &'static str, f: CoinFilter, label: String| { let on = cur == f; let enabled = self.coins.filter_available(f); - MoonButton::new(id) - .variant(if on { - MoonButtonVariant::Amber - } else { - MoonButtonVariant::Soft - }) - .size(MoonButtonSize::Micro) - .selected(on) - .disabled(!enabled) - .label(label) - .on_click(cx.listener(move |this, _, _, cx| { - this.coins.filter = f; - cx.notify(); - })) - .render() + // The button carries a caption plus a count ("Blacklist 5/12"); buttons are prose + // controls regardless of the number they display. + div().font_family(design::ui_font()).child( + MoonButton::new(id) + .variant(if on { + MoonButtonVariant::Amber + } else { + MoonButtonVariant::Soft + }) + .size(MoonButtonSize::Micro) + .selected(on) + .disabled(!enabled) + .label(label) + .on_click(cx.listener(move |this, _, _, cx| { + this.coins.filter = f; + cx.notify(); + })) + .render(), + ) }; let (bl_n, wl_n) = (self.coins.work.black.len(), self.coins.work.white.len()); h_flex() @@ -376,6 +380,7 @@ impl AnalyticsView { let note = div() .w_full() .p(design::ui_px(cx, 18.0)) + .font_family(design::ui_font()) .text_center() .text_color(moon(p.text_muted)) .child(t!("analytics.strat.no_match").to_string()) @@ -447,11 +452,15 @@ impl AnalyticsView { .child( div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .child(t!("analytics.tab.coins").to_string()), ) .child( + // NOT flipped: `scope` (`scope_label()`) is sometimes the raw name of + // the single selected strategy, sometimes a caption ("all strategies" / + // "N selected") — it stays mono so the raw-name case remains legible. div() .flex_1() .min_w_0() @@ -464,6 +473,7 @@ impl AnalyticsView { el.child( div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.amber)) .child( @@ -472,25 +482,28 @@ impl AnalyticsView { ), ) .child( - MoonButton::new("an-coin-revert") - .variant(MoonButtonVariant::Soft) - .size(MoonButtonSize::Micro) - .label(t!("analytics.coins.revert").to_string()) - .on_click(cx.listener(|this, _, _, cx| { - this.coins.revert(); - this.coins.settle_filter(); - // Through the SAME debounced path as a tick: reverting - // is one more edit of the same lists. - this.arm_coin_kpi(cx); - cx.notify(); - })) - .render(), + div().font_family(design::ui_font()).child( + MoonButton::new("an-coin-revert") + .variant(MoonButtonVariant::Soft) + .size(MoonButtonSize::Micro) + .label(t!("analytics.coins.revert").to_string()) + .on_click(cx.listener(|this, _, _, cx| { + this.coins.revert(); + this.coins.settle_filter(); + // Through the SAME debounced path as a tick: reverting + // is one more edit of the same lists. + this.arm_coin_kpi(cx); + cx.notify(); + })) + .render(), + ), ) }) .when(total > shown, |el| { el.child( div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child( diff --git a/crates/moon-ui-gpui/src/analytics/tuner/coins/picker.rs b/crates/moon-ui-gpui/src/analytics/tuner/coins/picker.rs index d975a54f..e9ad2e22 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/coins/picker.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/coins/picker.rs @@ -437,6 +437,7 @@ impl AnalyticsView { ( div() .w_full() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(if picked_any { diff --git a/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs b/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs index e77ee6a4..c1a97ce1 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs @@ -755,6 +755,7 @@ impl AnalyticsView { .flex_1() .min_w_0() .truncate() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon_alpha(p.text_muted, 0.7)) .child(t!("analytics.tuner.no_param").to_string()) @@ -868,6 +869,7 @@ impl AnalyticsView { .w(design::font_w_px(cx, 74.0)) .flex_none() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child(caption), ) @@ -953,6 +955,7 @@ impl AnalyticsView { .w(design::font_w_px(cx, 74.0)) .flex_none() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child(t!("analytics.tuner.compose_result").to_string()), ) @@ -961,6 +964,7 @@ impl AnalyticsView { .flex_1() .min_w_0() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_soft)) .child(decision.to_string()), ), @@ -989,6 +993,7 @@ impl AnalyticsView { .w(design::font_w_px(cx, 74.0)) .flex_none() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child(t!("analytics.tuner.compose_set").to_string()), ) @@ -1032,6 +1037,7 @@ impl AnalyticsView { .w(design::font_w_px(cx, 74.0)) .flex_none() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child( t!("analytics.tuner.compose_rejected") @@ -1081,6 +1087,7 @@ impl AnalyticsView { .w(design::font_w_px(cx, 74.0)) .flex_none() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child(t!("analytics.tuner.compose_result").to_string()), ) @@ -1089,6 +1096,7 @@ impl AnalyticsView { .flex_1() .min_w_0() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.text_soft)) .child( t!("analytics.tuner.compose_decision_all_direct") @@ -1100,6 +1108,7 @@ impl AnalyticsView { div() .w_full() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.orange)) .child(text.to_string()), ) @@ -1131,6 +1140,7 @@ impl AnalyticsView { div() .w_full() .truncate() + .font_family(design::ui_font()) .text_color(moon(p.orange)) .child(t!("analytics.tuner.compose_no_split").to_string()), ) @@ -1175,7 +1185,12 @@ impl AnalyticsView { .border_t_1() .border_color(moon_alpha(p.border, 0.7)) .text_size(design::t_caption(cx)) - .child(div().text_color(moon(p.text_soft)).child(label)); + .child( + div() + .font_family(design::ui_font()) + .text_color(moon(p.text_soft)) + .child(label), + ); if strat.found && !(sub && class != FieldClass::BvSv) { let (flag, cur_ignore) = flag_of(class, strat); let staged = self.tuner.staged_ignore.get(flag).copied(); diff --git a/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs b/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs index f014400e..34ae03f0 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs @@ -438,20 +438,22 @@ impl AnalyticsView { .child(self.strat_type_menu(cx)) .child(self.strat_lists_menu(cx)) .child( - MoonCheckbox::new("an-strat-active") - .checked(self.strat_active_only) - .size(MoonCheckboxSize::Compact) - .label(t!("analytics.strat.active_only").to_string()) - .on_change({ - let view = cx.entity(); - move |ch: &bool, _w, app| { - let on = *ch; - view.update(app, |this, cx| { - this.strat_active_only = on; - cx.notify(); - }); - } - }), + div().font_family(design::ui_font()).child( + MoonCheckbox::new("an-strat-active") + .checked(self.strat_active_only) + .size(MoonCheckboxSize::Compact) + .label(t!("analytics.strat.active_only").to_string()) + .on_change({ + let view = cx.entity(); + move |ch: &bool, _w, app| { + let on = *ch; + view.update(app, |this, cx| { + this.strat_active_only = on; + cx.notify(); + }); + } + }), + ), ) .child(self.strat_column_menu(cx)) .into_any_element() diff --git a/crates/moon-ui-gpui/src/analytics/tuner/list/table.rs b/crates/moon-ui-gpui/src/analytics/tuner/list/table.rs index 30032cd9..f88a723c 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/list/table.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/list/table.rs @@ -102,6 +102,7 @@ impl AnalyticsView { let note = div() .w_full() .p(design::ui_px(cx, 18.0)) + .font_family(design::ui_font()) .text_center() .text_color(moon(p.text_muted)) .child(t!("analytics.strat.no_match").to_string()); @@ -192,6 +193,9 @@ impl AnalyticsView { .py(design::ui_px(cx, 8.0)) .items_center() .gap(design::ui_px(cx, 8.0)) + // Title, mode buttons and the trailing counter are all prose — no data value + // sits in this row. + .font_family(design::ui_font()) .child( div() .flex_none() diff --git a/crates/moon-ui-gpui/src/analytics/tuner/save.rs b/crates/moon-ui-gpui/src/analytics/tuner/save.rs index a2e09443..4eaf8e3b 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/save.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/save.rs @@ -819,6 +819,7 @@ impl AnalyticsView { .child( div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(t!("analytics.tuner.copy_name_lbl").to_string()), @@ -892,6 +893,7 @@ impl AnalyticsView { div() .flex_1() .min_w_0() + .font_family(design::ui_font()) .text_color(moon(p.amber)) .child(note), ); @@ -938,6 +940,7 @@ impl AnalyticsView { .border_t_1() .border_color(moon_alpha(p.border, 0.5)) .text_size(design::t_caption(cx)) + .font_family(design::ui_font()) .text_color(moon(p.orange)) .child(w.clone()), ); @@ -973,6 +976,10 @@ impl AnalyticsView { .items_center() .gap(design::ui_px(cx, 8.0)) .child( + // NOT flipped to `ui_font()`: `title` always interpolates a + // raw target name or a strategy count into the caption text + // (`copy_title`/`save_title`/`save_title_bulk`), so this box + // would sometimes render a VALUE. Left mono. div() .flex_1() .min_w_0() @@ -985,6 +992,7 @@ impl AnalyticsView { .children((!bulk).then(|| { div() .flex_none() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(t!("analytics.tuner.save_now_next").to_string()) @@ -1008,6 +1016,7 @@ impl AnalyticsView { .justify_end() .border_t_1() .border_color(moon_alpha(p.border, 0.6)) + .font_family(design::ui_font()) .child( MoonButton::new("an-save-no") .variant(MoonButtonVariant::Ghost) diff --git a/crates/moon-ui-gpui/src/analytics/tuner/shared.rs b/crates/moon-ui-gpui/src/analytics/tuner/shared.rs index 96868863..600e2a57 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/shared.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/shared.rs @@ -103,6 +103,9 @@ pub(super) fn card( cx: &Context, ) -> AnyElement { // One clipping box keeps the accessory outside the width that may yield. + // NOT flipped to `ui_font()`: callers interpolate a raw strategy name, a field name, or + // `scope_label()`'s bare name arm into `title`/`sub` alongside the caption text, so this + // box would sometimes render a VALUE. It stays mono for that raw-name case. let mut text = h_flex() .flex_1() .min_w_0() diff --git a/crates/moon-ui-gpui/src/analytics/tuner/shell.rs b/crates/moon-ui-gpui/src/analytics/tuner/shell.rs index a365f432..bfdfce7b 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/shell.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/shell.rs @@ -125,6 +125,9 @@ impl AnalyticsView { .py(design::ui_px(cx, 8.0)) .items_center() .gap(design::ui_px(cx, 6.0)) + // Title, round-label and the copy/save buttons are all prose; this toolbar carries + // no data value of its own. + .font_family(design::ui_font()) // Let the title yield before the hosting card clips the trailing controls. .child( div() @@ -257,6 +260,10 @@ impl AnalyticsView { } /// The frame every suggestion row shares: pinned above the scrollable rows, caption-sized. + /// + /// The caption, status text and buttons it hosts are prose, so the frame carries + /// `ui_font()` for the whole row; the restart/min-trades input boxes are parameter values + /// and force `mono()` back on themselves individually. fn config_row_frame(&self, cx: &Context) -> Div { h_flex() .w_full() @@ -267,6 +274,7 @@ impl AnalyticsView { .items_center() .gap(design::ui_px(cx, 6.0)) .text_size(design::t_caption(cx)) + .font_family(design::ui_font()) } /// The "By filter" suggestion row: restarts, the settings gear, live status, Stop, Search. @@ -298,11 +306,15 @@ impl AnalyticsView { self.config_row_frame(cx) .child(cfg_label(t!("analytics.tuner.iters").to_string(), p)) .child( - div().w(design::font_w_px(cx, 46.0)).flex_none().child( - MoonInput::new(SharedString::from("tun-cfg-it-f")) - .state(&it_input) - .small(), - ), + div() + .w(design::font_w_px(cx, 46.0)) + .flex_none() + .font_family(design::mono()) + .child( + MoonInput::new(SharedString::from("tun-cfg-it-f")) + .state(&it_input) + .small(), + ), ) .child(settings) // The status takes the free space and truncates, so a long failure message cannot @@ -643,6 +655,9 @@ impl AnalyticsView { .overflow_y_scroll() .gap(gap) .text_size(design::t_caption(cx)) + // Title, section headers, row captions and buttons are prose; the min-trades/seed + // input boxes and the last-seed value force `mono()` back on themselves below. + .font_family(design::ui_font()) .child(head) .child(section_title( t!("analytics.tuner.cfg_search_section").to_string(), @@ -652,6 +667,7 @@ impl AnalyticsView { div() .w(design::font_w_px(cx, 76.0)) .flex_none() + .font_family(design::mono()) .child( MoonInput::new(SharedString::from("tun-cfg-mn-f")) .state(&mn_input) @@ -756,6 +772,7 @@ impl AnalyticsView { div() .w(design::font_w_px(cx, 126.0)) .flex_none() + .font_family(design::mono()) .child( MoonInput::new(SharedString::from("tun-cfg-seed-f")) .state(&seed_input) @@ -778,6 +795,7 @@ impl AnalyticsView { .flex_1() .min_w_0() .truncate() + .font_family(design::mono()) .text_color(moon(p.text_soft)) .tooltip(move |_w, cx| { cx.new(|_| MoonTooltipView::new(full.clone())).into() @@ -816,11 +834,15 @@ impl AnalyticsView { self.config_row_frame(cx) .child(cfg_label(t!("analytics.tuner.min_trades").to_string(), p)) .child( - div().w(design::font_w_px(cx, 52.0)).flex_none().child( - MoonInput::new(SharedString::from("tun-cfg-mn-t")) - .state(&mn_input) - .small(), - ), + div() + .w(design::font_w_px(cx, 52.0)) + .flex_none() + .font_family(design::mono()) + .child( + MoonInput::new(SharedString::from("tun-cfg-mn-t")) + .state(&mn_input) + .small(), + ), ) .child(div().flex_1()) .child( diff --git a/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs b/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs index b782ae93..ddfa2980 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs @@ -303,6 +303,7 @@ impl AnalyticsView { .flex_none() .px(design::ui_px(cx, 12.0)) .py(design::ui_px(cx, 3.0)) + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_soft)) .child(t!("analytics.tuner.time.core_clock_note").to_string()) diff --git a/crates/moon-ui-gpui/src/analytics/tuner/time/mod.rs b/crates/moon-ui-gpui/src/analytics/tuner/time/mod.rs index 05285de3..456af4ad 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/time/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/time/mod.rs @@ -282,6 +282,7 @@ impl AnalyticsView { .size_full() .items_center() .justify_center() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(t!("analytics.time.no_data").to_string()) @@ -316,12 +317,15 @@ impl AnalyticsView { .gap(design::ui_px(cx, 8.0)) .child( div() + .font_family(design::ui_font()) .text_size(design::t_title(cx)) .font_weight(FontWeight::SEMIBOLD) .child(t!("analytics.time.by_hour").to_string()), ) .child(div().flex_1()) .child( + // NOT flipped: `scope` (`scope_label()`) is sometimes a raw strategy + // name, which stays mono for legibility. div() .min_w_0() .truncate() diff --git a/crates/moon-ui-gpui/src/chrome/quiet.rs b/crates/moon-ui-gpui/src/chrome/quiet.rs index ab352830..10c38b7a 100644 --- a/crates/moon-ui-gpui/src/chrome/quiet.rs +++ b/crates/moon-ui-gpui/src/chrome/quiet.rs @@ -53,7 +53,10 @@ fn toggle_label() -> String { /// Returns: /// Toggle (label + gap + track), the chrome gap, and the square gear button. pub(crate) fn header_quiet_width(cx: &App) -> f32 { - let label = design::mono_caption_text_width(cx, &toggle_label(), TOGGLE_LABEL_WEIGHT); + // The UI family, matching the caption's own `font_family` below: the toggle label is a control + // caption. `shell::ticker` offsets its popup BY this width, so a measurement in the other + // family would move the popup as well as the cluster. + let label = design::ui_caption_text_width(cx, &toggle_label(), TOGGLE_LABEL_WEIGHT); label + design::ui_value(cx, TOGGLE_GAP) + design::ui_value(cx, TOGGLE_TRACK_W) @@ -106,7 +109,9 @@ pub(crate) fn header_quiet_cluster( .child( div() .flex_none() - .font_family(design::mono()) + // A control caption, measured in the same family by + // `header_quiet_width` above. + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(rgb(design::chrome_toggle_label_color(p, sleeping, true))) .child(toggle_label()), diff --git a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs index a87f9a1d..e2145e10 100644 --- a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs +++ b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs @@ -517,6 +517,12 @@ fn workspace_mode_selector(group: &str, backend: &Entity, cx: &App) -> ); div() .id("header-workspace-mode-tip") + // No family is set here on purpose. The mode name IS a caption and the rule would move it + // to the UI face, but `MoonDropdown` forces its trigger mono -- `DROPDOWN_TRIGGER_MONO` + // (MoonUI `moon/dropdown.rs:27`) reaches both the render (`dropdown/trigger.rs:494`) and + // the fitted-label measurement (`:471`, `:516`), with no builder override. Setting the + // family on this wrapper could not reach the trigger, so it would leave a font call that + // does nothing behind a comment claiming otherwise. .tooltip(crate::panels::common::text_tooltip(tooltip)) .child( MoonDropdown::new("header-workspace-mode") diff --git a/crates/moon-ui-gpui/src/controls/toolbar.rs b/crates/moon-ui-gpui/src/controls/toolbar.rs index 57366dd2..39bfddab 100644 --- a/crates/moon-ui-gpui/src/controls/toolbar.rs +++ b/crates/moon-ui-gpui/src/controls/toolbar.rs @@ -151,8 +151,11 @@ const SIZE_UNIT: &str = "USDT eq."; /// Measure one complete localized launcher button at ToolbarCompact geometry. /// -/// The Shell root supplies the monospaced family inherited by the text segment. MoonUI gives this -/// size zero native padding so icon-only targets stay square; labeled launchers add +/// The label is measured AND rendered in [`design::ui_font`]: it is a control caption, and the two +/// sections that host labeled launchers set that family so `MoonButton`'s text segment inherits it +/// (the segment pins a family only when its own `mono` flag is set). Measuring in the other family +/// would budget the whole trailing cluster wrongly at every `row_fit` shedding threshold. MoonUI +/// gives this size zero native padding so icon-only targets stay square; labeled launchers add /// [`TOOLBAR_LAUNCHER_PAD_X`] on each side via `MoonButton::padding_x`. The reserved width is the /// leading icon, its UI-scaled gap, both insets, and the two border pixels. The button is never /// allowed to become narrower than its stable icon-only target. @@ -164,12 +167,16 @@ const SIZE_UNIT: &str = "USDT eq."; /// Returns: /// Full icon-plus-label width in logical pixels. fn launcher_label_width(cx: &App, label: &str) -> f32 { + // The UI family, not the monospaced one: a launcher label is a control caption and its + // buttons render it in `design::ui_font()` (the two sections that host them set it). This + // measurement sizes the button the label is drawn in, so the two must name the same family -- + // measure mono, draw proportional, and the whole trailing cluster is budgeted too wide. let text = design::ui_text_width( cx, label, TOOLBAR_LAUNCHER_TEXT_SIZE, TOOLBAR_LAUNCHER_TEXT_WEIGHT, - true, + false, ); let icon = (design::font_value(cx, TOOLBAR_LAUNCHER_ICON_FONT_SIZE) + 1.0).clamp(10.0, 14.0); let chrome = icon @@ -1166,6 +1173,11 @@ pub fn toolbar( .child(design::chrome_divider(cx, p)) .child( section() + // Launcher captions are control captions, so they read in the UI face. It is set + // on the section rather than per button because `MoonButton` can only force + // MONO on its own segments -- it has no proportional prop, and inherits + // otherwise. Paired with `launcher_label_width`, which measures the same family. + .font_family(design::ui_font()) .child(open_window_button( "toolbar-strategies", strategies_label, @@ -1197,6 +1209,8 @@ pub fn toolbar( // the user is still typing into is not a configured core. div() .relative() + // The UI face, as for the other labeled launchers above. + .font_family(design::ui_font()) .child(open_window_button( "toolbar-settings", settings_label, diff --git a/crates/moon-ui-gpui/src/design.rs b/crates/moon-ui-gpui/src/design.rs index 85c0a3b9..b6f46dfe 100644 --- a/crates/moon-ui-gpui/src/design.rs +++ b/crates/moon-ui-gpui/src/design.rs @@ -827,6 +827,22 @@ pub fn mono_title_text_width(cx: &App, text: &str, weight: f32) -> f32 { ui_text_width(cx, text, base_text(cx) + 3.0, weight, true) } +/// Width of UI-face text drawn at the terminal's body size — the [`ui_font`] partner of +/// [`mono_body_text_width`], filling in the same body base. +/// +/// A layout that sizes itself from a measured label must measure in the family that label is +/// RENDERED in: prose reads in [`ui_font`], so measuring it with the mono trio above overstates +/// every proportional string and drifts the column it sizes. +pub fn ui_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { + ui_text_width(cx, text, base_text(cx), weight, false) +} + +/// Width of UI-face text drawn at the terminal's caption size — the [`t_caption`] partner of +/// [`ui_body_text_width`], and the [`ui_font`] partner of [`mono_caption_text_width`]. +pub fn ui_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { + ui_text_width(cx, text, base_text(cx) - 2.0, weight, false) +} + /// Cache key for one glyph advance under an exact resolved font and requested weight. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct MonoGlyphKey { diff --git a/crates/moon-ui-gpui/src/panels/assets/settings.rs b/crates/moon-ui-gpui/src/panels/assets/settings.rs index d3fb62cf..cc24c4c9 100644 --- a/crates/moon-ui-gpui/src/panels/assets/settings.rs +++ b/crates/moon-ui-gpui/src/panels/assets/settings.rs @@ -152,8 +152,11 @@ fn settings_content_width(cx: &App) -> f32 { &t!(SETTINGS_TITLE), tokens.typography.mono_font_size, 400.0, - true, + false, ); + // `MoonGroupBox::title()` hardcodes `MoonText::new(..).mono(true)` in MoonUI (group_box.rs) + // with no builder override — this caption stays mono no matter what the popup root does, so + // the measurement stays mono too. let group_width = design::ui_text_width( cx, &t!(DISPLAY_GROUP), @@ -196,6 +199,7 @@ fn settings_content( v_flex() .id("assets-wallets-settings-popup") .w_full() + .font_family(design::ui_font()) .gap(design::ui_px(cx, 8.0)) .child( h_flex() diff --git a/crates/moon-ui-gpui/src/panels/assets/table.rs b/crates/moon-ui-gpui/src/panels/assets/table.rs index 379fa2ee..a143817c 100644 --- a/crates/moon-ui-gpui/src/panels/assets/table.rs +++ b/crates/moon-ui-gpui/src/panels/assets/table.rs @@ -1403,6 +1403,9 @@ fn open_market_sell_confirm( let p = MoonPalette::active(cx); content.child( div() + // MIXED NODE: `assets.market_sell_q` welds the COIN TICKER into the + // question, and a ticker must read the same here as in the table the + // user just came from. Half a node cannot be styled, so the value wins. .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(p.text)) diff --git a/crates/moon-ui-gpui/src/panels/assets/wallets.rs b/crates/moon-ui-gpui/src/panels/assets/wallets.rs index 94d33819..f70b6e3f 100644 --- a/crates/moon-ui-gpui/src/panels/assets/wallets.rs +++ b/crates/moon-ui-gpui/src/panels/assets/wallets.rs @@ -353,21 +353,33 @@ impl AssetsView { let input = content_view.read(cx).transfer_input.clone(); let mut body = v_flex() .gap(design::ui_px(cx, 10.0)) - .font_family(design::mono()) + .font_family(design::ui_font()) .child( + // MIXED NODE: `assets.transfer_title` combines the coin ticker and + // the from/to wallet names with the sentence in one text node + // (locales/assets.yml:253-256) — stays mono. div() + .font_family(design::mono()) .font_weight(FontWeight::SEMIBOLD) .text_color(rgb(p.text)) .child(title.clone()), ) .child( + // MIXED NODE: `assets.free` combines the label with the figure in one + // text node (locales/assets.yml:257-260) — stays mono. div() + .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(p.text_muted)) .child(t!("assets.free", n = num(pending.free)).to_string()), ); if let Some(input) = input { - body = body.child(MoonInput::new("transfer-amount").state(&input).small()); + body = body.child( + MoonInput::new("transfer-amount") + .state(&input) + .small() + .mono(true), + ); } content.child(body) }) diff --git a/crates/moon-ui-gpui/src/panels/common.rs b/crates/moon-ui-gpui/src/panels/common.rs index 0441ffc1..bd8f63af 100644 --- a/crates/moon-ui-gpui/src/panels/common.rs +++ b/crates/moon-ui-gpui/src/panels/common.rs @@ -563,7 +563,9 @@ pub(crate) fn data_table_host( .h(px(design::table_row_h(cx))) .flex() .items_center() - .font_family(design::mono()) + // An empty state is a sentence, not a row: it reads as prose even though it + // sits where data would. One hunk covers every table panel that hosts here. + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .text_color(rgb(p.text_muted)) .child(empty_msg), diff --git a/crates/moon-ui-gpui/src/panels/core_status/config_popup.rs b/crates/moon-ui-gpui/src/panels/core_status/config_popup.rs index 3092c03c..69e0a4a8 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/config_popup.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/config_popup.rs @@ -571,7 +571,7 @@ impl CoreStatusView { .justify_center() .text_size(design::t_caption(cx)) .text_color(rgb(p.text_muted)) - .font_family(design::mono()) + .font_family(design::ui_font()) .whitespace_nowrap() .child(caption), ) diff --git a/crates/moon-ui-gpui/src/panels/core_status/interactions.rs b/crates/moon-ui-gpui/src/panels/core_status/interactions.rs index 8f3d063a..e9730287 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/interactions.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/interactions.rs @@ -768,6 +768,8 @@ impl CoreStatusView { let p = MoonPalette::active(cx); content.child( div() + // MIXED NODE: `core_update.confirm.q` welds the core and server + // COUNTS into the question. Half a node cannot be styled. .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(p.text)) @@ -860,6 +862,8 @@ fn problem_confirm_dialog( let p = MoonPalette::active(cx); content.child( div() + // MIXED NODE: both questions that reach this dialog weld a CORE NAME into the + // sentence, and a core name is shown verbatim and identically everywhere. .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(p.text)) diff --git a/crates/moon-ui-gpui/src/panels/core_status/problems.rs b/crates/moon-ui-gpui/src/panels/core_status/problems.rs index c6170a77..bbecd7f2 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/problems.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/problems.rs @@ -385,6 +385,7 @@ fn notice( .py(design::ui_px(cx, 4.0)) .items_center() .bg(rgb(p.table_head)) + // MIXED NODE: `notice_text` welds the unknown/truncated COUNTS into the sentence. .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(rgb(p.text_muted)) diff --git a/crates/moon-ui-gpui/src/panels/detects/mod.rs b/crates/moon-ui-gpui/src/panels/detects/mod.rs index dece9eff..e3767211 100644 --- a/crates/moon-ui-gpui/src/panels/detects/mod.rs +++ b/crates/moon-ui-gpui/src/panels/detects/mod.rs @@ -722,6 +722,7 @@ impl Render for DetectsPanel { .justify_center() .px_3() .py_2() + .font_family(crate::design::ui_font()) .text_size(crate::design::t_body(cx)) .text_color(rgb(p.text_soft)) .child( diff --git a/crates/moon-ui-gpui/src/panels/news/mod.rs b/crates/moon-ui-gpui/src/panels/news/mod.rs index 98fb5f46..5430f910 100644 --- a/crates/moon-ui-gpui/src/panels/news/mod.rs +++ b/crates/moon-ui-gpui/src/panels/news/mod.rs @@ -774,7 +774,7 @@ impl NewsView { .id("news-tags-content") .w_full() .gap(design::ui_px(cx, 4.0)) - .font_family(design::mono()) + .font_family(design::ui_font()) .child(head) .child(untagged) .when(empty, |this| { @@ -899,9 +899,12 @@ impl NewsView { .py(design::ui_px(cx, 2.0)) .child(checkbox) .child( + // AMBIGUOUS: a user-defined tag identifier, not a sentence — treated as a value + // like a badge or strategy name and left mono. div() .flex_1() .min_w(px(0.0)) + .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(p.text)) .child(format!("#{label}")), diff --git a/crates/moon-ui-gpui/src/panels/order_edit/body.rs b/crates/moon-ui-gpui/src/panels/order_edit/body.rs index fb01a802..47c423fe 100644 --- a/crates/moon-ui-gpui/src/panels/order_edit/body.rs +++ b/crates/moon-ui-gpui/src/panels/order_edit/body.rs @@ -44,6 +44,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl p, t!("orders.edit.coin").to_string(), div() + .font_family(design::mono()) .text_color(moon(p.accent)) .font_weight(FontWeight::SEMIBOLD) // Coin and quote as the CORE names them, resolved by the feed; matches the @@ -63,6 +64,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl p, t!("orders.edit.side").to_string(), div() + .font_family(design::mono()) .text_color(moon(side_tone.color(p))) .font_weight(FontWeight::SEMIBOLD) .child(side_text), @@ -70,7 +72,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl .child(info_kv( p, t!("orders.edit.status").to_string(), - div().child(status), + div().font_family(design::mono()).child(status), )), ) .child( @@ -80,7 +82,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl .child(info_kv( p, t!("orders.edit.size").to_string(), - div().child(format!( + div().font_family(design::mono()).child(format!( "{} ({:.0}%)", crate::panels::num(r.size), r.fill_pct @@ -89,19 +91,22 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl .child(info_kv( p, t!("orders.edit.strategy").to_string(), - div().child(strat), + div().font_family(design::mono()).child(strat), )) .child(info_kv( p, t!("orders.edit.core").to_string(), - div().child(s.core_name.clone()), + div().font_family(design::mono()).child(s.core_name.clone()), )), ); // Show the pending entry condition as read-only protocol data when present. + // MIXED NODE: `orders.edit.cond` combines the localized condition sentence with the price + // figure in one text node (locales/orders.yml:175-178) — stays mono. let cond = r.pending_cond.map(|c| { div() .w_full() + .font_family(design::mono()) .text_color(moon(p.text_muted)) .child(t!("orders.edit.cond", p = crate::panels::num(c)).to_string()) }); @@ -125,17 +130,24 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl div().w(px(150.0)).child( MoonInput::new("oe-price-input") .state(&s.price_input) - .small(), + .small() + .mono(true), ), ) .child( - div().text_color(moon(p.text_muted)).child( - t!( - "orders.edit.current", - p = crate::panels::num(r.price as f64) - ) - .to_string(), - ), + // MIXED NODE: `orders.edit.current` combines the localized "Current:" label + // with the price figure in one text node (locales/orders.yml:187-190) — stays + // mono. + div() + .font_family(design::mono()) + .text_color(moon(p.text_muted)) + .child( + t!( + "orders.edit.current", + p = crate::panels::num(r.price as f64) + ) + .to_string(), + ), ), ); @@ -172,8 +184,14 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl } else { div().flex_1().min_w(px(70.0)) }; - host.child(MoonInput::new(id).state(input).small().disabled(!enabled)) - .into_any_element() + host.child( + MoonInput::new(id) + .state(input) + .small() + .disabled(!enabled) + .mono(true), + ) + .into_any_element() }; let fixed_label = t!("orders.edit.fixed").to_string(); @@ -265,7 +283,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl let mut body = v_flex() .w_full() .gap_2() - .font_family(design::mono()) + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .text_color(moon(p.text)) .child(info); diff --git a/crates/moon-ui-gpui/src/settings/badges.rs b/crates/moon-ui-gpui/src/settings/badges.rs index 1d35e030..3ca938aa 100644 --- a/crates/moon-ui-gpui/src/settings/badges.rs +++ b/crates/moon-ui-gpui/src/settings/badges.rs @@ -422,14 +422,16 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-ord-{idx}"))) .state(&row.ordinal) - .small(), + .small() + .mono(true), ), ) .child( div().flex_none().w(px(120.0)).child( MoonInput::new(SharedString::from(format!("badge-name-{idx}"))) .state(&row.name) - .small(), + .small() + .mono(true), ), ) .child(active_chk) @@ -442,7 +444,8 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-code-{idx}"))) .state(&row.code) - .small(), + .small() + .mono(true), ), ) .child(distinguish_chk) @@ -456,7 +459,8 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-codeshort-{idx}"))) .state(&row.code_short) - .small(), + .small() + .mono(true), ), ) }) diff --git a/crates/moon-ui-gpui/src/settings/common.rs b/crates/moon-ui-gpui/src/settings/common.rs index 3d2958c8..0ded8026 100644 --- a/crates/moon-ui-gpui/src/settings/common.rs +++ b/crates/moon-ui-gpui/src/settings/common.rs @@ -107,6 +107,7 @@ pub(super) fn slider_row( h_flex() .w_full() .justify_between() + .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(rgba_from(p.text_muted, 1.0)) .child(min) @@ -117,6 +118,7 @@ pub(super) fn slider_row( div() .w(design::font_w_px(cx, 76.0)) .flex_none() + .font_family(design::mono()) .text_align(TextAlign::Right) .text_color(rgba_from(p.text_muted, 1.0)) .child(val), diff --git a/crates/moon-ui-gpui/src/settings/general.rs b/crates/moon-ui-gpui/src/settings/general.rs index cede4930..3ca8c654 100644 --- a/crates/moon-ui-gpui/src/settings/general.rs +++ b/crates/moon-ui-gpui/src/settings/general.rs @@ -172,6 +172,7 @@ impl SettingsView { .child( div() .w(design::font_w_px(cx, 72.0)) + .font_family(design::mono()) .text_center() .text_color(color) .child(value_text), @@ -227,7 +228,8 @@ impl SettingsView { div().w(design::font_w_px(cx, 56.0)).child( MoonInput::new("ui-font-delta") .state(&self.ui_font_input) - .small(), + .small() + .mono(true), ), ) } @@ -551,6 +553,10 @@ fn font_delta_marks(cx: &App, track_w: f32) -> impl IntoElement { .top(px(tick_h + design::ui_value(cx, 1.0))) .w(px(label_w)) .text_center() + // Scale marks are figures read against each other along the slider. The + // current-value cell and the input beside them are already pinned; without + // this the marks would be the one part of the control left proportional. + .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(label) .child(m.to_string()), diff --git a/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs b/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs index f295a2cd..d817192b 100644 --- a/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs +++ b/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs @@ -66,7 +66,7 @@ impl SettingsView { .child( MoonText::new(t!("hotkeys.group.builtin").to_string()) .uppercase(false) - .mono(true) + .mono(false) .font_size(11.0) .line_height(14.0) .color(p.text) @@ -75,7 +75,7 @@ impl SettingsView { .child( MoonText::new(t!("hotkeys.group.builtin_hint").to_string()) .uppercase(false) - .mono(true) + .mono(false) .wrap() .line_height(12.0) .color(p.text_muted) @@ -121,7 +121,7 @@ impl SettingsView { .child( MoonText::new(self.hotkeys_group.hint()) .uppercase(false) - .mono(true) + .mono(false) .wrap() .line_height(12.0) .color(p.text_muted) @@ -488,7 +488,7 @@ impl SettingsView { .child( MoonText::new(line.into()) .uppercase(false) - .mono(true) + .mono(false) .wrap() .font_size(11.0) .line_height(14.0) @@ -518,6 +518,13 @@ impl SettingsView { cx: &Context, ) -> AnyElement { let p = MoonPalette::active(cx); + // Most rows title themselves with a localized phrase, but the preset slots title + // themselves with their own IDENTITY -- `F3`, `S2` -- which is a value, and `core_pull_row` + // pins that same string mono. Read it off the slot the row already carries rather than + // asking every call site to declare it: the two that pass an identity are exactly the two + // preset variants. + let title_is_identity = + matches!(slot, HotkeySlot::OrderSize(_) | HotkeySlot::SellPreset(_)); let raw = slot_value(hotkeys, slot); let parsed = parse_hotkey(raw); let invalid = !raw.trim().is_empty() && parsed.is_none(); @@ -544,7 +551,7 @@ impl SettingsView { .child( MoonText::new(title.into()) .uppercase(false) - .mono(true) + .mono(title_is_identity) .wrap() .font_size(11.0) .line_height(14.0) @@ -561,7 +568,7 @@ impl SettingsView { .child( MoonText::new(desc.into()) .uppercase(false) - .mono(true) + .mono(false) .wrap() .font_size(11.0) .line_height(14.0) @@ -700,7 +707,7 @@ impl SettingsView { let p = MoonPalette::active(cx); MoonText::new(t!("hotkeys.move_kind.title").to_string()) .uppercase(false) - .mono(true) + .mono(false) .font_size(9.0) .line_height(12.0) .color(p.text_muted) @@ -752,7 +759,7 @@ impl SettingsView { .child( MoonText::new(title) .uppercase(false) - .mono(true) + .mono(false) .wrap() .font_size(11.0) .line_height(14.0) @@ -769,7 +776,7 @@ impl SettingsView { .child( MoonText::new(desc) .uppercase(false) - .mono(true) + .mono(false) .wrap() .font_size(11.0) .line_height(14.0) @@ -881,7 +888,7 @@ impl SettingsView { fn wip_tag(&self, p: &MoonPalette, _cx: &Context) -> AnyElement { MoonText::new(t!("hotkeys.todo").to_string()) .uppercase(false) - .mono(true) + .mono(false) .line_height(12.0) .color(p.amber) .render() @@ -1036,6 +1043,11 @@ impl SettingsView { div() .flex_none() .w(design::ui_px(cx, 96.0)) + // A slot identity (F1, S1) is a value, and the key columns it is compared + // against on this same row -- MoonHotkeyInput, the arrow, MoonKbd -- all stay + // mono. Without this pin it would be the only column of the comparison that + // changed face. + .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(rgba_from(p.text, 1.0)) .child(slot_label(row.slot)), @@ -1090,7 +1102,7 @@ impl SettingsView { .into_any_element(), MoonText::new(t!("hotkeys.pull.title").to_string()) .uppercase(false) - .mono(true) + .mono(false) .font_size(11.0) .line_height(14.0) .color(p.text) diff --git a/crates/moon-ui-gpui/src/settings/import_preview.rs b/crates/moon-ui-gpui/src/settings/import_preview.rs index e5f93b14..270dd86f 100644 --- a/crates/moon-ui-gpui/src/settings/import_preview.rs +++ b/crates/moon-ui-gpui/src/settings/import_preview.rs @@ -372,6 +372,9 @@ impl SettingsView { for item in &state.plan.core_commands { group = group.child( div() + // `label: new` is a config key beside the value it will take -- the same + // data `value_el` pins below, just rendered through a bare `format!`. + .font_family(design::mono()) .text_color(rgba_from(p.text_soft, 1.0)) .child(format!("{}: {}", item.label, item.new)), ); @@ -392,6 +395,10 @@ impl SettingsView { for u in &state.plan.unsupported { group = group.child( div() + // `u.name` is a raw config key or path (`[buy] color = ...`), a value the + // user matches against their own file, so this line keeps the data face + // even though the reason after it reads as prose. + .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(rgba_from(p.text_muted, 1.0)) .child(format!("{} — {}", u.name, u.reason)), @@ -589,6 +596,11 @@ fn value_el(item: &SettingChange, p: MoonPalette, cx: &Context) -> use moon_core::config::moonbot_import::plan::PlannedValue; let muted = |s: String| { div() + // These are the OLD and NEW values of one setting, drawn side by side with an arrow + // between them so the user can compare them before overwriting a config. That is the + // definition of data here, so they keep the mono face the flipped Settings root would + // otherwise take away. + .font_family(design::mono()) .text_size(design::t_caption(cx)) .text_color(rgba_from(p.text_muted, 1.0)) .child(s) diff --git a/crates/moon-ui-gpui/src/settings/render.rs b/crates/moon-ui-gpui/src/settings/render.rs index e8d5716b..3f2d6531 100644 --- a/crates/moon-ui-gpui/src/settings/render.rs +++ b/crates/moon-ui-gpui/src/settings/render.rs @@ -96,6 +96,9 @@ impl Render for SettingsView { .p(design::ui_px(cx, 18.0)) .gap(design::ui_px(cx, 10.0)) .overflow_hidden() + // The Settings root flips to the UI face for prose; Connections holds a data + // table and chrome that must stay mono like every other data surface. + .font_family(design::mono()) .child(content) .into_any_element() } else { @@ -255,7 +258,7 @@ impl Render for SettingsView { .size_full() .relative() .bg(rgba_from(p.shell, 1.0)) - .font_family(design::mono()) + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .line_height(design::line_px(cx, 14.0)) .text_color(rgba_from(p.text, 1.0)) diff --git a/crates/moon-ui-gpui/src/settings/security.rs b/crates/moon-ui-gpui/src/settings/security.rs index 8747bd02..5bb0406f 100644 --- a/crates/moon-ui-gpui/src/settings/security.rs +++ b/crates/moon-ui-gpui/src/settings/security.rs @@ -261,6 +261,11 @@ fn password_row( .state(state) .small() .disabled(!enabled) + // The Settings root renders prose in the UI face and `MoonInput` inherits it. + // A secret must not: the eye below reveals it, and a key is read character by + // character, where a proportional face merges 0 with O and l with 1. Same pin + // as `window/login.rs`'s password field, for the same reason. + .mono(true) // The eye is what makes a masked field usable; without it the only way to // check a long password is to retype it. .mask_toggle(), @@ -409,6 +414,9 @@ impl SettingsView { .items_center() .child( div() + // MIXED NODE: the locale string welds the prose label to the used/max slot + // FIGURES, and half a node cannot be styled. The figure wins. + .font_family(design::mono()) .text_color(rgba_from(p.text_soft, 1.0)) .child(text.to_string()), ) @@ -614,6 +622,8 @@ fn strength_meter(password: &str, p: MoonPalette, cx: &App) -> impl IntoElement ) .child( div() + // MIXED NODE: an entropy FIGURE welded to its unit in one locale string. + .font_family(design::mono()) .text_color(rgba_from(p.text_muted, 1.0)) .text_size(design::t_caption(cx)) .child(t!("security.bits", bits = verdict.bits.round() as i32).to_string()), diff --git a/crates/moon-ui-gpui/src/settings/storage.rs b/crates/moon-ui-gpui/src/settings/storage.rs index e74cbe02..c283eb5f 100644 --- a/crates/moon-ui-gpui/src/settings/storage.rs +++ b/crates/moon-ui-gpui/src/settings/storage.rs @@ -228,6 +228,7 @@ impl SettingsView { .child(div().font_bold().child(t!("storage.data_dir").to_string())) .child( div() + .font_family(design::mono()) .text_color(muted) .child(paths::db_dir().display().to_string()), ) @@ -245,23 +246,32 @@ impl SettingsView { .render(), ), ) - .child(hint( - t!("storage.total_size", size = fmt_size(total)).to_string(), - )) + // MIXED NODE: `storage.total_size` combines the localized "Total:" label with the + // figure in one text node (locales/storage.yml:22-25) — cannot style half of it, so + // it stays mono. + .child( + hint(t!("storage.total_size", size = fmt_size(total)).to_string()) + .font_family(design::mono()), + ) .child(separator(p, cx)) // ── Reports ───────────────────────────────────────────────────── .child(section(&t!("storage.reports_title"), p, cx)) - .child(hint(format!( - "{} · {}", - size_line(info.reports), - match &info.report_rows { - Some(Ok(rows)) => t!("storage.reports_rows", rows = rows).to_string(), - // A missing replica and a pending snapshot are non-errors, - // but neither is evidence of zero rows. - Some(Err(moon_core::db::ReadFail::NotReady)) | None => "—".to_string(), - Some(Err(_)) => t!("common.db_read_failed_short").to_string(), - } - ))) + // MIXED NODE: `size_line` and `storage.reports_rows` each combine a localized label + // with a figure in one text node (locales/storage.yml:22-25, 43-46) — stays mono. + .child( + hint(format!( + "{} · {}", + size_line(info.reports), + match &info.report_rows { + Some(Ok(rows)) => t!("storage.reports_rows", rows = rows).to_string(), + // A missing replica and a pending snapshot are non-errors, + // but neither is evidence of zero rows. + Some(Err(moon_core::db::ReadFail::NotReady)) | None => "—".to_string(), + Some(Err(_)) => t!("common.db_read_failed_short").to_string(), + } + )) + .font_family(design::mono()), + ) .child( h_flex().child( tool_btn("reports-compact", t!("storage.compact").to_string(), busy) @@ -293,16 +303,20 @@ impl SettingsView { })), ) .child(hint(t!("storage.strategies_enabled_hint").to_string())) - .child(hint(format!( - "{} · {}", - size_line(info.strategies), - t!( - "storage.strategies_rows", - live = info.strat_live, - deleted = info.strat_deleted, - versions = info.strat_versions - ) - ))) + // MIXED NODE: same as the reports readout above — stays mono. + .child( + hint(format!( + "{} · {}", + size_line(info.strategies), + t!( + "storage.strategies_rows", + live = info.strat_live, + deleted = info.strat_deleted, + versions = info.strat_versions + ) + )) + .font_family(design::mono()), + ) .child( h_flex() .gap(design::ui_px(cx, 8.0)) @@ -353,7 +367,8 @@ impl SettingsView { .child(separator(p, cx)) // ── Kline cache ───────────────────────────────────────────────── .child(section(&t!("storage.klines_title"), p, cx)) - .child(hint(size_line(info.klines))) + // MIXED NODE: `size_line` combines label and figure — stays mono. + .child(hint(size_line(info.klines)).font_family(design::mono())) .child(hint(t!("storage.klines_hint").to_string())) } } diff --git a/crates/moon-ui-gpui/src/strategies/params.rs b/crates/moon-ui-gpui/src/strategies/params.rs index e0d2ec9e..3ba7e596 100644 --- a/crates/moon-ui-gpui/src/strategies/params.rs +++ b/crates/moon-ui-gpui/src/strategies/params.rs @@ -1278,7 +1278,13 @@ impl StrategiesView { ParamsPanelModel::Content { .. } => unreachable!(), }; return col - .child(div().mt_2().text_color(moon(p.text_muted)).child(text)) + .child( + div() + .mt_2() + .font_family(design::ui_font()) + .text_color(moon(p.text_muted)) + .child(text), + ) .into_any_element(); }; let keys: Vec = row_pairs.iter().map(|(key, _)| *key).collect(); diff --git a/crates/moon-ui-gpui/src/strategies/sections.rs b/crates/moon-ui-gpui/src/strategies/sections.rs index edcea245..30a6e17b 100644 --- a/crates/moon-ui-gpui/src/strategies/sections.rs +++ b/crates/moon-ui-gpui/src/strategies/sections.rs @@ -119,7 +119,8 @@ impl StrategiesView { /// Measure the longest selected runtime section title for responsive first-run layout. /// /// The localized panel heading is the fallback when no selected runtime schema is available. - /// Measurement matches the monospaced body text inherited by section rows. + /// Section titles render as the clickable caption of a table-of-contents row, so this measures + /// them in the UI face they actually render in. /// /// Args: /// store: Live core store containing the selected strategy schema. @@ -133,7 +134,7 @@ impl StrategiesView { sections .iter() .map(|section| { - design::mono_body_text_width( + design::ui_body_text_width( cx, §ion_display_title(§ion.title), 400.0, @@ -142,7 +143,7 @@ impl StrategiesView { .reduce(f32::max) }) .unwrap_or_else(|| { - design::mono_body_text_width(cx, &t!("strat.sections").to_string(), 600.0) + design::ui_body_text_width(cx, &t!("strat.sections").to_string(), 600.0) }) } @@ -174,6 +175,7 @@ impl StrategiesView { .gap(design::ui_px(cx, 7.0)) .child( div() + .font_family(design::ui_font()) .font_weight(FontWeight::SEMIBOLD) .child(t!("strat.sections").to_string()), ) @@ -183,13 +185,22 @@ impl StrategiesView { // right already says so. Repeating the sentence here made the window ask the same question // twice side by side, so the column keeps its heading and stays otherwise empty. let Some(sections) = selected_sections(self, store) else { - return col.into_any_element(); + return col + .child( + div() + .mt_2() + .font_family(design::ui_font()) + .text_color(moon(p.text_muted)) + .child(t!("strat.no_selection").to_string()), + ) + .into_any_element(); }; if sections.is_empty() { return col .child( div() .mt_2() + .font_family(design::ui_font()) .text_color(moon(p.text_muted)) .child(t!("strat.no_schema").to_string()), ) @@ -216,6 +227,7 @@ impl StrategiesView { }; let on_all = self.versions.section.is_none(); let mut all_row = row_base("sec-ver-all".into(), cx) + .font_family(design::ui_font()) .font_weight(FontWeight::SEMIBOLD) .text_color(moon(p.text)) .child(t!("strat.sections_all").to_string()) @@ -257,6 +269,7 @@ impl StrategiesView { } let on = self.versions.section == Some(i); let mut row = row_base(SharedString::from(format!("sec-ver-{i}")), cx) + .font_family(design::ui_font()) .text_color(moon(p.text)) // The count badge beside it cannot shrink, and a Russian section name is // half again as long as the schema's own: without this the title paints @@ -335,6 +348,7 @@ impl StrategiesView { .flex() .items_center() .cursor_pointer() + .font_family(design::ui_font()) .text_color(moon(tcol)) // The pane is user-resizable down to a width no Russian section name fits, so the // caption degrades to an ellipsis rather than spilling into the splitter. diff --git a/crates/moon-ui-gpui/src/strategies/settings.rs b/crates/moon-ui-gpui/src/strategies/settings.rs index 7fc5b2b5..61f9c928 100644 --- a/crates/moon-ui-gpui/src/strategies/settings.rs +++ b/crates/moon-ui-gpui/src/strategies/settings.rs @@ -275,6 +275,7 @@ impl StrategiesView { div() .id("strategies-active-only-label") .cursor_pointer() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(palette.text_soft)) .child(t!("strat.active_only").to_string()) @@ -384,7 +385,12 @@ fn settings_content_width(cx: &App) -> f32 { let title = t!("strat.settings.title"); let group = t!(DISPLAY_GROUP); let title_width = popup_text_width(cx, &title, tokens.typography.mono_font_size, 400.0); - let group_width = popup_text_width(cx, &group, POPUP_GROUP_CAPTION_FONT, 600.0); + // Measured MONO on purpose, unlike every other run in this function. This caption is rendered + // by `popup_group` -> `MoonGroupBox::title()`, which MoonUI builds as `MoonText.mono(true)` + // (`moon/group_box.rs:98-104`) with no builder override -- so the popup's own `ui_font()` root + // never reaches it. Measure it proportionally and the width would describe a caption that is + // not the one drawn. + let group_width = design::ui_text_width(cx, &group, POPUP_GROUP_CAPTION_FONT, 600.0, true); let checkbox_label_width = POPUP_ROWS .iter() .map(|row| popup_text_width(cx, &t!(row.label), COMPACT_CHECKBOX_FONT, 400.0)) @@ -418,13 +424,13 @@ fn settings_content_width(cx: &App) -> f32 { /// Measure one popup text run in the family this popup actually renders in. /// -/// The Strategies window root applies `.font_family(design::mono())`, and the popup never sets a -/// family of its own, so every measurement here must use -/// the same monospaced family the renderer will use — this is the only [`design::ui_text_width`] +/// The tree pane root stays `.font_family(design::mono())`, but `settings_content`'s own root now +/// applies `.font_family(design::ui_font())` on top of it, so every measurement here must use +/// the same proportional family the renderer will use — this is the only [`design::ui_text_width`] /// call left in this file, so the family cannot diverge per call site again. It resolves through -/// the theme's mono TOKEN (`mono = true` -> `MoonTheme::active_tokens(cx).font_family(true)`, -/// `design::measure_font`), not the [`design::mono`] literal the window root reaches for: if the -/// token ever stops being Geist Mono, this measurement follows it, and the root's literal becomes +/// the theme's UI TOKEN (`mono = false` -> `MoonTheme::active_tokens(cx).font_family(false)`, +/// `design::measure_font`), not the [`design::ui_font`] literal the popup root reaches for: if the +/// token ever stops being Inter, this measurement follows it, and the root's literal becomes /// the thing to report. /// /// Args: @@ -436,7 +442,7 @@ fn settings_content_width(cx: &App) -> f32 { /// Returns: /// The summed glyph-advance estimate in pixels. fn popup_text_width(cx: &App, text: &str, base_size: f32, weight: f32) -> f32 { - design::ui_text_width(cx, text, base_size, weight, true) + design::ui_text_width(cx, text, base_size, weight, false) } /// Pure arithmetic core of [`settings_content_width`], free of `cx` and the GPUI text system. @@ -550,10 +556,13 @@ fn settings_content( palette: MoonPalette, cx: &App, ) -> AnyElement { - // MoonPopover owns the surface chrome; this root supplies only content and spacing. + // MoonPopover owns the surface chrome; this root supplies only content and spacing. Every + // title, caption, and checkbox label here is prose, so the popup flips to the UI face on + // its own root rather than inheriting the tree pane's mono. v_flex() .id("strategies-settings-popup") .w_full() + .font_family(design::ui_font()) .gap(design::ui_px(cx, 8.0)) .child( h_flex() diff --git a/crates/moon-ui-gpui/src/strategies/tree/mod.rs b/crates/moon-ui-gpui/src/strategies/tree/mod.rs index 965cb61f..eafaed61 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/mod.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/mod.rs @@ -640,6 +640,10 @@ impl StrategiesView { .min_w_0() .truncate() .text_center() + // MIXED NODE: `strat.staged` welds its label to the COUNT in one locale string, so it + // keeps mono while the rest of the footer reads in the UI face. Half a node cannot be + // styled, and the figure is the half that has to stay legible. + .font_family(design::mono()) .text_size(design::t_body(cx)) .text_color(rgb(MoonPalette::active(cx).amber)) .child(staged_label.unwrap_or_default()); @@ -650,6 +654,9 @@ impl StrategiesView { .py(design::ui_px(cx, 8.0)) .gap(design::ui_px(cx, group_gap)) .items_center() + // The footer's leading icons carry no text of their own; its action labels are prose. + // `staged_slot` pins its mixed caption/count node back to mono above. + .font_family(design::ui_font()) .child(self.selection_toolbar(store, show_labels, !cores.is_empty(), cx)) .child(design::chrome_divider(cx, MoonPalette::active(cx))) .child(staged_slot) diff --git a/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs b/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs index b649627f..64b1b0ca 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs @@ -225,7 +225,7 @@ impl StrategiesView { let locale = rust_i18n::locale(); let locale: &str = &locale; let metrics = - design::text_metrics_key(cx, design::ACTION_LABEL_BASE, FOOTER_LABEL_WEIGHT, true); + design::text_metrics_key(cx, design::ACTION_LABEL_BASE, FOOTER_LABEL_WEIGHT, false); // Compared field by field rather than against a freshly built key: owning the locale means // allocating it, and doing that on a HIT would put a per-frame allocation in the one path // this module exists to keep empty. @@ -334,11 +334,14 @@ fn footer_label_width(cx: &App, staged: usize) -> f32 { label, design::ACTION_LABEL_BASE, FOOTER_LABEL_WEIGHT, - true, + false, ) }) .sum(); if staged > 0 { + // MONO, unlike the action labels summed above: `strat.staged` welds its label to a COUNT + // in one locale string, so `staged_slot` keeps the data face while the rest of the footer + // reads in the UI face. This term measures the family that slot actually draws. width += design::ui_text_width( cx, &t!("strat.staged", n = staged), diff --git a/crates/moon-ui-gpui/src/strategies/versions.rs b/crates/moon-ui-gpui/src/strategies/versions.rs index 99d8b3fe..82366d0a 100644 --- a/crates/moon-ui-gpui/src/strategies/versions.rs +++ b/crates/moon-ui-gpui/src/strategies/versions.rs @@ -699,6 +699,7 @@ impl StrategiesView { .child(div().w_full().h(px(1.0)).bg(border)) .child( div() + .font_family(design::ui_font()) .text_size(design::t_caption(cx)) .text_color(moon(p.text_muted)) .child(t!("strat.versions_history").to_string()), @@ -877,7 +878,7 @@ impl StrategiesView { // away (defect 5, and it must be unlosable). if let (Some(vf), true) = (self.versions.sel, live_exists) { let compact = - design::mono_body_text_width(cx, &t!("strat.version_restore").to_string(), 400.0) + design::ui_body_text_width(cx, &t!("strat.version_restore").to_string(), 400.0) + 40.0 > self.panels.versions_w - design::ui_value(cx, VERSIONS_PANE_PADDING * 2.0); col = col.child( @@ -885,6 +886,7 @@ impl StrategiesView { .flex_none() .w_full() .pt(design::ui_px(cx, 6.0)) + .font_family(design::ui_font()) .child(self.version_restore_button(vf, compact, cx)), ); } diff --git a/crates/moon-ui-gpui/src/window/login.rs b/crates/moon-ui-gpui/src/window/login.rs index 2dc61f8b..266253c7 100644 --- a/crates/moon-ui-gpui/src/window/login.rs +++ b/crates/moon-ui-gpui/src/window/login.rs @@ -284,6 +284,11 @@ impl LoginView { .placeholder(t!("login.placeholder").to_string()) .disabled(busy) .loading(busy) + // The window root renders prose in the UI face, and `MoonInput` inherits it. + // The secret must not: `mask_toggle` reveals it on demand, and a key is read + // character by character, where a proportional face merges 0 with O and l + // with 1. + .mono(true) .mask_toggle(), ) .when_some(self.error, |this, key| { @@ -404,7 +409,9 @@ impl Render for LoginView { .size_full() .relative() .bg(rgba_from(p.shell, 1.0)) - .font_family(design::mono()) + // Prose window: a caption, an error line and a throttle notice, all sentences. The one + // value it holds is the secret itself, pinned back to mono at its own input. + .font_family(design::ui_font()) .text_size(design::t_body(cx)) .line_height(design::line_px(cx, 14.0)) .text_color(rgba_from(p.text, 1.0)) diff --git a/crates/moon-ui-gpui/tests/theme_contract/analytics.rs b/crates/moon-ui-gpui/tests/theme_contract/analytics.rs index 5150850f..fc0afaaf 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/analytics.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/analytics.rs @@ -1070,7 +1070,6 @@ fn analytics_tabs_and_core_caption_follow_their_content() { let body = braced_body(&toolbar, "pub(super) fn tabs_bar("); for needle in [ "let title = t.title();", - "design::ui_text_width(cx, &title, 10.5, 400.0, true)", "design::ui_value(cx, 20.0)", ".max(design::ui_value(cx, 72.0))", ".width(tab_width)", @@ -1081,6 +1080,11 @@ fn analytics_tabs_and_core_caption_follow_their_content() { "`tabs_bar` must contain {needle:?} so every localized tab keeps measured padding" ); } + assert!( + body.contains("design::ui_text_width(cx, &title, 10.5, 400.0, false)") + && body.contains("div().font_family(design::ui_font()).child("), + "Analytics tab widths must measure UI-family captions and host each tab button in that same UI family" + ); assert!( !body.contains(".width(112.0)"), "the old fixed width must not override content-driven tab sizing" diff --git a/crates/moon-ui-gpui/tests/theme_contract/fonts.rs b/crates/moon-ui-gpui/tests/theme_contract/fonts.rs new file mode 100644 index 00000000..d8ecfe31 --- /dev/null +++ b/crates/moon-ui-gpui/tests/theme_contract/fonts.rs @@ -0,0 +1,339 @@ +//! Static font-family contracts for data surfaces and paired width measurements. +//! +//! `moon-ui-gpui` is a binary crate, so these tests read render sources rather than importing +//! views. Each assertion names a data surface whose values users compare across rows or read as +//! figures; changing one to the UI family would make a font sweep silently corrupt that visual +//! distinction. + +use super::support::{braced_body, chain_between, code_only, read_src}; + +/// Return one render-root body without permitting comments to satisfy a source contract. +fn render_root(source: &str, render_impl: &str) -> String { + let render_impl = braced_body(source, render_impl); + code_only(braced_body(render_impl, "fn render(")) +} + +/// The panel, window, and strategy-pane roots below contain data tables, figures, names, or logs. +/// +/// Breakage: changing any listed render root from `design::mono()` to `design::ui_font()` makes +/// values and row comparisons proportional, so users can no longer scan aligned data reliably. +#[test] +fn data_render_roots_keep_the_mono_family() { + for (path, render_impl, surface) in [ + ( + "shell/render.rs", + "impl Render for Shell", + "the Shell trade and status data", + ), + ( + "panels/log/view.rs", + "impl Render for LogPanel", + "log lines", + ), + ( + "panels/orders/render.rs", + "impl Render for OrdersPanel", + "order-table values", + ), + ( + "panels/report/render.rs", + "impl Render for ReportPanel", + "report rows", + ), + ( + "panels/report/trade_log/view.rs", + "impl Render for TradeLog", + "trade-log lines", + ), + ( + "panels/assets/render.rs", + "impl Render for AssetsView", + "asset figures", + ), + ( + "panels/core_status/mod.rs", + "impl Render for CoreStatusView", + "core-status figures", + ), + ( + "panels/alerts/mod.rs", + "impl Render for AlertsPanel", + "alert rows", + ), + ( + "panels/news/mod.rs", + "impl Render for NewsView", + "news timestamps and tickers", + ), + ( + "analytics/render.rs", + "impl Render for AnalyticsView", + "analytics figures", + ), + ( + "analytics/profit_monitor/mod.rs", + "impl Render for ProfitMonitorView", + "profit-monitor rows", + ), + ( + "screener/view.rs", + "impl Render for ScreenerView", + "screener rows", + ), + ( + "strategies/mod.rs", + "impl Render for StrategiesView", + "strategy rows and names", + ), + ] { + let source = read_src(path); + let root = render_root(&source, render_impl); + let mono = if path == "panels/log/view.rs" { + ".font_family(crate::design::mono())" + } else { + ".font_family(design::mono())" + }; + assert!( + root.contains(mono), + "{path}:{render_impl} must keep mono for {surface}; a proportional root makes compared values drift" + ); + } + + for (path, outer, function, surface) in [ + ( + "strategies/tree/mod.rs", + "impl StrategiesView", + "pub(super) fn tree_panel(", + "strategy tree names and row values", + ), + ( + "strategies/params.rs", + "impl StrategiesView", + "pub(super) fn params_panel(", + "strategy parameter values", + ), + ( + "strategies/versions.rs", + "impl StrategiesView", + "pub(super) fn versions_panel(", + "strategy-version timestamps and values", + ), + ] { + let source = read_src(path); + let outer = braced_body(&source, outer); + let body = code_only(braced_body(outer, function)); + assert!( + body.contains(".font_family(design::mono())"), + "{path}:{function} must keep mono for {surface}; a proportional pane breaks comparison scanning" + ); + } +} + +/// Chart glyphs, chart-tab names, and header figures are data, not prose. +/// +/// Breakage: replacing their mono face with the UI face changes the width of prices, axes, coin +/// names, timestamps, tickers, and balances while the adjacent geometry remains data-oriented. +#[test] +fn chart_and_header_data_text_keep_the_mono_family() { + let text = read_src("chartdx/text/mod.rs"); + for function in [ + "fn draw_text_run(", + "fn measure_text_run(", + "fn draw_label_text_run(", + "fn measure_label_text_run(", + "fn measure_run_width(", + ] { + assert!( + code_only(braced_body(&text, function)).contains("gpui::font(crate::design::mono())"), + "chartdx/text/mod.rs:{function} must keep mono because chart axis and order-line text are compared figures" + ); + } + let captions = read_src("chartdx/text/captions.rs"); + for function in ["fn measure_caption_run(", "fn draw_caption_run("] { + assert!( + code_only(braced_body(&captions, function)) + .contains("gpui::font(crate::design::mono())"), + "chartdx/text/captions.rs:{function} must keep mono because chart captions carry figures and labels with aligned geometry" + ); + } + let runs = read_src("chartdx/text/runs.rs"); + assert!( + code_only(braced_body(&runs, "impl RenderState")) + .contains("gpui::font(crate::design::mono())"), + "chartdx/text/runs.rs:RenderState must keep mono for retained chart-text runs" + ); + let stack = read_src("chart_tabs/stack.rs"); + assert_eq!( + code_only(braced_body(&stack, "pub(super) fn chart_stack_card(")) + .matches(".font_family(crate::design::mono())") + .count(), + 2, + "chart stack coin/core titles and their trailing data note must both stay mono" + ); + for (path, function, surface) in [ + ( + "chrome/clock.rs", + "fn render_header_clock(", + "the header timestamp", + ), + ( + "chrome/terminal_chrome.rs", + "fn ticker_readout(", + "the ticker readout", + ), + ( + "chrome/terminal_chrome.rs", + "fn balance_label(", + "the balance figure", + ), + ] { + let source = read_src(path); + assert!( + code_only(braced_body(&source, function)).contains(".font_family(design::mono())"), + "{path}:{function} must keep mono for {surface}, which users compare as a figure" + ); + } + // `analytics::report_strategy_combo...` already pins report/controls.rs:strategy_combo's + // `design::mono()` call. Keep that existing assertion as the single oracle for this site. +} + +/// Settings changes its root to the UI face, while its input and numeric value containers remain data. +/// +/// Breakage: omitting one re-pin makes a typed value, slider endpoint, or counter render in Inter +/// after the Settings root changes, despite users reading it as a comparable value. +#[test] +fn settings_values_and_connections_repin_the_mono_family() { + let general = read_src("settings/general.rs"); + for (function, mono) in [ + ("pub(super) fn font_delta_control(", ".mono(true)"), + ( + "pub(super) fn stepper_controls(", + ".font_family(design::mono())", + ), + ] { + assert!( + code_only(braced_body(&general, function)).contains(mono), + "settings/general.rs:{function} must explicitly keep its editable or stepped value mono" + ); + } + assert!( + code_only(braced_body(&general, "fn font_delta_marks(")) + .contains(".font_family(design::mono())"), + "settings/general.rs:font_delta_marks must keep its compared tick numbers mono" + ); + let badges = read_src("settings/badges.rs"); + let badge_row = code_only(braced_body(&badges, "fn badge_row(")); + assert_eq!( + badge_row.matches(".mono(true)").count(), + 5, + "the ordinal, name, code, optional short-code, and status badge values must all stay mono after Settings becomes proportional" + ); + let common = read_src("settings/common.rs"); + assert!( + code_only(braced_body(&common, "pub(super) fn slider_row(")) + .contains(".font_family(design::mono())"), + "slider endpoints and current numeric value must share a mono container after Settings becomes proportional" + ); + let settings = read_src("settings/render.rs"); + let render = render_root(&settings, "impl Render for SettingsView"); + let connections = chain_between( + &render, + "if self.active == Tab::Connections {", + "} else {", + "Settings Connections body", + ); + assert!( + connections.contains(".font_family(design::mono())"), + "Settings Connections must pin mono at its branch because its table inherits the Settings root otherwise" + ); + + let security = read_src("settings/security.rs"); + assert!( + code_only(braced_body(&security, "fn password_row(")).contains(".mono(true)"), + "settings/security.rs:password_row must keep every revealable secret mono" + ); + assert!( + code_only(braced_body(&security, "fn machines_row(")) + .contains(".font_family(design::mono())"), + "settings/security.rs:machines_row must keep its machine-slot figures mono" + ); + assert!( + code_only(braced_body(&security, "fn strength_meter(")) + .contains(".font_family(design::mono())"), + "settings/security.rs:strength_meter must keep its security.bits entropy figure mono" + ); + + let import_preview = read_src("settings/import_preview.rs"); + assert!( + code_only(braced_body(&import_preview, "fn value_el(")) + .contains(".font_family(design::mono())"), + "settings/import_preview.rs:value_el must keep old and new setting values mono" + ); + + let hotkeys = read_src("settings/hotkeys/tab.rs"); + let pull_row = code_only(braced_body(&hotkeys, "fn core_pull_row(")); + let pull_slot = chain_between( + &pull_row, + "let id = format!(\"core-pull-{}\", slot_id(row.slot));", + "MoonHotkeyInput::new", + "core hotkey slot identity", + ); + assert!( + pull_slot.contains(".font_family(design::mono())"), + "settings/hotkeys/tab.rs:core_pull_row must keep the compared slot identity mono" + ); + + let tree = read_src("strategies/tree/mod.rs"); + let action_bar = code_only(braced_body(&tree, "fn action_bar(")); + let staged_slot = chain_between( + &action_bar, + "let staged_slot = div()", + ".child(staged_label.unwrap_or_default())", + "Strategies staged-count slot", + ); + assert!( + staged_slot.contains(".font_family(design::mono())"), + "strategies/tree/mod.rs:action_bar must keep its staged count mono" + ); + + let strategy_settings = read_src("strategies/settings.rs"); + let settings_width = code_only(braced_body( + &strategy_settings, + "fn settings_content_width(", + )); + let group_width = chain_between( + &settings_width, + "let group_width =", + "let checkbox_label_width =", + "Strategies group-caption measurement", + ); + assert!( + group_width.contains("true"), + "strategies/settings.rs:group_width must measure MoonGroupBox's hardcoded mono caption" + ); +} + +/// Widths and rendered control captions must use one font family, while row-fit keeps its mono cache identity. +/// +/// Breakage: changing only one side shifts the quiet toggle and ticker popup, or preserves stale +/// header shedding thresholds after a mono/UI-family font change. +#[test] +fn width_measurements_agree_with_their_rendered_family_and_cache_key() { + let quiet = read_src("chrome/quiet.rs"); + assert!( + code_only(braced_body(&quiet, "pub(crate) fn header_quiet_width(")) + .contains("design::ui_caption_text_width("), + "header_quiet_width must measure the UI caption family because shell::ticker reuses this width as its popup offset" + ); + assert!( + code_only(braced_body(&quiet, "pub(crate) fn header_quiet_cluster(")) + .contains(".font_family(design::ui_font())"), + "the quiet toggle caption must render in the UI family that header_quiet_width measures" + ); + let wrap_fit = read_src("controls/wrap_fit.rs"); + let signature = code_only(braced_body(&wrap_fit, "pub(crate) fn signature(")); + assert!( + signature.contains("design::text_metrics_key(cx, design::ACTION_LABEL_BASE, 400.0, true)"), + "row-fit signature must hash the mono family used by its Report filter caller" + ); +} diff --git a/crates/moon-ui-gpui/tests/theme_contract/main.rs b/crates/moon-ui-gpui/tests/theme_contract/main.rs index 6d1d0975..5a1fdcf4 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/main.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/main.rs @@ -20,6 +20,7 @@ mod core_pick; mod core_status; mod detects; mod dock_chrome; +mod fonts; mod log; mod naming; mod report; diff --git a/crates/moon-ui-gpui/tests/theme_contract/shell.rs b/crates/moon-ui-gpui/tests/theme_contract/shell.rs index 60d7fd89..70cbd93b 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/shell.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/shell.rs @@ -1256,8 +1256,21 @@ fn toolbar_launcher_labels_are_measured_and_all_or_none() { assert!(measure.contains("TOOLBAR_LAUNCHER_TEXT_SIZE")); assert!(measure.contains("TOOLBAR_LAUNCHER_TEXT_WEIGHT")); assert!( - measure.contains("true,"), - "launcher widths must use the monospaced family inherited from the Shell root" + measure.contains("false,"), + "launcher widths must measure the UI family their labeled controls render in, or narrow-header shedding clips captions or hides them too early" + ); + let labeled_launchers = chain_between( + toolbar, + "toolbar-screener", + "settings_hint_at", + "labeled launcher containers", + ); + assert_eq!( + labeled_launchers + .matches(".font_family(design::ui_font())") + .count(), + 2, + "the Strategies/Analytics section and Settings hosting div must both render control captions in the UI family measured by launcher_label_width" ); assert!(measure.contains(".max(ICON_BTN_W)")); for (label, width) in [ From a88d7f3b564bfb05a6433709b2e87da0f2d4e3d7 Mon Sep 17 00:00:00 2001 From: kirillDevPro <113171057+kirillDevPro@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:30:08 +0200 Subject: [PATCH 2/2] fix(strategies): lead every section with Moonbot's own title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Sections list showed the Russian label with the schema title one hover away, the opposite of the field rows beside it, and a trader who knows a section by Moonbot's wording had to guess. The column also repeated the "select a strategy" sentence the parameters pane already shows, a duplicate an integration merge brought back. Rows now read the schema title first with the localized name as a muted caption under it, headings read "Main · Основные", and the measured column width follows the wider of the two lines. The empty column keeps only its heading. Claude-Session: https://claude.ai/code/session_01RaQ2tnAyr4pyiAiwGktPBv --- .../moon-ui-gpui/src/strategies/sections.rs | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/crates/moon-ui-gpui/src/strategies/sections.rs b/crates/moon-ui-gpui/src/strategies/sections.rs index 30a6e17b..9312a3aa 100644 --- a/crates/moon-ui-gpui/src/strategies/sections.rs +++ b/crates/moon-ui-gpui/src/strategies/sections.rs @@ -101,20 +101,60 @@ pub(super) fn section_label_key(raw_title: &str) -> Option<&'static str> { .map(|(_, key)| *key) } -/// Human name for a runtime section title, or the raw title when there is no label for it. +/// Heading text for a runtime section: Moonbot's own title first, the human name after it. +/// +/// The schema title leads because it is what the Moonbot manual, a forum post and the strategy +/// file all call the section; the localized name follows as a gloss, so a heading reads +/// `Main · Основные` and stays findable by either word. /// /// Args: /// raw_title: Section title exactly as the streamed schema produced it. /// /// Returns: -/// The localized section name, or `raw_title` unchanged. +/// `" · "` when a label exists, or `raw_title` unchanged. pub(super) fn section_display_title(raw_title: &str) -> String { match section_label_key(raw_title) { - Some(key) => t!(key).to_string(), + Some(key) => format!("{raw_title} · {}", t!(key)), None => raw_title.to_string(), } } +/// Two-line caption for a table-of-contents row: the schema title, then its human name under it. +/// +/// A section with no label keeps one line, so an unrecognised section looks exactly as it did. +/// +/// Args: +/// raw_title: Section title exactly as the streamed schema produced it. +/// muted: Colour of the localized second line. +/// cx: Application context providing active text metrics. +/// +/// Returns: +/// A width-owning column that truncates each line on its own. +fn section_caption(raw_title: &str, muted: Hsla, cx: &App) -> impl IntoElement { + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .w_full() + .min_w_0() + .truncate() + .child(raw_title.to_string()), + ) + .when_some(section_label_key(raw_title), |col, key| { + col.child( + div() + .w_full() + .min_w_0() + .truncate() + .text_size(design::t_caption(cx)) + .line_height(design::line_px(cx, 12.0)) + .text_color(muted) + .child(t!(key).to_string()), + ) + }) +} + impl StrategiesView { /// Measure the longest selected runtime section title for responsive first-run layout. /// @@ -134,11 +174,11 @@ impl StrategiesView { sections .iter() .map(|section| { - design::ui_body_text_width( - cx, - §ion_display_title(§ion.title), - 400.0, - ) + let raw = design::ui_body_text_width(cx, §ion.title, 400.0); + let label = section_label_key(§ion.title).map_or(0.0, |key| { + design::ui_caption_text_width(cx, &t!(key).to_string(), 400.0) + }); + raw.max(label) }) .reduce(f32::max) }) @@ -185,15 +225,7 @@ impl StrategiesView { // right already says so. Repeating the sentence here made the window ask the same question // twice side by side, so the column keeps its heading and stays otherwise empty. let Some(sections) = selected_sections(self, store) else { - return col - .child( - div() - .mt_2() - .font_family(design::ui_font()) - .text_color(moon(p.text_muted)) - .child(t!("strat.no_selection").to_string()), - ) - .into_any_element(); + return col.into_any_element(); }; if sections.is_empty() { return col @@ -216,7 +248,8 @@ impl StrategiesView { div() .id(id) .w_full() - .h(design::fit_h_px(cx, 24.0, 14.0, 5.0)) + .min_h(design::fit_h_px(cx, 24.0, 14.0, 5.0)) + .py(design::ui_px(cx, 3.0)) .px(design::ui_px(cx, 6.0)) .rounded(design::ui_px(cx, 3.0)) .border_1() @@ -271,16 +304,9 @@ impl StrategiesView { let mut row = row_base(SharedString::from(format!("sec-ver-{i}")), cx) .font_family(design::ui_font()) .text_color(moon(p.text)) - // The count badge beside it cannot shrink, and a Russian section name is - // half again as long as the schema's own: without this the title paints - // over the badge instead of degrading to an ellipsis. - .child( - div() - .flex_1() - .min_w_0() - .truncate() - .child(section_display_title(&sec.title)), - ) + // The count badge beside it cannot shrink, so the caption owns the width + // and degrades to an ellipsis instead of painting over the badge. + .child(section_caption(&sec.title, moon(p.text_muted), cx)) .child( h_flex().ml_auto().flex_none().child( MoonBadge::new(n.to_string()) @@ -334,13 +360,11 @@ impl StrategiesView { let sec = §ions[i]; let on = self.selected_section == i; let tcol = if !active { p.text_muted } else { p.text }; - // Resolved once: the caption and the raw-title tooltip ask the same question of the - // same string, and this row is rebuilt on every repaint. - let label_key = section_label_key(&sec.title); let mut row = div() .id(SharedString::from(format!("sec-{i}"))) .w_full() - .h(design::fit_h_px(cx, 24.0, 14.0, 5.0)) + .min_h(design::fit_h_px(cx, 24.0, 14.0, 5.0)) + .py(design::ui_px(cx, 3.0)) .px(design::ui_px(cx, 6.0)) .rounded(design::ui_px(cx, 3.0)) .border_1() @@ -350,17 +374,9 @@ impl StrategiesView { .cursor_pointer() .font_family(design::ui_font()) .text_color(moon(tcol)) - // The pane is user-resizable down to a width no Russian section name fits, so the - // caption degrades to an ellipsis rather than spilling into the splitter. - .child(div().flex_1().min_w_0().truncate().child(match label_key { - Some(key) => t!(key).to_string(), - None => sec.title.clone(), - })) - // The raw schema title stays one hover away wherever a human name replaced it, so - // a trader who knows Moonbot's own wording can still find the section by it. - .when_some(label_key.map(|_| sec.title.clone()), |row, raw| { - row.tooltip(crate::panels::common::text_tooltip(raw)) - }) + // The pane is user-resizable down to a width no section name fits, so each line + // of the caption degrades to an ellipsis rather than spilling into the splitter. + .child(section_caption(&sec.title, moon(p.text_muted), cx)) .on_click(cx.listener(move |this, _, _, cx| { if this.selected_section != i { this.selected_section = i;