From 5f0820ccc1626791c72e816b0f08583608fe6005 Mon Sep 17 00:00:00 2001 From: ThisMad Date: Sat, 19 Sep 2026 18:47:35 +0200 Subject: [PATCH 1/3] feat(ui): one proportional UI scale as window content zoom, density removed The Compact / Standard / Large density setting shifted the whole application one MoonUI tier. MoonUI draws each tier by hand, so a tier step substituted a different design rather than scaling the reviewed one, and the design's own mixture of tiers lost its proportions. The UI zoom slider fed the token multipliers, which reach only the geometry that goes through `tokens.ui()`. Now `ui_scale` (50-200 %) is installed as `MoonScale::zoom`; every window's `MoonRoot` applies it as content zoom, so every pixel, glyph and hitbox scales by construction. The tokens stay at the design's size system, spelled once as `design::CONTROL_TIER` / `DESIGN_FONT_DELTA` / `BODY_TEXT` / `INPUT_SIZE`; the tier resolvers, `UiDensity`, its settings row, locale keys and the density-migration contracts are gone. The chart keeps device density: `chartdx` sizes its render target by the combined factor and its own geometry and text by `scale_factor / content_zoom`, the text layer crosses into content pixels only at the GPUI boundary, and the overlays over chart geometry divide by the zoom in `chart_origin_logical`. All of it is shared code above the three GPU backends. `responsive_width` and the click series read content and screen space as they should; the FireTest probe is built in platform space and a `MOON_FIXTURE_SETTINGS` hook lets the bench open at a chosen scale. Requires MoonUI with `Window::set_content_zoom` (Moonbot-Tech/MoonUI#77). --- crates/moon-core/src/config/mod.rs | 19 +- crates/moon-core/src/config/reconcile.rs | 15 +- .../moon-core/src/config/reconcile/tests.rs | 47 ++- crates/moon-core/src/config/schema.rs | 37 +-- crates/moon-core/src/config/schema/tests.rs | 48 ++- crates/moon-core/src/config/tests.rs | 2 - crates/moon-ui-gpui/src/analytics/toolbar.rs | 19 +- .../src/analytics/toolbar/tests.rs | 20 +- .../src/analytics/tuner/coins/mod.rs | 2 +- .../src/analytics/tuner/filter/mod.rs | 6 +- .../src/analytics/tuner/list/mod.rs | 2 +- .../moon-ui-gpui/src/analytics/tuner/save.rs | 2 +- .../moon-ui-gpui/src/analytics/tuner/shell.rs | 8 +- .../src/analytics/tuner/time/grid.rs | 6 +- .../src/analytics/tuner/time/sliders.rs | 2 +- .../src/chart_tabs/detached_host/render.rs | 2 +- .../src/chart_tabs/labels_popup/mod.rs | 2 +- .../src/chart_tabs/labels_popup/rows.rs | 2 +- .../src/chart_tabs/layout_popup.rs | 8 +- .../chart_tabs/main_stack/empty/arrange.rs | 2 +- .../src/chart_tabs/main_stack/empty/detect.rs | 4 +- crates/moon-ui-gpui/src/chart_tabs/strip.rs | 2 +- .../src/chartdx/data_state/state.rs | 14 +- crates/moon-ui-gpui/src/chartdx/engine.rs | 8 +- crates/moon-ui-gpui/src/chartdx/mod.rs | 8 +- .../src/chartdx/render_state/tests.rs | 1 + .../moon-ui-gpui/src/chartdx/text/captions.rs | 33 +- crates/moon-ui-gpui/src/chartdx/text/mod.rs | 101 ++++-- .../moon-ui-gpui/src/chartdx/text/prepare.rs | 6 +- crates/moon-ui-gpui/src/chartdx/text/runs.rs | 16 +- .../src/chrome/terminal_chrome.rs | 4 +- .../src/controls/core_group_dialogs.rs | 4 +- .../moon-ui-gpui/src/controls/manual_strat.rs | 19 +- crates/moon-ui-gpui/src/controls/metric.rs | 2 +- crates/moon-ui-gpui/src/controls/strips.rs | 8 +- crates/moon-ui-gpui/src/controls/toolbar.rs | 15 +- .../src/controls/toolbar/tests.rs | 57 ++-- crates/moon-ui-gpui/src/controls/wrap_fit.rs | 6 +- .../src/core_expert/pages/autobuy.rs | 10 +- .../src/core_expert/pages/general.rs | 2 +- .../src/core_expert/pages/login.rs | 12 +- .../src/core_expert/pages/special.rs | 8 +- .../src/core_expert/pages/telegram.rs | 2 +- .../moon-ui-gpui/src/core_expert/widgets.rs | 14 +- crates/moon-ui-gpui/src/design.rs | 254 +++++--------- crates/moon-ui-gpui/src/design/tests.rs | 313 +++++++----------- .../src/panels/alerts/controls.rs | 2 +- .../src/panels/assets/settings.rs | 5 +- .../src/panels/assets/spot_order.rs | 4 +- .../moon-ui-gpui/src/panels/assets/table.rs | 2 +- .../moon-ui-gpui/src/panels/assets/wallets.rs | 2 +- .../moon-ui-gpui/src/panels/chart/arb_open.rs | 46 ++- .../src/panels/chart/filter_headers.rs | 2 +- .../src/panels/chart/market_actions.rs | 32 +- .../src/panels/chart/market_actions/tests.rs | 44 ++- .../moon-ui-gpui/src/panels/chart/render.rs | 19 +- .../src/panels/chart/render_input.rs | 7 +- .../src/panels/chart/volume_menu.rs | 2 +- crates/moon-ui-gpui/src/panels/common.rs | 35 +- .../src/panels/core_status/config_popup.rs | 2 +- .../src/panels/core_status/interactions.rs | 2 +- .../src/panels/core_status/server_view.rs | 6 +- .../src/panels/core_status/update_menu.rs | 2 +- .../moon-ui-gpui/src/panels/detects/cards.rs | 8 +- .../src/panels/label_edit/body.rs | 2 +- crates/moon-ui-gpui/src/panels/log/view.rs | 2 +- crates/moon-ui-gpui/src/panels/news/mod.rs | 2 +- .../src/panels/order_edit/body.rs | 4 +- .../src/panels/report/controls.rs | 2 +- .../moon-ui-gpui/src/panels/report/render.rs | 2 +- crates/moon-ui-gpui/src/screener/view.rs | 4 +- crates/moon-ui-gpui/src/settings/apply.rs | 5 +- crates/moon-ui-gpui/src/settings/badges.rs | 8 +- crates/moon-ui-gpui/src/settings/common.rs | 2 +- .../src/settings/connections/columns.rs | 21 +- .../src/settings/connections/columns/tests.rs | 86 +++-- .../src/settings/connections/table.rs | 2 +- crates/moon-ui-gpui/src/settings/general.rs | 78 +---- .../src/settings/general/tests.rs | 42 ++- .../moon-ui-gpui/src/settings/hotkeys/tab.rs | 23 +- crates/moon-ui-gpui/src/settings/mod.rs | 2 - crates/moon-ui-gpui/src/settings/security.rs | 2 +- crates/moon-ui-gpui/src/settings/telegram.rs | 2 +- .../src/settings/telegram/access.rs | 4 +- .../src/settings/telegram/core_section.rs | 2 +- .../telegram/core_section/login_steps.rs | 2 +- .../src/shell/core_settings_popup/general.rs | 4 +- .../src/shell/core_settings_popup/widgets.rs | 11 +- crates/moon-ui-gpui/src/shell/quiet_popup.rs | 6 +- crates/moon-ui-gpui/src/shell/ticker.rs | 2 +- crates/moon-ui-gpui/src/startup.rs | 44 +-- crates/moon-ui-gpui/src/startup/fixture.rs | 27 +- crates/moon-ui-gpui/src/startup/tests.rs | 43 ++- crates/moon-ui-gpui/src/startup/unlock.rs | 8 +- crates/moon-ui-gpui/src/strategies/params.rs | 6 +- .../moon-ui-gpui/src/strategies/settings.rs | 35 +- .../src/strategies/tree/checks.rs | 14 +- .../src/strategies/tree/dialogs.rs | 16 +- .../moon-ui-gpui/src/strategies/tree/mod.rs | 2 +- .../moon-ui-gpui/src/strategies/tree/moon.rs | 19 +- .../src/strategies/tree/pane_cache.rs | 6 +- crates/moon-ui-gpui/src/window/windowing.rs | 15 +- .../tests/theme_contract/analytics.rs | 6 +- .../tests/theme_contract/chart.rs | 2 +- .../tests/theme_contract/density.rs | 105 ------ .../tests/theme_contract/dock_chrome.rs | 6 +- .../tests/theme_contract/fonts.rs | 2 +- .../moon-ui-gpui/tests/theme_contract/main.rs | 1 - .../tests/theme_contract/shell.rs | 2 +- .../tests/theme_contract/strategies.rs | 16 +- .../tests/theme_contract/theme_selector.rs | 4 +- docs/ARCHITECTURE.md | 23 ++ docs/FIRETEST.md | 3 + locales/interface.yml | 24 +- 114 files changed, 904 insertions(+), 1243 deletions(-) delete mode 100644 crates/moon-ui-gpui/tests/theme_contract/density.rs diff --git a/crates/moon-core/src/config/mod.rs b/crates/moon-core/src/config/mod.rs index 7bcbaec5e..1b8ba072d 100644 --- a/crates/moon-core/src/config/mod.rs +++ b/crates/moon-core/src/config/mod.rs @@ -100,7 +100,7 @@ pub use layout::{ pub use news_tags::NewsTagSettings; pub use orders::{LineStyle, OrdersStyle, OrdersStyleSet}; pub use quiet::{QuietCfg, QuietWarnBypass}; -pub use schema::{TelegramConfig, UiDensity, UiThemeMode}; +pub use schema::{TelegramConfig, UiThemeMode}; pub use secrets::Secret; pub use servers::{ ChartBucket, CoreSortMode, FeedFlags, MANUAL_STRAT_SLOTS, ManualStratState, ServerConfig, @@ -120,12 +120,11 @@ use crate::market::MarketDataMode; /// Interface preferences readable WITHOUT any key, for a window shown before the config is open. /// /// The login window has to be painted before `servers.enc` can be decrypted, and everything it -/// needs is in plaintext `settings.toml` anyway. Exposing these four values keeps that window from +/// needs is in plaintext `settings.toml` anyway. Exposing these three values keeps that window from /// either flashing default colours or reaching into the config internals. #[derive(Clone, Copy, Debug)] pub struct PresentationPrefs { pub ui_theme_mode: UiThemeMode, - pub ui_density: UiDensity, pub ui_scale: f32, pub language: Language, } @@ -216,7 +215,6 @@ pub fn presentation_prefs() -> PresentationPrefs { let (settings, load) = store::read_settings(); PresentationPrefs { ui_theme_mode: schema::resolve_ui_theme_mode(settings.ui_theme_mode, load, profile_age()), - ui_density: settings.resolved_ui_density(), ui_scale: schema::repair_ui_scale(settings.ui_scale), language: settings.language, } @@ -293,11 +291,9 @@ pub struct AppConfig { pub log_to_file: bool, /// Log-file retention in days; 0 keeps everything (settings.toml). Defaults to 14. pub log_retention_days: u32, - /// Interface density; Standard retains the historical +3 text adjustment in the UI crate. - pub ui_density: UiDensity, /// Interface theme mode (plaintext settings.toml); Graphite shares the dark colour set. pub ui_theme_mode: UiThemeMode, - /// Overall UI geometry scale. Defaults to 1.0. + /// Window content zoom applied to every window. Defaults to 1.0. pub ui_scale: f32, /// Startup retained-history depth percentage passed to MoonProto. /// @@ -367,7 +363,6 @@ impl AppConfig { main_idle_close_secs: Default::default(), log_to_file: Default::default(), log_retention_days: Default::default(), - ui_density: Default::default(), ui_theme_mode: Default::default(), ui_scale: Default::default(), chart_memory_percent: Default::default(), @@ -467,7 +462,6 @@ impl AppConfig { main_idle_close_secs: merged.main_idle_close_secs, log_to_file: merged.log_to_file, log_retention_days: merged.log_retention_days, - ui_density: merged.ui_density, ui_theme_mode: merged.ui_theme_mode, ui_scale: merged.ui_scale, chart_memory_percent: merged.chart_memory_percent, @@ -521,7 +515,6 @@ impl AppConfig { cfg.chart_stack_height = schema::default_chart_stack_height(); cfg.log_to_file = true; cfg.log_retention_days = 14; - cfg.ui_density = UiDensity::default(); cfg.ui_theme_mode = UiThemeMode::default(); cfg.ui_scale = schema::default_ui_scale(); // The serde default is `true` while the field's `Default` is `false`; `save()` below @@ -549,7 +542,6 @@ impl AppConfig { cfg.chart_stack_height = schema::default_chart_stack_height(); cfg.log_to_file = true; cfg.log_retention_days = 14; - cfg.ui_density = UiDensity::default(); cfg.ui_theme_mode = UiThemeMode::default(); cfg.ui_scale = schema::default_ui_scale(); // The serde default is `true` while the field's `Default` is `false`; `save()` below @@ -579,7 +571,6 @@ impl AppConfig { chart_stack_height: schema::default_chart_stack_height(), log_to_file: true, log_retention_days: 14, - ui_density: meta.resolved_ui_density(), // A brand-new profile opens LIGHT; anyone else keeps exactly what they had. Reaching // this branch already proves `servers.enc` and both legacy configs are absent, but NOT // that `settings.toml` and `layout.toml` are — so the shared fact is asked rather than @@ -773,7 +764,6 @@ impl AppConfig { workspace_membership: WorkspaceMembership::default(), }) .collect(); - let ui_density = settings.resolved_ui_density(); let mut config = Self { servers, groups: Vec::new(), @@ -790,7 +780,6 @@ impl AppConfig { main_idle_close_secs: 0, log_to_file: settings.log_to_file, log_retention_days: settings.log_retention_days, - ui_density, ui_theme_mode: settings.ui_theme_mode, ui_scale: settings.ui_scale, chart_memory_percent: settings.chart_memory_percent, @@ -896,7 +885,6 @@ impl AppConfig { self.main_idle_close_secs, self.log_to_file, self.log_retention_days, - self.ui_density, self.ui_theme_mode, self.ui_scale, self.chart_memory_percent, @@ -1037,7 +1025,6 @@ impl AppConfig { 0, // main_idle_close_secs is behavioral. true, // Log settings are non-structural. 14, - UiDensity::default(), UiThemeMode::default(), schema::default_ui_scale(), schema::default_chart_memory_percent(), diff --git a/crates/moon-core/src/config/reconcile.rs b/crates/moon-core/src/config/reconcile.rs index 41adde0a4..82df267dc 100644 --- a/crates/moon-core/src/config/reconcile.rs +++ b/crates/moon-core/src/config/reconcile.rs @@ -10,7 +10,7 @@ use super::hotkeys::HotkeysConfig; use super::lang::Language; use super::schema::{ COREID_UID_VERSION, SCHEMA_VERSION, ServerEntry, ServerMeta, ServersFile, SettingsFile, - TelegramConfig, UiDensity, UiThemeMode, clamp_chart_memory_percent, clamp_chart_stack_height, + TelegramConfig, UiThemeMode, clamp_chart_memory_percent, clamp_chart_stack_height, repair_ui_scale, }; use super::servers::{self, CoreSortMode}; @@ -44,8 +44,6 @@ pub struct Merged { pub log_to_file: bool, /// Log-file retention period in days (0 = keep everything). pub log_retention_days: u32, - /// Explicit interface density, or Standard when the key is absent. - pub ui_density: UiDensity, /// Dark/light MoonUI theme. pub ui_theme_mode: UiThemeMode, /// Overall UI geometry scale. @@ -87,9 +85,7 @@ pub fn merge(sf: ServersFile, meta: SettingsFile, uid_floor: Option) -> Mer let mut next_uid = next_free_uid(&sf, &meta, uid_floor); // A counter that had to be raised is written back, so the repair survives a later boot on // which the stores cannot be read. - let mut dirty = meta.version < SCHEMA_VERSION - || next_uid.get() > meta.next_uid - || meta.ui_density.is_none(); + let mut dirty = meta.version < SCHEMA_VERSION || next_uid.get() > meta.next_uid; // Before v11, runtime CoreId was positional, so charts.json contains positional ids. let chart_core_remap_needed = meta.version < COREID_UID_VERSION; let language = meta.language; @@ -102,7 +98,6 @@ pub fn merge(sf: ServersFile, meta: SettingsFile, uid_floor: Option) -> Mer let main_idle_close_secs = meta.main_idle_close_secs; let log_to_file = meta.log_to_file; let log_retention_days = meta.log_retention_days; - let ui_density = meta.resolved_ui_density(); let ui_theme_mode = meta.ui_theme_mode; let ui_scale = repair_ui_scale(meta.ui_scale); let chart_memory_percent = clamp_chart_memory_percent(meta.chart_memory_percent); @@ -217,7 +212,6 @@ pub fn merge(sf: ServersFile, meta: SettingsFile, uid_floor: Option) -> Mer main_idle_close_secs, log_to_file, log_retention_days, - ui_density, ui_theme_mode, ui_scale, chart_memory_percent, @@ -236,7 +230,8 @@ pub fn merge(sf: ServersFile, meta: SettingsFile, uid_floor: Option) -> Mer /// Server connection keys go to `ServersFile`; server metadata, server-group settings, saved core /// groups, and presentation preferences go to `SettingsFile`. Saved core groups are copied as /// supplied because the caller owns sanitizing the runtime list before persistence. -/// Density is always written explicitly; the retired font adjustment is never written. +/// The retired `ui_density` and `ui_font_delta` keys are never written: a file that still carries +/// them loses them on its next save. #[allow(clippy::too_many_arguments)] pub fn split( servers: &[ServerConfig], @@ -252,7 +247,6 @@ pub fn split( main_idle_close_secs: u32, log_to_file: bool, log_retention_days: u32, - ui_density: UiDensity, ui_theme_mode: UiThemeMode, ui_scale: f32, chart_memory_percent: u16, @@ -284,7 +278,6 @@ pub fn split( main_idle_close_secs, log_to_file, log_retention_days, - ui_density: Some(ui_density), ui_theme_mode, ui_scale, chart_memory_percent: clamp_chart_memory_percent(chart_memory_percent), diff --git a/crates/moon-core/src/config/reconcile/tests.rs b/crates/moon-core/src/config/reconcile/tests.rs index 518069cb6..2849b1500 100644 --- a/crates/moon-core/src/config/reconcile/tests.rs +++ b/crates/moon-core/src/config/reconcile/tests.rs @@ -1,6 +1,5 @@ use super::super::schema::{ - SCHEMA_VERSION, ServersFile, SettingsFile, TelegramConfig, UiDensity, UiThemeMode, - default_ui_scale, + SCHEMA_VERSION, ServersFile, SettingsFile, TelegramConfig, UiThemeMode, default_ui_scale, }; use super::{Merged, merge, split}; use crate::config::{CoreGroup, DEFAULT_ORDER_SIZES_USD, GroupConfig, Language}; @@ -57,35 +56,36 @@ fn an_unusual_but_usable_scale_survives_the_load() { } /// Restoring legacy delta mapping during merge would resize upgraded users. -/// Every upgraded user starts at Standard regardless of their retired slider value: the -/// `ui_font_delta` key of an old file has no field any more and must stay inert on load. +/// The retired `ui_font_delta` and `ui_density` keys of an old file have no field any more and +/// must stay inert on load: the zoom beside them is what the interface uses. #[test] -fn legacy_font_delta_is_ignored_during_merge() { +fn retired_keys_are_ignored_during_merge() { for legacy in [ - "-2.0", "0.0", "0.01", "3.0", "3.01", "6.0", "inf", "-inf", "nan", + "ui_font_delta = -2.0", + "ui_font_delta = nan", + "ui_density = \"compact\"", + "ui_density = \"large\"\nui_font_delta = 6.0", ] { - let settings: SettingsFile = - toml::from_str(&format!("ui_scale = 1.0\nui_font_delta = {legacy}")).unwrap(); + let settings: SettingsFile = toml::from_str(&format!("ui_scale = 1.0\n{legacy}")).unwrap(); let merged = merge(ServersFile::default(), settings, None); - assert_eq!(merged.ui_density, UiDensity::Standard); + assert_eq!(merged.ui_scale, 1.0, "`{legacy}` must not touch the scale"); } } -/// Catches removing the density migration dirty bit or losing the choice in split. -/// Every upgraded user starts at Standard; a current-version file must write that choice -/// once and then stop migrating, without changing the independently stored zoom. +/// Catches a retired key regaining a migration dirty bit or a serialized field: a +/// current-version file that still carries them is not rewritten for their sake, and when it +/// is saved for another reason they are gone, with the zoom beside them intact. #[test] -fn density_migrates_through_the_persistence_pipeline_once() { +fn retired_keys_leave_the_persistence_pipeline_quietly() { let settings: SettingsFile = toml::from_str(&format!( - "version = {SCHEMA_VERSION}\nnext_uid = 1\nui_font_delta = 6.0\nui_scale = 1.25" + "version = {SCHEMA_VERSION}\nnext_uid = 1\nui_density = \"large\"\nui_font_delta = 6.0\nui_scale = 1.25" )) .unwrap(); let merged = merge(ServersFile::default(), settings, None); assert!( - merged.dirty, - "density migration must request a write even at the current schema" + !merged.dirty, + "a retired key is not a reason to rewrite a current-version file" ); - assert_eq!(merged.ui_density, UiDensity::Standard); let (servers, settings) = split( &merged.servers, &merged.groups, @@ -100,7 +100,6 @@ fn density_migrates_through_the_persistence_pipeline_once() { merged.main_idle_close_secs, merged.log_to_file, merged.log_retention_days, - merged.ui_density, merged.ui_theme_mode, merged.ui_scale, merged.chart_memory_percent, @@ -111,13 +110,9 @@ fn density_migrates_through_the_persistence_pipeline_once() { ); let saved = toml::to_string(&settings).unwrap(); assert!(!saved.contains("ui_font_delta")); - assert!(saved.contains("ui_density = \"standard\"")); + assert!(!saved.contains("ui_density")); let reloaded = merge(servers, toml::from_str(&saved).unwrap(), None); - assert!( - !reloaded.dirty, - "saved density must not trigger migration again" - ); - assert_eq!(reloaded.ui_density, UiDensity::Standard); + assert!(!reloaded.dirty, "a clean save reloads clean"); assert_eq!(reloaded.ui_scale, 1.25); } @@ -367,7 +362,6 @@ fn a_clean_core_group_list_round_trips_through_merge_and_split() { version: SCHEMA_VERSION, next_uid: 1, core_groups: groups.clone(), - ui_density: Some(UiDensity::Standard), ..Default::default() }; @@ -392,7 +386,6 @@ fn a_clean_core_group_list_round_trips_through_merge_and_split() { merged.main_idle_close_secs, merged.log_to_file, merged.log_retention_days, - merged.ui_density, merged.ui_theme_mode, merged.ui_scale, merged.chart_memory_percent, @@ -477,7 +470,6 @@ fn the_transport_survives_a_split() { 0, true, 14, - UiDensity::default(), UiThemeMode::default(), default_ui_scale(), 100, @@ -540,7 +532,6 @@ id = 2981", 0, true, 14, - UiDensity::default(), UiThemeMode::default(), default_ui_scale(), 100, diff --git a/crates/moon-core/src/config/schema.rs b/crates/moon-core/src/config/schema.rs index d88d9932e..cf2fda3f5 100644 --- a/crates/moon-core/src/config/schema.rs +++ b/crates/moon-core/src/config/schema.rs @@ -44,28 +44,18 @@ pub fn default_version() -> u32 { 0 } -/// User-selected interface density, mapped to MoonUI tiers by the UI crate. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum UiDensity { - Compact, - #[default] - Standard, - Large, -} - -/// Return the default UI geometry scale. +/// Return the default UI scale: the window content zoom every window opens at. pub fn default_ui_scale() -> f32 { 1.0 } /// Repair a stored UI scale on the way in, touching ONLY values that cannot mean anything. /// -/// `MoonScale::ui` multiplies control heights, gaps, paddings and hit areas, and MoonUI's -/// `MoonThemeTokens::ui` floors the factor at `0.25`. So a stored `0.0` does not blank the -/// interface — it renders everything at a quarter size, which still paints text at its own font -/// metric while shrinking every hit rectangle to the point where clicks stop landing. A -/// `settings.toml` written before the loader applied schema defaults holds exactly that. +/// The scale is the window content zoom (`MoonScale::zoom`). MoonUI's `set_zoom` replaces an +/// impossible value with `1.0` on install, so a stored `0.0` would not blank the interface — but +/// the slider and the saved file would keep showing a number the windows do not honour, and the +/// next save would write it back. A `settings.toml` written before the loader applied schema +/// defaults holds exactly that, so it is repaired where it is read. /// /// Only non-finite and non-positive values are repaired. There is deliberately NO upper or lower /// bound beyond that: hand-edited scales outside the Settings slider range remain valid. @@ -357,13 +347,11 @@ pub struct SettingsFile { /// Number of days to retain log files; older files are deleted. 0 keeps all. Defaults to 14. #[serde(default = "servers::default_log_retention_days")] pub log_retention_days: u32, - /// Selected density. None means a legacy file; writers always supply the resolved choice. - #[serde(default)] - pub ui_density: Option, /// Interface theme mode. Graphite shares dark colour data; this plaintext setting is not secret. #[serde(default)] pub ui_theme_mode: UiThemeMode, - /// Overall UI geometry scale, edited independently of density in Settings. + /// Window content zoom, the one UI scale control in Settings. A retired `ui_density` key in + /// an older file is ignored on load and not written back. #[serde(default = "default_ui_scale")] pub ui_scale: f32, /// Startup retained-history depth percentage passed to MoonProto. @@ -411,14 +399,5 @@ pub struct SettingsFile { pub servers: Vec, } -impl SettingsFile { - /// Return the explicit density, or Standard so every upgraded user starts at Standard. - /// - /// The retired font slider never influences density. - pub fn resolved_ui_density(&self) -> UiDensity { - self.ui_density.unwrap_or(UiDensity::Standard) - } -} - #[cfg(test)] mod tests; diff --git a/crates/moon-core/src/config/schema/tests.rs b/crates/moon-core/src/config/schema/tests.rs index 8878049b5..2f9af4453 100644 --- a/crates/moon-core/src/config/schema/tests.rs +++ b/crates/moon-core/src/config/schema/tests.rs @@ -1,44 +1,40 @@ -//! Compatibility tests for persisted history sizing, theme defaults and density migration. +//! Compatibility tests for persisted history sizing, theme defaults and retired keys. use moonproto::state::MarketHistorySizing; use super::*; -/// Restoring legacy delta thresholds would resize upgraded users away from Standard. -/// Every upgraded user starts at Standard unless an explicit density key overrides it. +/// Catches `SettingsFile` growing `deny_unknown_fields`, or a retired key regaining a field: +/// every profile written while `ui_density` and `ui_font_delta` existed must still load, and +/// the values it carries for them must not reach the interface. #[test] -fn density_ignores_legacy_values_and_respects_explicit_choices() { +fn retired_density_and_font_keys_are_ignored_on_load() { for legacy in [ - "-2.0", "0.0", "0.01", "3.0", "3.01", "6.0", "nan", "inf", "-inf", + "ui_font_delta = 6.0", + "ui_density = \"compact\"", + "ui_density = \"large\"\nui_font_delta = nan", ] { - let input = format!("ui_font_delta = {legacy}"); - let parsed: SettingsFile = toml::from_str(&input).unwrap(); - assert_eq!(parsed.resolved_ui_density(), UiDensity::Standard); - for (stored, density) in [ - ("compact", UiDensity::Compact), - ("standard", UiDensity::Standard), - ("large", UiDensity::Large), - ] { - let parsed: SettingsFile = - toml::from_str(&format!("{input}\nui_density = \"{stored}\"")).unwrap(); - assert_eq!(parsed.resolved_ui_density(), density); - } + let parsed: SettingsFile = toml::from_str(legacy) + .unwrap_or_else(|e| panic!("a file carrying `{legacy}` must still load: {e}")); + assert_eq!( + parsed.ui_scale, + default_ui_scale(), + "`{legacy}` must not touch the scale" + ); } - let missing: SettingsFile = toml::from_str("").unwrap(); - assert_eq!(missing.resolved_ui_density(), UiDensity::Standard); } -/// Catches serializing the retired key or restoring its mapping on save. -/// Every upgraded user starts at Standard, and the saved choice must keep that on reload. +/// Catches serializing either retired key again: a file that carried them loses them on its +/// next save, so the interface never reads a density it no longer has. #[test] -fn density_serialization_drops_the_legacy_key() { - let mut settings: SettingsFile = toml::from_str("ui_font_delta = 6.0").unwrap(); - settings.ui_density = Some(settings.resolved_ui_density()); +fn retired_keys_are_dropped_on_save() { + let settings: SettingsFile = + toml::from_str("ui_font_delta = 6.0\nui_density = \"large\"\nui_scale = 1.25").unwrap(); let saved = toml::to_string(&settings).unwrap(); assert!(!saved.contains("ui_font_delta")); - assert!(saved.contains("ui_density = \"standard\"")); + assert!(!saved.contains("ui_density")); let reloaded: SettingsFile = toml::from_str(&saved).unwrap(); - assert_eq!(reloaded.resolved_ui_density(), UiDensity::Standard); + assert_eq!(reloaded.ui_scale, 1.25); } /// The plausible production mutation is `config/schema.rs:clamp_chart_memory_percent`: restoring diff --git a/crates/moon-core/src/config/tests.rs b/crates/moon-core/src/config/tests.rs index 7343e0e4f..284ecae20 100644 --- a/crates/moon-core/src/config/tests.rs +++ b/crates/moon-core/src/config/tests.rs @@ -88,7 +88,6 @@ fn a_plaintext_config_keeps_the_settings_it_was_given() { let mut settings = super::schema::SettingsFile::default(); settings.charts_split_by_core = false; settings.chart_stack_height = 321; - settings.ui_density = Some(super::UiDensity::Large); let config = AppConfig::build_plaintext_config( None, @@ -106,7 +105,6 @@ fn a_plaintext_config_keeps_the_settings_it_was_given() { assert!(!config.charts_split_by_core); assert_eq!(config.chart_stack_height, 321); - assert_eq!(config.ui_density, super::UiDensity::Large); // …except the one thing a bench must never inherit: a Main chart that closes itself after a // quiet minute takes the screenshot with it. assert_eq!(config.main_idle_close_secs, 0); diff --git a/crates/moon-ui-gpui/src/analytics/toolbar.rs b/crates/moon-ui-gpui/src/analytics/toolbar.rs index 9889fb46d..7fe70978f 100644 --- a/crates/moon-ui-gpui/src/analytics/toolbar.rs +++ b/crates/moon-ui-gpui/src/analytics/toolbar.rs @@ -452,15 +452,10 @@ impl AnalyticsView { } // Title width plus the density-tier button's own pad_x on both sides, with a floor so // a short label still has a useful click target. - let tab_width = (design::ui_text_width_zoomed( - cx, - &title, - design::tier_font_size(cx), - 400.0, - false, - ) + 2.0 - * design::ui_value(cx, design::button_tier(cx).control_metrics().pad_x)) - .max(design::ui_value(cx, 72.0)); + let tab_width = + (design::ui_text_width_zoomed(cx, &title, design::BODY_TEXT, 400.0, false) + + 2.0 * design::ui_value(cx, design::CONTROL_TIER.control_metrics().pad_x)) + .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( @@ -532,7 +527,7 @@ impl AnalyticsView { // clipped. Density-tier triggers scale those design-reference widths through `tokens.ui` // at the button tier's control font, not the legacy 10.5px font channel. let action_trigger_scale = { - let font_size = design::button_tier(cx).control_metrics().font_size.max(1.0); + let font_size = design::CONTROL_TIER.control_metrics().font_size.max(1.0); design::ui_value(cx, font_size) / font_size }; let clear_core_filter_w = if workspace_pinned || self.sel_cores.is_empty() { @@ -604,7 +599,7 @@ impl AnalyticsView { .child( MoonInput::new("an-strategy-mask") .state(&self.strategy_mask_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ); @@ -1219,7 +1214,7 @@ impl AnalyticsView { 1.0 + design::ui_text_width_zoomed( cx, &custom_label, - design::tier_font_size(cx) - 2.0, + design::BODY_TEXT - 2.0, 400.0, false, ) + field_w * 2.0 diff --git a/crates/moon-ui-gpui/src/analytics/toolbar/tests.rs b/crates/moon-ui-gpui/src/analytics/toolbar/tests.rs index b6cf4bd85..12f0f4534 100644 --- a/crates/moon-ui-gpui/src/analytics/toolbar/tests.rs +++ b/crates/moon-ui-gpui/src/analytics/toolbar/tests.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; -use moon_core::config::{CoreGroup, UiDensity, UiThemeMode}; +use moon_core::config::{CoreGroup, UiThemeMode}; use moon_core::db::analytics::UndatedCloses; use moon_core::db::{QuoteBreakdown, QuoteCurrency, QuoteTotal, ReadFail}; @@ -16,14 +16,14 @@ use super::{ analytics_core_filter_ids, presets_row_fits, sole_core_name, undated_banner_state, }; -/// Read the font deltas from the same density mapping used when installing the UI theme. -fn density_font_deltas() -> [f32; 3] { - [UiDensity::Compact, UiDensity::Standard, UiDensity::Large].map(|density| { - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0) +/// Read the font delta from the same presentation mapping used when installing the UI theme. +fn presentation_font_deltas() -> [f32; 1] { + [ + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0) .dark .scale - .font_delta - }) + .font_delta, + ] } /// `analytics/toolbar.rs:presets_row_fits` must keep the inline presets at the exact available @@ -69,8 +69,8 @@ fn every_preset_label_fits_its_fitted_cell_without_truncation(cx: &mut gpui::Tes } // Every preset except the pre-existing, out-of-scope Week overflow must survive the supported - // density-derived font deltas, which is the failure class this regression is meant to catch. - for delta in density_font_deltas() { + // presentation font delta, which is the failure class this regression is meant to catch. + for delta in presentation_font_deltas() { let ceiling = cx.update(|cx| { moon_ui::MoonTheme::global_mut(cx).scale.font_delta = delta; moon_ui::MoonTheme::active_tokens(cx).font_width(super::PRESET_CELL_MAX_W) @@ -126,7 +126,7 @@ fn captioned_filter_labels_fit_without_ellipsis_in_every_locale(cx: &mut gpui::T ), ] { let _locale = crate::test_locale::force(locale); - for delta in density_font_deltas() { + for delta in presentation_font_deltas() { cx.update(|cx| moon_ui::MoonTheme::global_mut(cx).scale.font_delta = delta); for (label, width) in labels { let fitted = cx.update(|cx| { 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 9fdce8243..2e7b5e8cd 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/coins/mod.rs @@ -168,7 +168,7 @@ impl AnalyticsView { div().flex_1().min_w_0().child( MoonInput::new("an-coin-search") .state(search) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) // Right after the search: both narrow WHICH rows are worth looking at, so they 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 5f55718ec..1e7b6c1b5 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/filter/mod.rs @@ -505,7 +505,7 @@ impl AnalyticsView { // The master checkbox ignores unmapped fields (no matching // strategy parameter): 'all enabled' = all MAPPED ones. .checked(self.tuner.all_mapped_enabled()) - .size(design::choice_tier(cx)) + .size(design::CONTROL_TIER) .on_change({ let view = cx.entity(); move |ch: &bool, _w, app| { @@ -632,7 +632,7 @@ impl AnalyticsView { div().flex_none().child( MoonCheckbox::new(SharedString::from(format!("tun-en-{fi}"))) .checked(self.tuner.enabled[fi]) - .size(design::choice_tier(cx)) + .size(design::CONTROL_TIER) .on_change({ let view = cx.entity(); move |ch: &bool, _w, app| { @@ -773,7 +773,7 @@ impl AnalyticsView { div().w(design::font_w_px(cx, in_w)).flex_none().child( MoonInput::new(SharedString::from(format!("tun-in-{vi}-{fi}-{is_to}"))) .state(&input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ); } 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 b6a5ace50..bb1236171 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/list/mod.rs @@ -451,7 +451,7 @@ impl AnalyticsView { div().flex_1().min_w_0().child( MoonInput::new("an-strat-search") .state(&search) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(self.strat_type_menu(cx)) diff --git a/crates/moon-ui-gpui/src/analytics/tuner/save.rs b/crates/moon-ui-gpui/src/analytics/tuner/save.rs index a38f8657f..58bbb4e9f 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/save.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/save.rs @@ -828,7 +828,7 @@ impl AnalyticsView { div().flex_1().min_w_0().child( MoonInput::new("tun-copy-name") .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ), ); diff --git a/crates/moon-ui-gpui/src/analytics/tuner/shell.rs b/crates/moon-ui-gpui/src/analytics/tuner/shell.rs index a292c0ca3..a58d1e6d4 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/shell.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/shell.rs @@ -303,7 +303,7 @@ impl AnalyticsView { .child( MoonInput::new(SharedString::from("tun-cfg-it-f")) .state(&it_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(settings) @@ -654,7 +654,7 @@ impl AnalyticsView { .child( MoonInput::new(SharedString::from("tun-cfg-mn-f")) .state(&mn_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .into_any_element(), )) @@ -753,7 +753,7 @@ impl AnalyticsView { .child( MoonInput::new(SharedString::from("tun-cfg-seed-f")) .state(&seed_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .into_any_element(), )); @@ -817,7 +817,7 @@ impl AnalyticsView { .child( MoonInput::new(SharedString::from("tun-cfg-mn-t")) .state(&mn_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(div().flex_1()) 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 adf623931..152c846d6 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/time/grid.rs @@ -384,7 +384,7 @@ impl AnalyticsView { div().flex_none().child( MoonCheckbox::new("tt-en-all") .checked(self.time_tuner.enabled.iter().all(|&e| e)) - .size(design::choice_tier(cx)) + .size(design::CONTROL_TIER) .on_change({ let view = cx.entity(); move |ch: &bool, _w, app| { @@ -529,7 +529,7 @@ impl AnalyticsView { .child( MoonCheckbox::new(SharedString::from(format!("tt-en-{field}"))) .checked(enabled) - .size(design::choice_tier(cx)) + .size(design::CONTROL_TIER) .on_change({ let view = cx.entity(); move |ch: &bool, _w, app| { @@ -603,7 +603,7 @@ impl AnalyticsView { "tt-in-{vi}-{field}-{is_to}" ))) .state(&input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ); } diff --git a/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs b/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs index 6ce15a69e..4e6d69ead 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs @@ -447,7 +447,7 @@ impl AnalyticsView { .child( div().flex_none().invisible().child( MoonCheckbox::new(SharedString::from(format!("tt-slider-lead-{field}"))) - .size(design::choice_tier(cx)), + .size(design::CONTROL_TIER), ), ) .child( diff --git a/crates/moon-ui-gpui/src/chart_tabs/detached_host/render.rs b/crates/moon-ui-gpui/src/chart_tabs/detached_host/render.rs index b23fa1cdb..156ddd064 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/detached_host/render.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/detached_host/render.rs @@ -95,7 +95,7 @@ impl Render for DetachedChartHost { MoonInput::new("detached-coin-search") .state(&self.coin_input) .cleanable(true) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ); // A press on either control group beside the field ends an open search, and only while // there is one to end; see `common::coin_toolbar_press_handler`. Coverage here is the diff --git a/crates/moon-ui-gpui/src/chart_tabs/labels_popup/mod.rs b/crates/moon-ui-gpui/src/chart_tabs/labels_popup/mod.rs index 5b1f5a0dc..2e5d10cf2 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/labels_popup/mod.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/labels_popup/mod.rs @@ -93,7 +93,7 @@ pub(super) fn content_width(cx: &App) -> Pixels { /// The rendered pixels available to the label, never negative. pub(super) fn name_budget(cx: &App) -> f32 { (design::font_w(cx, NAME_W) - - 2.0 * design::ui_value(cx, design::button_tier(cx).control_metrics().pad_x)) + - 2.0 * design::ui_value(cx, design::CONTROL_TIER.control_metrics().pad_x)) .max(0.0) } diff --git a/crates/moon-ui-gpui/src/chart_tabs/labels_popup/rows.rs b/crates/moon-ui-gpui/src/chart_tabs/labels_popup/rows.rs index be14cbd6e..1c8932267 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/labels_popup/rows.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/labels_popup/rows.rs @@ -116,7 +116,7 @@ fn row_line( &row_display_name(row), &format!(" ·{}", row.used_parts()), name_budget(cx), - |text| design::ui_text_width_zoomed(cx, text, design::tier_font_size(cx), 400.0, true), + |text| design::ui_text_width_zoomed(cx, text, design::BODY_TEXT, 400.0, true), ); MoonButton::new(SharedString::from(format!("cl-open-{ix}"))) .label(label) diff --git a/crates/moon-ui-gpui/src/chart_tabs/layout_popup.rs b/crates/moon-ui-gpui/src/chart_tabs/layout_popup.rs index c271d8116..590bba7c9 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/layout_popup.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/layout_popup.rs @@ -264,7 +264,7 @@ where div().w(px(64.0)).child( MoonInput::new(SharedString::from(format!("{id}-input"))) .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(div().text_color(rgb(p.text_muted)).child("px")); @@ -342,7 +342,7 @@ where div().w(px(64.0)).child( MoonInput::new(SharedString::from(format!("{id}-min-slot"))) .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(div().text_color(rgb(p.text_muted)).child("px")), @@ -414,7 +414,7 @@ where div().w(px(64.0)).child( MoonInput::new(SharedString::from(format!("{id}-max-charts"))) .state(cap.max_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ); let cap_hint = div() @@ -509,7 +509,7 @@ where div().flex_1().child( MoonInput::new(SharedString::from(format!("{id}-name"))) .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) }); diff --git a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs index 2f7902b78..7bdf546b6 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs @@ -269,7 +269,7 @@ fn row( .min_w_0() .text_size(design::ui_px( cx, - crate::panels::common::checkbox_metrics(cx).font, + crate::panels::common::CHECKBOX_METRICS.font, )) .text_color(rgb(palette.text_soft)) .child(t!(block_label(block)).to_string()), diff --git a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs index 37de9feb9..1649d0d80 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs @@ -505,7 +505,7 @@ fn field_row( .flex_1() .text_size(design::ui_px( cx, - crate::panels::common::checkbox_metrics(cx).font, + crate::panels::common::CHECKBOX_METRICS.font, )) .text_color(if enabled { rgba_from(palette.text_soft, 1.0) @@ -518,7 +518,7 @@ fn field_row( div().w(design::ui_px(cx, FIELD_WIDTH)).child( MoonInput::new(id) .state(input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true) .disabled(!enabled), ), diff --git a/crates/moon-ui-gpui/src/chart_tabs/strip.rs b/crates/moon-ui-gpui/src/chart_tabs/strip.rs index b7494ae4b..54ec87fcb 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/strip.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/strip.rs @@ -409,7 +409,7 @@ impl Render for ChartTabs { MoonInput::new("tabs-coin-search") .state(&self.coin_input) .cleanable(true) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ); let fig_style_panel = self.render_fig_style_panel(cx).map(|panel| { div() diff --git a/crates/moon-ui-gpui/src/chartdx/data_state/state.rs b/crates/moon-ui-gpui/src/chartdx/data_state/state.rs index 6b38b6aa3..a63834875 100644 --- a/crates/moon-ui-gpui/src/chartdx/data_state/state.rs +++ b/crates/moon-ui-gpui/src/chartdx/data_state/state.rs @@ -79,6 +79,7 @@ impl ChartDataState { present_rate_candidate_hz: 0.0, present_rate_candidate_hits: 0, last_ppp: 1.0, + content_zoom: 1.0, slot_bounds: None, last_order_sig: u64::MAX, last_prepared_market_sig: u64::MAX, @@ -315,11 +316,18 @@ impl ChartDataState { /// Applies slot geometry from logical-pixel canvas bounds to the engine's size, origin, and /// pixel scale. `frame()` synchronously obtains it from the fork's `GpuFrameInfo`, keeping the /// own pass in the current slot. + /// + /// Two factors, on purpose. The slot is `bounds` content pixels times the frame's effective + /// factor in device pixels, so the target and its origin use that. The chart's own geometry — + /// line widths, candle outlines, axis gutters, captions — is sized by the platform factor + /// alone, so that UI zoom scales the chrome around the chart while the chart keeps the + /// monitor's density inside whatever slot remains. fn apply_slot_geometry(&mut self, info: &GpuFrameInfo) { if info.bounds.is_empty() { return; } let sf = info.scale_factor.max(0.1); + let zoom = info.content_zoom.max(0.1); let w = (f32::from(info.bounds.size.width) * sf).round().max(1.0) as u32; let h = (f32::from(info.bounds.size.height) * sf).round().max(1.0) as u32; let ox = f32::from(info.bounds.origin.x) * sf; @@ -333,11 +341,13 @@ impl ChartDataState { self.origin = (ox, oy); self.mark_view_dirty(); } - self.last_ppp = sf; + let ppp = (sf / zoom).max(0.1); + self.last_ppp = ppp; + self.content_zoom = zoom; self.slot_bounds = Some(info.bounds); let mut st = self.render.borrow_mut(); st.set_slot_origin(ox, oy); // The setter dirties and presents only when the value changes. - st.set_pixel_scale(sf); + st.set_pixel_scale(ppp); } pub(crate) fn set_market_source(&mut self, source: Option) -> bool { diff --git a/crates/moon-ui-gpui/src/chartdx/engine.rs b/crates/moon-ui-gpui/src/chartdx/engine.rs index a7dc13c0e..e3ab06864 100644 --- a/crates/moon-ui-gpui/src/chartdx/engine.rs +++ b/crates/moon-ui-gpui/src/chartdx/engine.rs @@ -212,6 +212,12 @@ impl ChartEngine { Some((data.slot_bounds?, data.last_ppp, (data.w, data.h))) } + /// The window's content zoom at the last frame: content pixels times this are the chart's own + /// logical pixels, which the overlays over chart geometry need to convert both ways. + pub fn slot_content_zoom(&self) -> f32 { + self.data.borrow().content_zoom + } + pub fn slot_dev_size(&self) -> (u32, u32) { let data = self.data.borrow(); (data.w.max(1), data.h.max(1)) @@ -413,7 +419,7 @@ impl ChartEngine { /// do their job. Guarded on a real change because render calls this every frame. /// /// Args: - /// ppp: Current device pixels per logical pixel. + /// ppp: Current device pixels per chart-design pixel (the platform factor, excluding UI zoom). pub fn set_last_ppp(&mut self, ppp: f32) { let ppp = ppp.max(0.1); { diff --git a/crates/moon-ui-gpui/src/chartdx/mod.rs b/crates/moon-ui-gpui/src/chartdx/mod.rs index dd03171c3..7efeae59d 100644 --- a/crates/moon-ui-gpui/src/chartdx/mod.rs +++ b/crates/moon-ui-gpui/src/chartdx/mod.rs @@ -1392,7 +1392,7 @@ pub(crate) struct PaneAreas { /// time_axis_visible: Whether the time axis reserves its gutter under every area. /// price_axis_pos: Configured per-tab price-axis position. /// hvol: The horizontal volumes' width, `None` when they are off. -/// pixel_scale: Device pixels per logical pixel. +/// pixel_scale: Device pixels per chart-design pixel (the platform factor, excluding UI zoom). /// /// Returns: /// The pane's [`PaneAreas`]. @@ -1629,7 +1629,13 @@ struct ChartDataState { last_frame_tick_at: Option, present_rate_candidate_hz: f32, present_rate_candidate_hits: u8, + /// Device pixels per chart-design pixel: the platform factor, excluding UI zoom. Every size + /// the chart draws — line widths, candle outlines, axis gutters, caption text — goes through + /// it, which is what keeps the chart at the monitor's density while the interface zooms. last_ppp: f32, + /// The window's content zoom at the last frame: content pixels times this are chart-design + /// pixels. Read by the overlays that place GPUI elements over chart geometry. + content_zoom: f32, slot_bounds: Option>, last_order_sig: u64, last_prepared_market_sig: u64, diff --git a/crates/moon-ui-gpui/src/chartdx/render_state/tests.rs b/crates/moon-ui-gpui/src/chartdx/render_state/tests.rs index ac6cbf37a..22c840213 100644 --- a/crates/moon-ui-gpui/src/chartdx/render_state/tests.rs +++ b/crates/moon-ui-gpui/src/chartdx/render_state/tests.rs @@ -86,6 +86,7 @@ fn frame_info() -> GpuFrameInfo { now: Instant::now(), bounds: Bounds::new(point(px(0.0), px(0.0)), size(px(320.0), px(200.0))), scale_factor: 1.0, + content_zoom: 1.0, presentable: true, } } diff --git a/crates/moon-ui-gpui/src/chartdx/text/captions.rs b/crates/moon-ui-gpui/src/chartdx/text/captions.rs index 07d5ace64..ef99016d1 100644 --- a/crates/moon-ui-gpui/src/chartdx/text/captions.rs +++ b/crates/moon-ui-gpui/src/chartdx/text/captions.rs @@ -27,7 +27,9 @@ //! Two different zones are never divided against each other — each stays inside its own band, which //! is the paragraph above. -use gpui::{Hsla, point, px}; +use gpui::{Hsla, point}; + +use super::{chart_metrics, content_px}; use moon_core::config::{ ARB_PART_BASE, CHART_LABEL_ROWS, ChartLabelField, ChartLabelRow, ChartLabelsCfg, FILTER_HEADER_PART, LABEL_WRAP_LINES, LabelAlign, LabelColor, LabelZone, PREFIX_PART_BASE, @@ -1509,16 +1511,14 @@ impl RenderState { self.caption_runs .resize_with(run_ix + 1, gpui::GpuCanvasTextRun::default); } - self.caption_runs[run_ix] - .measure( - ctx, - text, - gpui::font(crate::design::mono()), - px(size), - px(size + 4.0), - ) - .width - .as_f32() + let metrics = self.caption_runs[run_ix].measure( + ctx, + text, + gpui::font(crate::design::mono()), + content_px(ctx, size), + content_px(ctx, size + 4.0), + ); + chart_metrics(ctx, metrics).width.as_f32() } /// Draw one caption through its OWN retained run. @@ -1547,17 +1547,18 @@ impl RenderState { self.caption_runs .resize_with(run_ix + 1, gpui::GpuCanvasTextRun::default); } - self.caption_runs[run_ix].draw_aligned( + let metrics = self.caption_runs[run_ix].draw_aligned( ctx, - point(px(x), px(y)), + point(content_px(ctx, x), content_px(ctx, y)), text, gpui::font(crate::design::mono()), - px(size), - px(size + 4.0), + content_px(ctx, size), + content_px(ctx, size + 4.0), color, ax, 0.0, - ) + )?; + Ok(chart_metrics(ctx, metrics)) } /// Re-resolve one pane's captions from the values it currently holds. diff --git a/crates/moon-ui-gpui/src/chartdx/text/mod.rs b/crates/moon-ui-gpui/src/chartdx/text/mod.rs index 2cb02d3db..3b01d072e 100644 --- a/crates/moon-ui-gpui/src/chartdx/text/mod.rs +++ b/crates/moon-ui-gpui/src/chartdx/text/mod.rs @@ -222,6 +222,29 @@ fn ensure_text_run(runs: &mut Vec, cursor: usize) { } /// Draw the volume scale and cursor readout with their explicit size and weight. +/// Where the chart's text crosses into GPUI. +/// +/// The chart lays its text out in its own logical pixels — the platform's, so the chart keeps +/// device density under UI zoom — while GPUI shapes and places text in the window's content +/// pixels, which are those divided by the window's content zoom. Sizes and origins cross here on +/// the way in and measured widths cross back through [`chart_metrics`]; nothing else in the text +/// layer knows the zoom. +pub(in crate::chartdx) fn content_px(ctx: &GpuCanvasTextContext<'_>, chart_px: f32) -> Pixels { + px(chart_px / ctx.content_zoom().max(0.1)) +} + +/// The inverse of [`content_px`] for what GPUI measured, back into the chart's logical pixels. +pub(in crate::chartdx) fn chart_metrics( + ctx: &GpuCanvasTextContext<'_>, + metrics: GpuCanvasTextMetrics, +) -> GpuCanvasTextMetrics { + let zoom = ctx.content_zoom().max(0.1); + GpuCanvasTextMetrics { + width: metrics.width * zoom, + line_height: metrics.line_height * zoom, + } +} + fn draw_sized_text_run( runs: &mut Vec, cursor: &mut usize, @@ -239,17 +262,18 @@ fn draw_sized_text_run( ensure_text_run(runs, *cursor); let run = &mut runs[*cursor]; *cursor += 1; - run.draw_aligned( + let metrics = run.draw_aligned( ctx, - point(px(x), px(y)), + point(content_px(ctx, x), content_px(ctx, y)), text, mono_font(weight), - px(size), - px(line_h), + content_px(ctx, size), + content_px(ctx, line_h), color, ax, ay, - ) + )?; + Ok(chart_metrics(ctx, metrics)) } /// The measuring partner of [`draw_sized_text_run`], taking the same size and weight. @@ -263,7 +287,14 @@ fn measure_sized_text_run( weight: FontWeight, ) -> GpuCanvasTextMetrics { ensure_text_run(runs, cursor); - runs[cursor].measure(ctx, text, mono_font(weight), px(size), px(line_h)) + let metrics = runs[cursor].measure( + ctx, + text, + mono_font(weight), + content_px(ctx, size), + content_px(ctx, line_h), + ); + chart_metrics(ctx, metrics) } /// Draw ordinary chart text without the volume-specific size or weight adjustment. @@ -281,17 +312,18 @@ fn draw_text_run( ensure_text_run(runs, *cursor); let run = &mut runs[*cursor]; *cursor += 1; - run.draw_aligned( + let metrics = run.draw_aligned( ctx, - point(px(x), px(y)), + point(content_px(ctx, x), content_px(ctx, y)), text, gpui::font(crate::design::mono()), - px(FONT_SIZE), - px(LINE_H), + content_px(ctx, FONT_SIZE), + content_px(ctx, LINE_H), color, ax, ay, - ) + )?; + Ok(chart_metrics(ctx, metrics)) } /// Measure ordinary chart text using the same metrics as its draw path. @@ -302,13 +334,14 @@ fn measure_text_run( text: &str, ) -> GpuCanvasTextMetrics { ensure_text_run(runs, cursor); - runs[cursor].measure( + let metrics = runs[cursor].measure( ctx, text, gpui::font(crate::design::mono()), - px(FONT_SIZE), - px(LINE_H), - ) + content_px(ctx, FONT_SIZE), + content_px(ctx, LINE_H), + ); + chart_metrics(ctx, metrics) } /// One bottom-volume scale label, one step larger and SEMIBOLD; see [`VOLUME_SCALE_FONT_SIZE`]. @@ -356,17 +389,18 @@ fn draw_label_text_run( ensure_text_run(runs, *cursor); let run = &mut runs[*cursor]; *cursor += 1; - run.draw_aligned( + let metrics = run.draw_aligned( ctx, - point(px(x), px(y)), + point(content_px(ctx, x), content_px(ctx, y)), text, gpui::font(crate::design::mono()), - px(fp), - px(fp + 4.0), + content_px(ctx, fp), + content_px(ctx, fp + 4.0), color, ax, ay, - ) + )?; + Ok(chart_metrics(ctx, metrics)) } /// Measure ordinary order labels with the same mono face and size used for drawing. @@ -379,13 +413,14 @@ fn measure_label_text_run( ) -> GpuCanvasTextMetrics { let fp = label_font_px(label_font_delta); ensure_text_run(runs, cursor); - runs[cursor].measure( + let metrics = runs[cursor].measure( ctx, text, gpui::font(crate::design::mono()), - px(fp), - px(fp + 4.0), - ) + content_px(ctx, fp), + content_px(ctx, fp + 4.0), + ); + chart_metrics(ctx, metrics) } /// The cursor volume readout's own size, above the order-line label size by a fixed step. @@ -512,14 +547,12 @@ pub(in crate::chartdx) use labels::{ /// the size the caller actually draws at — truncating against a narrower font would underestimate /// the width and overflow anyway. fn measure_run_width(ctx: &GpuCanvasTextContext<'_>, text: &str, size: f32) -> f32 { - GpuCanvasTextRun::default() - .measure( - ctx, - text, - gpui::font(crate::design::mono()), - px(size), - px(size + 4.0), - ) - .width - .as_f32() + let metrics = GpuCanvasTextRun::default().measure( + ctx, + text, + gpui::font(crate::design::mono()), + content_px(ctx, size), + content_px(ctx, size + 4.0), + ); + chart_metrics(ctx, metrics).width.as_f32() } diff --git a/crates/moon-ui-gpui/src/chartdx/text/prepare.rs b/crates/moon-ui-gpui/src/chartdx/text/prepare.rs index 42bed1fcd..44f8a9bf3 100644 --- a/crates/moon-ui-gpui/src/chartdx/text/prepare.rs +++ b/crates/moon-ui-gpui/src/chartdx/text/prepare.rs @@ -27,7 +27,11 @@ impl RenderState { ctx: &mut GpuCanvasTextContext<'_>, ) -> anyhow::Result<()> { self.text_run_cursor = 0; - let sf = ctx.scale_factor().max(0.1); + // The text layer lays out in the chart's own logical pixels — the platform's, with the + // window's content zoom taken out of the frame's factor — so its captions keep device + // density under UI zoom like the rest of the chart. `content_px` crosses into GPUI's + // content pixels at the draw and measure calls. + let sf = (ctx.scale_factor() / ctx.content_zoom().max(0.1)).max(0.1); let ink = color(self.axis_label); let readout = color(self.readout_label); let label_neutral = color(self.label_neutral); diff --git a/crates/moon-ui-gpui/src/chartdx/text/runs.rs b/crates/moon-ui-gpui/src/chartdx/text/runs.rs index a6238bc28..b9535dfc3 100644 --- a/crates/moon-ui-gpui/src/chartdx/text/runs.rs +++ b/crates/moon-ui-gpui/src/chartdx/text/runs.rs @@ -1,6 +1,6 @@ //! `RenderState` draw/measure helpers, FireTest text, and ghost-crosshair labels. -use gpui::{GpuCanvasTextMetrics, Hsla, point, px}; +use gpui::{GpuCanvasTextMetrics, Hsla, point}; use super::*; @@ -218,13 +218,21 @@ impl RenderState { cold += 1; } builder.set_label_id(i as u32); + let (origin, font_size, line_h) = { + let ctx = builder.context(); + ( + point(content_px(ctx, x), content_px(ctx, y)), + content_px(ctx, FIRETEST_TEXT_FONT_SIZE), + content_px(ctx, FIRETEST_TEXT_LINE_H), + ) + }; run.draw( builder.context(), - point(px(x), px(y)), + origin, self.firetest_text_labels[i].as_str(), font.clone(), - px(FIRETEST_TEXT_FONT_SIZE), - px(FIRETEST_TEXT_LINE_H), + font_size, + line_h, color, )?; drawn += 1; diff --git a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs index 32bbdb061..2927c7662 100644 --- a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs +++ b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs @@ -798,7 +798,7 @@ fn core_selector( &raw_active_name, design::font_w(cx, design::HEADER_LABEL_MAX_W), design::ui_value(cx, 44.0), - |text| design::ui_text_width_zoomed(cx, text, design::tier_font_size(cx), 500.0, true), + |text| design::ui_text_width_zoomed(cx, text, design::BODY_TEXT, 500.0, true), ); let trigger_h = design::action_control_h_value(cx); // `MoonSelectorPill` takes `height` and `radius` in DESIGN units and scales them by @@ -871,7 +871,7 @@ fn core_selector( MoonSelectorSegment::new(active_name) .color(p.text) .weight(500.0) - .font_size(design::tier_font_base(cx, 0.0)), + .font_size(design::body_font_base(cx, 0.0)), ) .render(), ); diff --git a/crates/moon-ui-gpui/src/controls/core_group_dialogs.rs b/crates/moon-ui-gpui/src/controls/core_group_dialogs.rs index ac0bf6b5a..1baa79824 100644 --- a/crates/moon-ui-gpui/src/controls/core_group_dialogs.rs +++ b/crates/moon-ui-gpui/src/controls/core_group_dialogs.rs @@ -129,7 +129,7 @@ impl Render for SaveCoreGroup { .child( MoonInput::new("core-group-name") .state(&self.name) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) } } @@ -482,7 +482,7 @@ impl Render for ManageCoreGroups { div().w(px(220.0)).child( MoonInput::new(format!("core-group-row-{index}")) .state(&row.input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(div().flex_1().text_color(moon(p.text_muted)).child(facts)) diff --git a/crates/moon-ui-gpui/src/controls/manual_strat.rs b/crates/moon-ui-gpui/src/controls/manual_strat.rs index 0bc5b9aa4..b2e44528b 100644 --- a/crates/moon-ui-gpui/src/controls/manual_strat.rs +++ b/crates/moon-ui-gpui/src/controls/manual_strat.rs @@ -230,14 +230,14 @@ pub fn manual_strategy_controls( let name_w = design::ui_text_width_zoomed( cx, name_label, - design::tier_font_size(cx), + design::BODY_TEXT, BTN_TEXT_WEIGHT, true, ); let numeric_w = design::ui_text_width_zoomed( cx, numeric_label, - design::tier_font_size(cx), + design::BODY_TEXT, BTN_TEXT_WEIGHT, true, ); @@ -251,7 +251,7 @@ pub fn manual_strategy_controls( + design::fit_text( &full_pill_text, design::font_w(cx, design::HEADER_LABEL_MAX_W), - |s| design::ui_text_width_zoomed(cx, s, design::tier_font_size(cx), 500.0, true), + |s| design::ui_text_width_zoomed(cx, s, design::BODY_TEXT, 500.0, true), ) .1; // The parameter summary sits at the END of this cluster and is measured like the buttons @@ -322,7 +322,7 @@ pub fn manual_strategy_controls( design::font_w(cx, design::HEADER_LABEL_MAX_W) }; let display = design::fit_text(&full_pill_text, pill_cap, |s| { - design::ui_text_width_zoomed(cx, s, design::tier_font_size(cx), 500.0, true) + design::ui_text_width_zoomed(cx, s, design::BODY_TEXT, 500.0, true) }) .0; let dot_color = if on && sel_row.is_some() { @@ -428,7 +428,7 @@ pub fn manual_strategy_controls( MoonSelectorSegment::new(display) .color(if on { p.text } else { p.text_soft }) .weight(500.0) - .font_size(design::tier_font_base(cx, 0.0)), + .font_size(design::body_font_base(cx, 0.0)), ) .render() }) @@ -474,13 +474,8 @@ pub fn manual_strategy_controls( // An explicit width, so the caption sits inside the button's own padding instead of // against its border. Measured through the same helper the fit ladder used, so the // budget and the rendered button cannot disagree. - let label_w = design::ui_text_width_zoomed( - cx, - &label, - design::tier_font_size(cx), - BTN_TEXT_WEIGHT, - true, - ); + let label_w = + design::ui_text_width_zoomed(cx, &label, design::BODY_TEXT, BTN_TEXT_WEIGHT, true); let btn_w = design::ui_value(cx, BTN_CHROME_W) + label_w; let mut btn = MoonButton::new(SharedString::from(format!("ms-btn-{i}"))) diff --git a/crates/moon-ui-gpui/src/controls/metric.rs b/crates/moon-ui-gpui/src/controls/metric.rs index 2ccf1451a..7fd40232f 100644 --- a/crates/moon-ui-gpui/src/controls/metric.rs +++ b/crates/moon-ui-gpui/src/controls/metric.rs @@ -715,7 +715,7 @@ pub fn metric_popup_content( div().w(px(72.0)).child( MoonInput::new(SharedString::from(format!("{}-input", metric.id()))) .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(div().text_color(rgb(p.text_muted)).child(metric.unit())), diff --git a/crates/moon-ui-gpui/src/controls/strips.rs b/crates/moon-ui-gpui/src/controls/strips.rs index a00540e0e..a04b76eb5 100644 --- a/crates/moon-ui-gpui/src/controls/strips.rs +++ b/crates/moon-ui-gpui/src/controls/strips.rs @@ -177,7 +177,6 @@ pub(super) fn sell_labels(pcts: [f64; 6]) -> [String; 6] { /// when the displayed core and the write target disagree (goal A2 FIX-3), so a click, /// double-click, or wheel step cannot mutate a source other than the one on screen. /// unit: USDT-equivalent unit shown in tooltips. -/// cx: Application context used to select the inline editor's density tier. /// /// Returns: /// The configured segmented control. @@ -189,7 +188,6 @@ pub(super) fn size_strip( backend: Entity, group: Option, unit: &str, - cx: &App, ) -> impl IntoElement { let interactive = group.is_some(); let unit = unit.to_string(); @@ -251,7 +249,7 @@ pub(super) fn size_strip( index, MoonInput::new("toolbar-size-edit") .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ); } segment.render() @@ -271,7 +269,6 @@ pub(super) fn size_strip( /// backend: Application state receiving edits. /// group: Interactive group; absence disables the strip — also used when the displayed core /// and the write target disagree (goal A2 FIX-3). -/// cx: Application context used to select the inline editor's density tier. /// /// Returns: /// The configured segmented control. @@ -282,7 +279,6 @@ pub(super) fn sell_strip( input: &Entity, backend: Entity, group: Option, - cx: &App, ) -> impl IntoElement { let interactive = group.is_some(); let items = (0..6).map(|index| { @@ -356,7 +352,7 @@ pub(super) fn sell_strip( index, MoonInput::new("toolbar-sell-edit") .state(input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ); } segment.render() diff --git a/crates/moon-ui-gpui/src/controls/toolbar.rs b/crates/moon-ui-gpui/src/controls/toolbar.rs index 8dec6c246..c1a3fcdd3 100644 --- a/crates/moon-ui-gpui/src/controls/toolbar.rs +++ b/crates/moon-ui-gpui/src/controls/toolbar.rs @@ -30,7 +30,7 @@ const SELL_CAPTION_GAP: f32 = 8.0; /// Caption for a preset group, muted, sharing the density-tier body size with the chips beside it. /// -/// `MoonText` with [`design::tier_text_metrics`] rather than a hand-rolled text div: it applies +/// `MoonText` with [`design::text_metrics`] rather than a hand-rolled text div: it applies /// the theme's mono family and already-scaled metrics, so the caption lands on the same size and /// baseline as the neighbouring density-tier controls. Colour is the remaining caption cue. /// @@ -60,7 +60,7 @@ fn strip_text(text: impl Into, color: u32, cx: &App) -> impl IntoE .mono(true) .color(color) .uppercase(false) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 11.0)) .render(), ) } @@ -152,7 +152,7 @@ fn launcher_label_width(cx: &App, label: &str) -> f32 { let text = design::ui_text_width_zoomed( cx, label, - design::tier_font_size(cx), + design::BODY_TEXT, TOOLBAR_LAUNCHER_TEXT_WEIGHT, false, ); @@ -327,7 +327,7 @@ fn row_fit( // The exchange max-order VALUE is permanent — outcome 4 asks for a readout that is always // on the row — so it belongs in the unsheddable budget rather than on the ladder. Measured // from the REAL rendered string: a coin's cap runs from three digits to nine. - + design::ui_text_width_zoomed(cx, max_order_value, design::tier_font_size(cx), 400.0, true); + + design::ui_text_width_zoomed(cx, max_order_value, design::BODY_TEXT, 400.0, true); // Seven 1px rules — the hairline is deliberately NOT font-scaled (see `design::vline`). Pinned // against the row itself by `toolbar_row_budget_counts_every_rule_it_draws` in // `tests/theme_contract/shell.rs`: adding a section here without updating this count is invisible @@ -347,9 +347,8 @@ fn row_fit( let gaps = gap * 21.0; let base = design::ui_value(cx, design::HEADER_PAD_X) * 2.0 + controls + rules + gaps; // A caption costs its own width plus the gap separating it from its strip. - let caption_w = |text: &str| { - design::ui_text_width_zoomed(cx, text, design::tier_font_size(cx), 400.0, true) + gap - }; + let caption_w = + |text: &str| design::ui_text_width_zoomed(cx, text, design::BODY_TEXT, 400.0, true) + gap; let full_caption = size_caption_text(); let unit_caption_width = caption_w(SIZE_UNIT); let full_caption_width = caption_w(&full_caption); @@ -974,7 +973,6 @@ pub fn toolbar( // than the one this row just showed. write_matches_display.then(|| group.to_string()), SIZE_UNIT, - cx, ), manual_block_tip.clone(), design::CHROME_GAP, @@ -1074,7 +1072,6 @@ pub fn toolbar( // sell price. A displayed core disagreeing with the write target disables it too // (goal A2 FIX-3). (!manual_on && write_matches_display).then(|| group.to_string()), - cx, ); let sell_block = captioned_strip( "toolbar-sell-caption", diff --git a/crates/moon-ui-gpui/src/controls/toolbar/tests.rs b/crates/moon-ui-gpui/src/controls/toolbar/tests.rs index 9c18514f4..1e51c31ac 100644 --- a/crates/moon-ui-gpui/src/controls/toolbar/tests.rs +++ b/crates/moon-ui-gpui/src/controls/toolbar/tests.rs @@ -4,7 +4,7 @@ use super::{ ICON_BTN_W, LabelLadder, LabelWidths, TOOLBAR_LAUNCHER_BORDER_W, TOOLBAR_LAUNCHER_PAD_X, label_ladder, launcher_label_width, manual_strategy_core, }; -use moon_core::config::{UiDensity, UiThemeMode}; +use moon_core::config::UiThemeMode; use moon_ui::MoonButtonSize; #[test] @@ -65,38 +65,31 @@ fn hovered_chart_core_controls_manual_strategy_applicability() { /// `controls/toolbar.rs:launcher_label_width` must budget the same tier font and gap it draws. /// /// Breakage: measuring launcher text with the old font channel or a stale icon gap makes the -/// Compact window buttons overlap their labels before row-fit can shed a caption. +/// window buttons overlap their labels before row-fit can shed a caption. #[gpui::test] fn launcher_label_width_matches_the_drawn_tier_geometry(cx: &mut gpui::TestAppContext) { - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - cx.update(|cx| { - moon_ui::MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0), - cx, - ); - }); - let (actual, expected) = cx.update(|cx| { - let tier = crate::design::button_tier(cx); - let metrics = tier.control_metrics(); - let text = crate::design::ui_text_width_zoomed( - cx, - "Settings", - metrics.font_size, - 500.0, - false, - ); - let chrome = - crate::design::ui_value(cx, MoonButtonSize::tier_icon_size(tier) + metrics.gap) - + crate::design::ui_value(cx, TOOLBAR_LAUNCHER_PAD_X) * 2.0 - + TOOLBAR_LAUNCHER_BORDER_W; - ( - launcher_label_width(cx, "Settings"), - (text + chrome).max(ICON_BTN_W), - ) - }); - assert_eq!( - actual, expected, - "{density:?}: launcher budget must equal drawn geometry" + cx.update(|cx| { + moon_ui::MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), + cx, ); - } + }); + let (actual, expected) = cx.update(|cx| { + let tier = crate::design::CONTROL_TIER; + let metrics = tier.control_metrics(); + let text = + crate::design::ui_text_width_zoomed(cx, "Settings", metrics.font_size, 500.0, false); + let chrome = + crate::design::ui_value(cx, MoonButtonSize::tier_icon_size(tier) + metrics.gap) + + crate::design::ui_value(cx, TOOLBAR_LAUNCHER_PAD_X) * 2.0 + + TOOLBAR_LAUNCHER_BORDER_W; + ( + launcher_label_width(cx, "Settings"), + (text + chrome).max(ICON_BTN_W), + ) + }); + assert_eq!( + actual, expected, + "launcher budget must equal drawn geometry" + ); } diff --git a/crates/moon-ui-gpui/src/controls/wrap_fit.rs b/crates/moon-ui-gpui/src/controls/wrap_fit.rs index f8397703f..ce1489e54 100644 --- a/crates/moon-ui-gpui/src/controls/wrap_fit.rs +++ b/crates/moon-ui-gpui/src/controls/wrap_fit.rs @@ -209,7 +209,7 @@ pub(crate) fn signature(cx: &App, composition: impl Hash) -> u64 { // compact trigger actually draws, not a hand-kept legacy font-size constant. design::text_metrics_key_zoomed( cx, - design::button_tier(cx).control_metrics().font_size, + design::CONTROL_TIER.control_metrics().font_size, 400.0, true, ) @@ -290,7 +290,7 @@ pub(crate) fn compact_design_floor(cx: &App, all_word: &str) -> f32 { /// The rendered width a design-reference width becomes on a density-tier trigger. /// -/// Scales by `ui(font_size)/font_size` at [`design::button_tier`]'s control font — the same `ui` +/// Scales by `ui(font_size)/font_size` at [`design::CONTROL_TIER`]'s control font — the same `ui` /// channel the button draws through. The Font-slider `font()` scale that [`design::font_w`] uses /// is a different number the moment the slider leaves zero. pub(crate) fn action_width(cx: &App, design_w: f32) -> f32 { @@ -299,7 +299,7 @@ pub(crate) fn action_width(cx: &App, design_w: f32) -> f32 { /// The factor a density-tier trigger applies to design-reference widths (`tokens.ui`). fn action_scale(cx: &App) -> f32 { - let font_size = design::button_tier(cx).control_metrics().font_size.max(1.0); + let font_size = design::CONTROL_TIER.control_metrics().font_size.max(1.0); design::ui_value(cx, font_size) / font_size } diff --git a/crates/moon-ui-gpui/src/core_expert/pages/autobuy.rs b/crates/moon-ui-gpui/src/core_expert/pages/autobuy.rs index 9807ad1e4..7ebcab87e 100644 --- a/crates/moon-ui-gpui/src/core_expert/pages/autobuy.rs +++ b/crates/moon-ui-gpui/src/core_expert/pages/autobuy.rs @@ -375,7 +375,7 @@ pub(super) fn body( view, |d, on| d.auto_buy.use_keywords = on, )) - .children(field(store, "exp-buy-long-words", true, cx)) + .children(field(store, "exp-buy-long-words", true)) .child(caption( t!("core_expert.buy_word_gap", v = b.buy_key_dist.to_string()).to_string(), true, @@ -391,7 +391,7 @@ pub(super) fn body( view, |d, on| d.auto_buy.use_black_words = on, )) - .children(field(store, "exp-buy-black-words", true, cx)) + .children(field(store, "exp-buy-black-words", true)) .child( h_flex() .w_full() @@ -422,7 +422,7 @@ pub(super) fn body( view, |d, on| d.auto_buy.use_lower_price_words = on, )) - .children(field(store, "exp-buy-dip-words", true, cx)) + .children(field(store, "exp-buy-dip-words", true)) .child(caption( t!( "core_expert.buy_market_price", @@ -454,7 +454,7 @@ pub(super) fn body( p, cx, )) - .children(field(store, "exp-buy-short-words", true, cx)) + .children(field(store, "exp-buy-short-words", true)) .child(flag( "exp-buy-tags-on", t!("core_expert.buy_tags").to_string(), @@ -463,7 +463,7 @@ pub(super) fn body( view, |d, on| d.auto_buy.use_token_tags = on, )) - .children(field(store, "exp-buy-tags", true, cx)) + .children(field(store, "exp-buy-tags", true)) .child( h_flex() .w_full() diff --git a/crates/moon-ui-gpui/src/core_expert/pages/general.rs b/crates/moon-ui-gpui/src/core_expert/pages/general.rs index e40281102..628bf3915 100644 --- a/crates/moon-ui-gpui/src/core_expert/pages/general.rs +++ b/crates/moon-ui-gpui/src/core_expert/pages/general.rs @@ -349,7 +349,7 @@ pub(super) fn body( |d, on| d.general.exclude_blacklisted_from_deltas = on, )), ) - .children(field(store, "exp-gen-bl-text", true, cx)) + .children(field(store, "exp-gen-bl-text", true)) .child(caption( t!( "core_expert.gen_bl_fresh", diff --git a/crates/moon-ui-gpui/src/core_expert/pages/login.rs b/crates/moon-ui-gpui/src/core_expert/pages/login.rs index 7981c2d73..3147f81aa 100644 --- a/crates/moon-ui-gpui/src/core_expert/pages/login.rs +++ b/crates/moon-ui-gpui/src/core_expert/pages/login.rs @@ -94,10 +94,10 @@ pub(super) fn body( .child(link("exp-log-hmac", "HMAC".to_string(), false)) .child(link("exp-log-rsa", "RSA".to_string(), false)), ) - .children(field(store, "exp-log-api-key", false, cx)) + .children(field(store, "exp-log-api-key", false)) .child(labeled( t!("core_expert.log_api_secret").to_string(), - field(store, "exp-log-api-secret", false, cx), + field(store, "exp-log-api-secret", false), false, p, cx, @@ -109,14 +109,14 @@ pub(super) fn body( .gap(design::ui_px(cx, 10.0)) .child(div().flex_1().min_w_0().child(labeled( t!("core_expert.log_your_name").to_string(), - field(store, "exp-log-name", false, cx), + field(store, "exp-log-name", false), false, p, cx, ))) .child(div().flex_1().min_w_0().child(labeled( t!("core_expert.log_telegram").to_string(), - field(store, "exp-log-telegram", false, cx), + field(store, "exp-log-telegram", false), false, p, cx, @@ -135,14 +135,14 @@ pub(super) fn body( )) .child(labeled( t!("core_expert.log_password").to_string(), - field_masked(store, "exp-log-password", false, true, cx), + field_masked(store, "exp-log-password", false, true), false, p, cx, )) .child(labeled( t!("core_expert.log_password_again").to_string(), - field_masked(store, "exp-log-password2", false, true, cx), + field_masked(store, "exp-log-password2", false, true), false, p, cx, diff --git a/crates/moon-ui-gpui/src/core_expert/pages/special.rs b/crates/moon-ui-gpui/src/core_expert/pages/special.rs index 847fe5d1c..f824092a2 100644 --- a/crates/moon-ui-gpui/src/core_expert/pages/special.rs +++ b/crates/moon-ui-gpui/src/core_expert/pages/special.rs @@ -643,7 +643,6 @@ pub(super) fn body( store, "exp-sp-no-trades", false, - cx, ))), ) }; @@ -695,7 +694,7 @@ pub(super) fn body( "core_expert.sp_own_bot_id", view, )) - .children(field(store, "exp-sp-bot-token", false, cx)), + .children(field(store, "exp-sp-bot-token", false)), ) .child( v_flex() @@ -703,7 +702,7 @@ pub(super) fn body( .min_w_0() .gap(design::ui_px(cx, 2.0)) .child(hint(t!("core_expert.sp_type_pin").to_string(), p, cx)) - .children(field(store, "exp-sp-pin", false, cx)), + .children(field(store, "exp-sp-pin", false)), ) .child( v_flex() @@ -711,7 +710,7 @@ pub(super) fn body( .min_w_0() .gap(design::ui_px(cx, 2.0)) .child(hint(t!("core_expert.sp_this_bot_name").to_string(), p, cx)) - .children(field(store, "exp-sp-bot-name", false, cx)), + .children(field(store, "exp-sp-bot-name", false)), ), ) .child( @@ -1022,7 +1021,6 @@ pub(super) fn body( store, "exp-sp-skip-balances", !sp.h_pos_black_list_text.contains(['\r', '\n']), - cx, ))), ) .child(text_block( diff --git a/crates/moon-ui-gpui/src/core_expert/pages/telegram.rs b/crates/moon-ui-gpui/src/core_expert/pages/telegram.rs index 13c1e8518..f06a5b2d0 100644 --- a/crates/moon-ui-gpui/src/core_expert/pages/telegram.rs +++ b/crates/moon-ui-gpui/src/core_expert/pages/telegram.rs @@ -265,7 +265,7 @@ pub(super) fn body( .child( div() .flex_1() - .children(field(store, "exp-tlg-add-channel", true, cx)), + .children(field(store, "exp-tlg-add-channel", true)), ), ) .child( diff --git a/crates/moon-ui-gpui/src/core_expert/widgets.rs b/crates/moon-ui-gpui/src/core_expert/widgets.rs index 43c88022d..ef61cd934 100644 --- a/crates/moon-ui-gpui/src/core_expert/widgets.rs +++ b/crates/moon-ui-gpui/src/core_expert/widgets.rs @@ -171,7 +171,7 @@ fn text_at( div().flex_none().child( MoonText::new(text) .color(color) - .rendered_metrics(design::tier_text_metrics(cx, step, 15.0)) + .rendered_metrics(design::text_metrics(cx, step, 15.0)) .weight(if bold { 600.0 } else { 400.0 }) .uppercase(false) .render(), @@ -189,7 +189,7 @@ pub(super) fn text_block( div().w_full().min_w_0().child( MoonText::new(text) .color(color) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 15.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 15.0)) .weight(if bold { 600.0 } else { 400.0 }) .uppercase(false) .wrap() @@ -229,7 +229,6 @@ pub(super) fn slider( /// store: Editor-state owner that may provide the field's input state. /// id: Page declaration key for the field. /// enabled: Whether the mirrored page permits interaction. -/// cx: Application context used to select the input's density tier. /// /// Returns: /// The configured field when the page declared its input state. @@ -237,9 +236,8 @@ pub(super) fn field( store: &EditorStore, id: &'static str, enabled: bool, - cx: &App, ) -> Option { - field_masked(store, id, enabled, false, cx) + field_masked(store, id, enabled, false) } /// Build the same field, optionally masked — Moonbot's password boxes show dots, and a mirrored @@ -250,7 +248,6 @@ pub(super) fn field( /// id: Page declaration key for the field. /// enabled: Whether the mirrored page permits interaction. /// masked: Whether the field must hide its entered characters. -/// cx: Application context used to select the input's density tier. /// /// Returns: /// The configured field when the page declared its input state. @@ -259,7 +256,6 @@ pub(super) fn field_masked( id: &'static str, enabled: bool, masked: bool, - cx: &App, ) -> Option { let state = store.input(id)?; // Emptied by the view when mixed (see `build_editors`); here it only says why it is empty. @@ -268,7 +264,7 @@ pub(super) fn field_masked( div().w_full().child( MoonInput::new(id) .state(&state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .disabled(!enabled) .when(mixed, |input| { input @@ -648,7 +644,7 @@ pub(super) fn num( div().flex_none().w(design::ui_px(cx, width)).child( MoonInput::new(id) .state(&state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .disabled(!enabled) // Too narrow for the placeholder: the tone and the emptiness say it. .when(mixed, |input| input.tone(MoonTone::Warning).selected(true)), diff --git a/crates/moon-ui-gpui/src/design.rs b/crates/moon-ui-gpui/src/design.rs index 5bc96432f..97de8df8a 100644 --- a/crates/moon-ui-gpui/src/design.rs +++ b/crates/moon-ui-gpui/src/design.rs @@ -2,6 +2,13 @@ //! //! This is a thin GPUI-div adapter over MoonPalette tokens. Keep it visual-only: //! no terminal logic, no chart renderer state. +//! +//! The terminal renders ONE design at ONE size system: ordinary controls on [`CONTROL_TIER`], +//! dense strips pinned to `MoonSize::Xs`, text at [`BODY_TEXT`] plus a local step. The user scales +//! that design as a whole through the window content zoom (`MoonScale::zoom`, installed by +//! `startup::moon_theme_config_for_presentation`), which multiplies every pixel a window draws. +//! MoonUI's own token multipliers stay at 1.0, so [`ui_px`] is an identity adapter kept for the +//! components that read the tokens, and nothing here asks which size the user picked. use gpui::prelude::FluentBuilder; use gpui::*; @@ -94,23 +101,20 @@ pub const DISCLOSURE_BOX: f32 = 12.0; /// Height of the toolbar strip. /// -/// The one home of the toolbar's tier-derived band, matching [`header_height`] and the -/// `table_row_h`/`table_head_h` pair below. It reads MoonUI's `tier_band_base` height metric, -/// so Compact uses its rebased band while Standard renders today's number exactly and Large keeps -/// its established value. Centralizing the call prevents callers that size or position adjacent -/// chrome from drifting away from the row that is actually rendered. +/// The one home of the toolbar's band, matching [`header_height`] and the +/// `table_row_h`/`table_head_h` pair below: MoonUI's `fit_band` over the design's base height, +/// so the band grows with the font delta exactly as the row it holds. Centralizing the call +/// prevents callers that size or position adjacent chrome from drifting away from the row that is +/// actually rendered. pub fn toolbar_height(cx: &App) -> f32 { - let t = MoonTheme::active_tokens(cx); - t.fit_band(t.tier_band_base(TOOLBAR_H, |m| m.height), 13.0) + MoonTheme::active_tokens(cx).fit_band(TOOLBAR_H, 13.0) } -/// Height of the window header strip — the companion to [`toolbar_height`]. It reads MoonUI's -/// `tier_band_base` height metric, so Standard renders today's number exactly while Compact and -/// Large use their respective tier bands. Drifting the two apart is exactly the failure this -/// shared shape exists to prevent. +/// Height of the window header strip — the companion to [`toolbar_height`], the same `fit_band` +/// over the header's base height. Drifting the two apart is exactly the failure this shared shape +/// exists to prevent. pub fn header_height(cx: &App) -> f32 { - let t = MoonTheme::active_tokens(cx); - t.fit_band(t.tier_band_base(HEADER_TOP_H, |m| m.height), 14.0) + MoonTheme::active_tokens(cx).fit_band(HEADER_TOP_H, 14.0) } /// [`header_height`] as `Pixels`, for the row that draws itself. @@ -382,25 +386,25 @@ pub const COLUMN_SELECTOR_ICON: &str = "icons/layout-dashboard.svg"; /// the height by `ui()` (a pure multiply), so the two diverge as soon as that delta leaves zero. /// /// Reads [`action_control_h_value`]: a square control matches the ordinary button height at the -/// app's density tier. +/// design's control tier. pub fn glyph_btn_w(cx: &App) -> f32 { action_control_h_value(cx) } -/// Rendered size of a density-tier button's leading glyph. +/// Rendered size of a control-tier button's leading glyph. /// -/// Reads `MoonButtonSize::tier_icon_size` for [`button_tier`]. `MoonButton` and `MoonDropdown` +/// Reads `MoonButtonSize::tier_icon_size` for [`CONTROL_TIER`]. `MoonButton` and `MoonDropdown` /// apply it themselves; this exists for the callers that draw their own trigger content and must /// leave the component's room for it. pub fn action_icon_px(cx: &App) -> f32 { - ui_value(cx, MoonButtonSize::tier_icon_size(button_tier(cx))) + ui_value(cx, MoonButtonSize::tier_icon_size(CONTROL_TIER)) } -/// Rendered width a density-tier leading glyph takes, including the gap after it. +/// Rendered width a control-tier leading glyph takes, including the gap after it. /// -/// The gap is the density tier's `control_metrics().gap`; see [`action_icon_px`] for the glyph. +/// The gap is [`CONTROL_TIER`]'s `control_metrics().gap`; see [`action_icon_px`] for the glyph. pub fn action_icon_reservation(cx: &App) -> f32 { - action_icon_px(cx) + ui_value(cx, button_tier(cx).control_metrics().gap) + action_icon_px(cx) + ui_value(cx, CONTROL_TIER.control_metrics().gap) } /// Ceiling for a header selector label (core, manual strategy). @@ -663,13 +667,31 @@ pub fn ui_font() -> SharedString { SharedString::from("Inter") } +// ---- The design's fixed size system ----------------------------------------------------------- + +/// The tier the design's ordinary controls render at: buttons, inputs, checkboxes and the chrome +/// bands derive their metrics from it. Dense strips pin `MoonSize::Xs` themselves. +pub const CONTROL_TIER: MoonSize = MoonSize::Sm; + +/// The design's text adjustment on MoonUI's font channel: `tokens.font(v)` renders `v + 3`, which +/// is what the reviewed design was measured against. Installed into the theme once. +pub const DESIGN_FONT_DELTA: f32 = 3.0; + +/// The body text size in design pixels: [`CONTROL_TIER`]'s control font. [`t_caption`], +/// [`t_body`], [`t_body_lg`] and [`t_title`] are this plus their step. +pub const BODY_TEXT: f32 = CONTROL_TIER.control_metrics().font_size; + +/// The `MoonInput` size the terminal's fixed-small inputs carry: `Small` (22/10/13), the size the +/// reviewed design measured every such field at. +pub const INPUT_SIZE: MoonInputSize = MoonInputSize::Small; + pub fn ui_value(cx: &App, value: f32) -> f32 { MoonTheme::active_tokens(cx).ui(value) } /// MoonUI-INTERNAL legacy font channel: `value * ui_scale + font_delta`. /// -/// This is NOT the terminal's text channel any more — that is [`tier_text_value`]. The only +/// This is NOT the terminal's text channel any more — that is [`t_body`]. The only /// legitimate remaining use is mirroring a formula MoonUI itself computes; its one such caller is /// `settings/connections/table.rs` / `columns.rs`, which reproduces MoonUI's own `Scaled` /// dropdown-trigger width. @@ -684,26 +706,10 @@ pub fn font_value(cx: &App, value: f32) -> f32 { MoonTheme::active_tokens(cx).font(value) } -/// The terminal's own text channel: `tokens.ui(value + tier_text_step)`. -/// -/// Compact / Standard / Large add 1 / 3 / 5 to the design-reference size at the default -/// `mono_font_size` of 11, then `tokens.ui` applies the UI zoom. At Standard density and 100% -/// zoom, this preserves the former `tokens.font(base_text)` result; Compact and Large instead -/// follow the density-tier control font. -/// -/// Args: -/// cx: Application context used to read the active tokens. -/// value: Unscaled design-reference size, the same kind [`base_text`] returns. -/// -/// Returns: -/// The rendered size in logical pixels. -pub fn tier_text_value(cx: &App, value: f32) -> f32 { - ui_value(cx, value + tier_text_step(cx)) -} - -/// Line-box height on the terminal's text channel: [`ui_value`] of `value` plus [`tier_text_step`]. +/// Line-box height on the terminal's text channel: [`ui_value`] of `value` plus the step from +/// [`base_text`] to [`BODY_TEXT`] (3 at the default `mono_font_size` of 11). /// -/// Moves with [`tier_text_value`] so a line box matches the text in it. +/// Moves with [`t_body`] so a line box matches the text in it. /// /// Args: /// cx: Application context used to read the active tokens. @@ -712,7 +718,7 @@ pub fn tier_text_value(cx: &App, value: f32) -> f32 { /// Returns: /// The rendered line height in logical pixels. pub fn line_value(cx: &App, value: f32) -> f32 { - ui_value(cx, value + tier_text_step(cx)) + ui_value(cx, value + BODY_TEXT - base_text(cx)) } /// The BASE size a MoonUI component must be handed so its text RENDERS at `target` logical pixels. @@ -796,7 +802,7 @@ pub fn base_text(cx: &App) -> f32 { /// Return the caption text size for raw GPUI elements such as `div().text_size(...)`. /// -/// The raw-GPUI tiers derive from the density-tier font via [`tier_text_value`]. MoonUI components such as +/// The raw-GPUI tiers derive from [`BODY_TEXT`]. MoonUI components such as /// `MoonText`, `MoonButtonSegment`, and `MoonDataCell` already scale their own default or supplied /// base size. Do not pass a `t_*` result or [`font_value`] into them, because that applies scaling /// twice. @@ -805,8 +811,7 @@ pub fn base_text(cx: &App) -> f32 { /// cx: Application context used to read active theme tokens. /// /// Returns: -/// 10 / 12 / 14 design px at zoom 1.0 at Compact / Standard / Large, for badges, small labels, -/// and counters. +/// 12 design px, for badges, small labels, and counters. pub fn t_caption(cx: &App) -> Pixels { t_body_step_px(cx, -2.0) } @@ -817,7 +822,7 @@ pub fn t_caption(cx: &App) -> Pixels { /// cx: Application context used to read active theme tokens. /// /// Returns: -/// 12 / 14 / 16 design px at zoom 1.0 at Compact / Standard / Large. +/// 14 design px, [`BODY_TEXT`] itself. pub fn t_body(cx: &App) -> Pixels { t_body_step_px(cx, 0.0) } @@ -825,15 +830,15 @@ pub fn t_body(cx: &App) -> Pixels { /// Return the one-step-up body size for a row that must read above its neighbours in place. /// /// Sits between [`t_body`] and [`t_title`] for the case where a row is emphasized INSIDE a list of -/// fixed-height rows: `t_title` is three steps up, and at Large density its line box outgrows a -/// row height that does not track the font, so the text clips. One step clears the neighbours -/// while still fitting. +/// fixed-height rows: `t_title` is three steps up, and its line box outgrows a row height that +/// does not track the font, so the text clips. One step clears the neighbours while still +/// fitting. /// /// Args: /// cx: Application context used to read active theme tokens. /// /// Returns: -/// 13 / 15 / 17 design px at zoom 1.0 at Compact / Standard / Large. +/// 15 design px. pub fn t_body_lg(cx: &App) -> Pixels { t_body_step_px(cx, 1.0) } @@ -844,7 +849,7 @@ pub fn t_body_lg(cx: &App) -> Pixels { /// cx: Application context used to read active theme tokens. /// /// Returns: -/// 15 / 17 / 19 design px at zoom 1.0 at Compact / Standard / Large. +/// 17 design px. pub fn t_title(cx: &App) -> Pixels { t_body_step_px(cx, 3.0) } @@ -854,20 +859,19 @@ pub fn t_title(cx: &App) -> Pixels { /// /// `step` is 0 for body/control text, -2 for a caption, +1 for `body_lg`, +3 for a title. /// `line_base` is the caller's own unscaled design line-height (the value previously passed to -/// `.line_height(...)`, or `MoonTextStyle`'s default 11). Font size is `ui(tier_font + step)`; -/// line height is [`line_value`] of `line_base`, which stays Standard-safe the way [`line_px`] -/// does. +/// `.line_height(...)`, or `MoonTextStyle`'s default 11). Font size is `ui(BODY_TEXT + step)`; +/// line height is [`line_value`] of `line_base`, the way [`line_px`] does it. /// /// Args: /// cx: Application context used to read the active Moon scale. -/// step: Local unscaled addition on top of the density-tier font size. +/// step: Local unscaled addition on top of [`BODY_TEXT`]. /// line_base: Unscaled design-reference line height the caller used before migration. /// /// Returns: /// Rendered font size and line height, ready for `MoonText::rendered_metrics`. -pub fn tier_text_metrics(cx: &App, step: f32, line_base: f32) -> MoonTextMetrics { +pub fn text_metrics(cx: &App, step: f32, line_base: f32) -> MoonTextMetrics { MoonTextMetrics { - font_size: ui_value(cx, tier_font_size(cx) + step), + font_size: ui_value(cx, BODY_TEXT + step), line_height: line_value(cx, line_base), } } @@ -875,19 +879,19 @@ pub fn tier_text_metrics(cx: &App, step: f32, line_base: f32) -> MoonTextMetrics /// [`t_body`] plus a local design-px `step`, already scaled through `tokens.ui`, for raw-GPUI /// `.text_size(...)`. /// -/// At Standard density, `step = 0` and 100% zoom preserve the former body-text size. +/// `step = 0` is the body text itself. /// /// Args: /// cx: Application context used to read the active Moon scale. -/// step: Local unscaled addition on top of [`tier_font_size`]. +/// step: Local unscaled addition on top of [`BODY_TEXT`]. /// /// Returns: /// The rendered size as `Pixels`. pub fn t_body_step_px(cx: &App, step: f32) -> Pixels { - ui_px(cx, tier_font_size(cx) + step) + ui_px(cx, BODY_TEXT + step) } -/// The BASE a font-channel MoonUI prop must be handed so it renders at the density-tier +/// The BASE a font-channel MoonUI prop must be handed so it renders at the body text's /// `tokens.ui` size plus `step`. /// /// Inverse of [`font_value`], via [`font_base_for`]. For `MoonSelectorSegment::font_size` and @@ -895,105 +899,21 @@ pub fn t_body_step_px(cx: &App, step: f32) -> Pixels { /// /// Args: /// cx: Application context used to read the active tokens. -/// step: Local unscaled addition on top of [`tier_font_size`]. +/// step: Local unscaled addition on top of [`BODY_TEXT`]. /// /// Returns: /// An unscaled base for a MoonUI component's own size field. -pub fn tier_font_base(cx: &App, step: f32) -> f32 { - font_base_for(cx, ui_value(cx, tier_font_size(cx) + step)) +pub fn body_font_base(cx: &App, step: f32) -> f32 { + font_base_for(cx, ui_value(cx, BODY_TEXT + step)) } pub fn fit_h_px(cx: &App, base_height: f32, base_line_height: f32, base_pad_y: f32) -> Pixels { px(fit_h_value(cx, base_height, base_line_height, base_pad_y)) } -/// The size tier a Moon button actually renders at, snapped to what buttons support. -/// -/// Args: -/// cx: Application context used to read the active Moon scale. -/// -/// Returns: -/// `tokens.tier().nearest(MoonButtonSize::SUPPORTED)`. -pub fn button_tier(cx: &App) -> MoonSize { - MoonTheme::active_tokens(cx) - .tier() - .nearest(MoonButtonSize::SUPPORTED) -} - -/// The size tier a fixed dense row's checkbox or radio renders at: `Xs` at Compact, `Sm` at -/// Standard and Large. -/// -/// Capped at `Sm` rather than following [`button_tier`] all the way to `Md` at Large — these are -/// FIXED DENSE ROWS (the tuner's filter rows, the time grids, the strategy tree's check column), -/// and letting them grow to `Md` at Large would move Large, which this goal's boundary forbids. -/// Capping here gives Compact its smaller checkbox while Standard and Large render -/// byte-identically to today. -/// -/// Args: -/// cx: Application context used to read the active Moon scale. -/// -/// Returns: -/// `Xs` at Compact, `Sm` at Standard and Large. -pub fn choice_tier(cx: &App) -> MoonSize { - button_tier(cx).min(MoonSize::Sm) -} - -/// The `MoonInput` size a fixed-small terminal input should carry, at every density tier. -/// -/// Terminal call sites that pin `.small()` today render `Small` (22/10/13) at every tier, -/// including Compact, where that is 10px font beside 12px Compact body text. Simply dropping the -/// pin does not fix this: an omitted `MoonInput` size resolves to the density default, `Normal` -/// (28/10.5/14) at Standard and Large, which would move those tiers hard — a large Standard -/// regression the boundary forbids outright. This is why the non-`Xs` arm stays `Small`, not the -/// density default: it resolves to `Compact` (20/12/16) at Compact and `Small` (22/10/13) at -/// Standard and Large — byte-identical to the pin those sites carry today, with Compact fixed. -/// -/// Args: -/// cx: Application context used to read the active Moon scale. -/// -/// Returns: -/// `MoonInputSize::Compact` at Compact, `MoonInputSize::Small` at Standard and Large. -pub fn input_tier(cx: &App) -> MoonInputSize { - if MoonTheme::active_tokens(cx).tier() == MoonSize::Xs { - MoonInputSize::Compact - } else { - MoonInputSize::Small - } -} - -/// Unscaled density-tier control font size, from [`button_tier`]'s `control_metrics().font_size`, -/// for the terminal's `tokens.ui` text channel. -/// -/// Compact 12 / Standard 14 / Large 16. The terminal's own text channel is this number plus a -/// local step, scaled through [`ui_value`] — see [`tier_text_value`]. -/// -/// Args: -/// cx: Application context used to read the active Moon scale. -/// -/// Returns: -/// Design-reference font size, unscaled, like [`base_text`]. -pub fn tier_font_size(cx: &App) -> f32 { - button_tier(cx).control_metrics().font_size -} - -/// Offset from [`base_text`] to [`tier_font_size`] before the terminal passes text through -/// `tokens.ui`: Compact 1 / Standard 3 / Large 5 at the default `mono_font_size` of 11. -/// -/// Replaces `font_delta` (0/3/6) for the terminal's own text. At Standard density and 100% zoom, -/// the resulting rendered size therefore preserves the former value. -/// -/// Args: -/// cx: Application context used to read the active Moon scale. -/// -/// Returns: -/// Unscaled design-reference step. -pub fn tier_text_step(cx: &App) -> f32 { - tier_font_size(cx) - base_text(cx) -} - /// Return the drawn height of a dense-strip button, in base px. /// -/// Pinned to `MoonSize::Xs.control_metrics().height`, not the density tier. Two callers need it: +/// Pinned to `MoonSize::Xs.control_metrics().height`, not [`CONTROL_TIER`]. Two callers need it: /// a plain `div` sitting BESIDE such a button (a card title) takes the same box so the row's /// `items_center` centres two equal heights instead of centring a text line box against a taller /// pill, and the chart's action overlay sizes its own layout from it. @@ -1009,12 +929,12 @@ pub fn micro_control_h(cx: &App) -> Pixels { // ---- Goal C: dock chrome shared by every panel ---- -/// Return the drawn height of an ordinary button at the app's density tier, in base px. +/// Return the drawn height of an ordinary button at the design's control tier, in base px. /// -/// Reads [`button_tier`]'s `control_metrics().height` through [`ui_value`]. This is the +/// Reads [`CONTROL_TIER`]'s `control_metrics().height` through [`ui_value`]. This is the /// ordinary-control family; dense strips use [`micro_control_h_value`]. pub fn action_control_h_value(cx: &App) -> f32 { - ui_value(cx, button_tier(cx).control_metrics().height) + ui_value(cx, CONTROL_TIER.control_metrics().height) } /// [`action_control_h_value`] as `Pixels` — the `*_value`/`*_px` pair every geometry helper in @@ -1024,7 +944,7 @@ pub fn action_control_h_px(cx: &App) -> Pixels { } /// Height floor a panel footer row never sits below, so a footer carrying only text never reads -/// shorter than one carrying an ordinary density-tier control beside it. +/// shorter than one carrying an ordinary control beside it. pub fn panel_band_min_h_px(cx: &App) -> Pixels { action_control_h_px(cx) } @@ -1058,14 +978,14 @@ pub fn footer_tone_color(p: MoonPalette, tone: MoonTone) -> u32 { } /// Width of mono text drawn at the terminal's body size — [`ui_text_width_zoomed`] at -/// [`tier_font_size`]. +/// [`BODY_TEXT`]. /// /// Exists so a caller measuring text it drew with `t_body()` + [`mono`] cannot reach for /// `t_body(cx)` as the size argument: that value is ALREADY scaled, and passing it in would scale /// twice. These five helpers measure TERMINAL text, which now follows the density tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { - ui_text_width_zoomed(cx, text, tier_font_size(cx), weight, true) + ui_text_width_zoomed(cx, text, BODY_TEXT, weight, true) } /// Width of mono text drawn at the terminal's caption size — the [`t_caption`] partner of @@ -1073,7 +993,7 @@ pub fn mono_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// so passing it in would scale twice. Measures TERMINAL text on the density tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { - ui_text_width_zoomed(cx, text, tier_font_size(cx) - 2.0, weight, true) + ui_text_width_zoomed(cx, text, BODY_TEXT - 2.0, weight, true) } /// Width of mono text drawn at the terminal's title size — the [`t_title`] partner of @@ -1081,7 +1001,7 @@ pub fn mono_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// so passing it in would scale twice. Measures TERMINAL text on the density tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_title_text_width(cx: &App, text: &str, weight: f32) -> f32 { - ui_text_width_zoomed(cx, text, tier_font_size(cx) + 3.0, weight, true) + ui_text_width_zoomed(cx, text, BODY_TEXT + 3.0, weight, true) } /// Width of UI-face text drawn at the terminal's body size — the [`ui_font`] partner of @@ -1093,7 +1013,7 @@ pub fn mono_title_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// tier; plain [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal /// text. pub fn ui_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { - ui_text_width_zoomed(cx, text, tier_font_size(cx), weight, false) + ui_text_width_zoomed(cx, text, BODY_TEXT, weight, false) } /// Width of UI-face text drawn at the terminal's caption size — the [`t_caption`] partner of @@ -1101,7 +1021,7 @@ pub fn ui_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// TERMINAL text on the density tier; plain [`ui_text_width`] stays on the legacy font channel for /// mirroring MoonUI-internal text. pub fn ui_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { - ui_text_width_zoomed(cx, text, tier_font_size(cx) - 2.0, weight, false) + ui_text_width_zoomed(cx, text, BODY_TEXT - 2.0, weight, false) } /// Cache key for one glyph advance under an exact resolved font and requested weight. @@ -1588,8 +1508,8 @@ pub fn table_head_base_px(cx: &App) -> Pixels { /// Return the current text-container width scale relative to the theme base. /// -/// [`tier_text_value`] of [`base_text`], divided by [`base_text`]. At zoom 1.0 this is 1.091 / -/// 1.273 / 1.455 at Compact / Standard / Large. Identical to MoonUI's `font_width_scale` at +/// [`ui_value`] of [`BODY_TEXT`], divided by [`base_text`]: 14 / 11 at the default typography. +/// Identical to MoonUI's `font_width_scale` at /// Standard; Compact and Large follow the density tier, not `font_delta`. /// /// Args: @@ -1598,15 +1518,14 @@ pub fn table_head_base_px(cx: &App) -> Pixels { /// Returns: /// The active scaled base font size divided by the unscaled base. pub fn font_scale(cx: &App) -> f32 { - tier_text_value(cx, base_text(cx)) / base_text(cx) + ui_value(cx, BODY_TEXT) / base_text(cx) } /// Scale a fixed text-container width with the terminal's text channel and return `Pixels`. /// /// Args: /// cx: Application context used to calculate [`font_scale`]. -/// base: Design-reference width; the scale is 1.091 / 1.273 / 1.455 at Compact / Standard / -/// Large at zoom 1.0. +/// base: Design-reference width; the scale is 14 / 11 at the default typography. /// /// Returns: /// The font-scaled width as `Pixels`. @@ -1622,8 +1541,7 @@ pub fn font_w_px(cx: &App, base: f32) -> Pixels { /// /// Args: /// cx: Application context used to calculate [`font_scale`]. -/// base: Design-reference width; the scale is 1.091 / 1.273 / 1.455 at Compact / Standard / -/// Large at zoom 1.0. +/// base: Design-reference width; the scale is 14 / 11 at the default typography. /// /// Returns: /// The font-scaled raw pixel width. @@ -1642,7 +1560,7 @@ pub fn font_w(cx: &App, base: f32) -> f32 { /// Unscaled shared `container_radius` token for MoonUI builders that apply UI scaling internally. pub const R_CONTAINER_BASE: f32 = M.container_radius; -/// Return the density-tier control radius for buttons, cards, popups, and panels. +/// Return the control-tier radius for buttons, cards, popups, and panels. /// /// Args: /// cx: Application context used to apply UI scaling. @@ -1650,7 +1568,7 @@ pub const R_CONTAINER_BASE: f32 = M.container_radius; /// Returns: /// The ready-to-use raw-GPUI radius. pub fn r_button(cx: &App) -> Pixels { - ui_px(cx, button_tier(cx).control_metrics().radius) + ui_px(cx, CONTROL_TIER.control_metrics().radius) } /// Return the scaled MoonUI `container_radius`, default 8, for dialogs, modals, and containers. @@ -1810,7 +1728,7 @@ pub fn logo_glow_frame_w(_cx: &App, width: f32) -> Pixels { /// /// The height goes through `ui()`, matching MoonUI, which draws its own separators the same way /// (the brand cluster in `MoonWindowFrame` is one). Density-tier terminal text also tracks `ui()` -/// via [`tier_text_value`]; MoonUI-internal font-channel text still tracks `font_delta`, so the +/// via [`t_body`]; MoonUI-internal font-channel text still tracks `font_delta`, so the /// rule can sit beside a MoonUI row that grew on that channel. The 1px width stays raw, also /// matching MoonUI, since a hairline must not thicken with the font. pub fn vline(cx: &App, height: f32, color: u32) -> impl IntoElement { diff --git a/crates/moon-ui-gpui/src/design/tests.rs b/crates/moon-ui-gpui/src/design/tests.rs index 485daa475..35ec23c89 100644 --- a/crates/moon-ui-gpui/src/design/tests.rs +++ b/crates/moon-ui-gpui/src/design/tests.rs @@ -168,7 +168,7 @@ fn brand_cuts_match_their_geometry_and_scheme() { // `theme_contract` toggle-tone check instead). use super::{CHROME_RULE_H, HEADER_TOP_H, TOOLBAR_H, chrome_toggle_tone, readout_color}; -use moon_core::config::{UiDensity, UiThemeMode}; +use moon_core::config::UiThemeMode; use moon_ui::MoonPalette; /// `design::readout_color` is the ONE place a toolbar readout decides muted-vs-present. @@ -197,67 +197,43 @@ fn readout_color_is_muted_when_absent_and_full_strength_when_present() { ); } -/// `CHROME_RULE_H` must stay strictly under the shorter of the header and toolbar bands at EVERY -/// density-derived font delta, or the seam -/// `design::chrome_divider` draws paints outside the chrome strip and over the dock border below -/// it — and since `chrome_divider` has ~11 consumers app-wide, that lands in every one of them at -/// once. +/// `CHROME_RULE_H` must stay strictly under the shorter of the header and toolbar bands, or the +/// seam `design::chrome_divider` draws paints outside the chrome strip and over the dock border +/// below it — and since `chrome_divider` has ~11 consumers app-wide, that lands in every one of +/// them at once. /// /// Breakage this pins: raising `CHROME_RULE_H` to make the seam more visible without checking the -/// narrowest band (at Compact density both bands are 28px; the token is 20). +/// bands (the token is 20; both bands are 35 at the design's font delta). /// -/// Computed directly against `MoonThemeTokens::fit_band` and `tier_band_base` rather than through +/// Computed directly against `MoonThemeTokens::fit_band` rather than through /// `design::header_height`/`design::toolbar_height`, because those two need a live `App` this -/// unit test does not have. Those are the exact pure token calls both adapters delegate to, so the -/// test measures their rendered band values rather than the superseded legacy fit-height triples. +/// unit test does not have. That is the exact pure token call both adapters delegate to. #[test] -fn chrome_rule_h_stays_under_both_chrome_bands_at_every_font_delta() { - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - let tokens = - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0) - .dark; - let delta = tokens.scale.font_delta; - let header = tokens.fit_band(tokens.tier_band_base(HEADER_TOP_H, |m| m.height), 14.0); - let toolbar = tokens.fit_band(tokens.tier_band_base(TOOLBAR_H, |m| m.height), 13.0); - assert!( - CHROME_RULE_H < header.min(toolbar), - "font delta {delta}: CHROME_RULE_H {CHROME_RULE_H} must stay under header {header} \ - and toolbar {toolbar}" - ); - } +fn chrome_rule_h_stays_under_both_chrome_bands() { + let tokens = crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0).dark; + let header = tokens.fit_band(HEADER_TOP_H, 14.0); + let toolbar = tokens.fit_band(TOOLBAR_H, 13.0); + assert!( + CHROME_RULE_H < header.min(toolbar), + "CHROME_RULE_H {CHROME_RULE_H} must stay under header {header} and toolbar {toolbar}" + ); } -/// Catches the compact-band rebase moving the frozen Standard or Large rendered dimensions, -/// which would change table and chrome geometry outside the user-selected Compact density. +/// Catches moving the design's fixed size system — `design::CONTROL_TIER`, +/// `design::DESIGN_FONT_DELTA` or the band bases — which would move every table row and both +/// chrome bands away from the reviewed Standard design. #[test] -fn density_band_tokens_keep_standard_and_large_rendered_values() { - let values = [ - (UiDensity::Compact, (21.0, 22.0, 28.0, 28.0)), - (UiDensity::Standard, (28.0, 29.0, 35.0, 35.0)), - (UiDensity::Large, (31.0, 32.0, 38.0, 38.0)), - ]; - let mut previous: Option<(f32, f32, f32, f32)> = None; - for (density, expected) in values { - let tokens = - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0) - .dark; - let actual = ( +fn design_band_tokens_render_the_reviewed_standard_values() { + let tokens = crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0).dark; + assert_eq!( + ( tokens.table_row_height(), tokens.table_header_height(), - tokens.fit_band(tokens.tier_band_base(HEADER_TOP_H, |m| m.height), 14.0), - tokens.fit_band(tokens.tier_band_base(TOOLBAR_H, |m| m.height), 13.0), - ); - assert_eq!(actual, expected); - if let Some(before) = previous { - assert!( - before.0 < actual.0 - && before.1 < actual.1 - && before.2 < actual.2 - && before.3 < actual.3 - ); - } - previous = Some(actual); - } + tokens.fit_band(HEADER_TOP_H, 14.0), + tokens.fit_band(TOOLBAR_H, 13.0), + ), + (28.0, 29.0, 35.0, 35.0) + ); } /// `design::chrome_toggle_tone` is the one truth table every chrome toggle (Sleep, own-trade, SL) @@ -340,161 +316,126 @@ fn a_degenerate_scale_answers_the_target_itself() { assert_eq!(invert_font_scale(12.0, 12.0, 14.0), 14.0); } -// --- MoonButton density tier: dense strips vs ordinary rows ------------------------------------ +// --- Dense strips vs ordinary rows ------------------------------------------------------------- /// `design::micro_control_h_value` is PINNED to `MoonSize::Xs` while `design::action_control_h_value` -/// FOLLOWS the app's density tier through [`super::button_tier`] — the whole point of splitting a -/// dense strip's height from an ordinary control row's. +/// reads [`super::CONTROL_TIER`] — the whole point of splitting a dense strip's height from an +/// ordinary control row's. /// -/// Breakage this pins: swapping the two helpers' bodies. That inversion — dense strips growing -/// with density while ordinary rows stop growing — is the single most damaging way this goal can -/// be silently undone, and nothing else in either repo catches it. +/// Breakage this pins: swapping the two helpers' bodies, or moving `CONTROL_TIER` off `Sm`. That +/// inversion — dense strips as tall as ordinary rows — is the single most damaging way this split +/// can be silently undone, and nothing else in either repo catches it. #[gpui::test] -fn micro_control_h_value_stays_pinned_while_action_follows_density(cx: &mut gpui::TestAppContext) { - for (density, expected_action) in [ - (UiDensity::Compact, 20.0), - (UiDensity::Standard, 24.0), - (UiDensity::Large, 32.0), - ] { - cx.update(|cx| { - moon_ui::MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0), - cx, - ); - }); - let (micro, action) = cx.update(|cx| { - ( - super::micro_control_h_value(cx), - super::action_control_h_value(cx), - ) - }); - assert_eq!( - micro, 20.0, - "{density:?}: micro_control_h_value must stay pinned to MoonSize::Xs, got {micro}" - ); - assert_eq!( - action, expected_action, - "{density:?}: action_control_h_value must follow the density tier, got {action}" +fn micro_control_h_value_stays_below_the_control_row(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + moon_ui::MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), + cx, ); - } + }); + let (micro, action) = cx.update(|cx| { + ( + super::micro_control_h_value(cx), + super::action_control_h_value(cx), + ) + }); + assert_eq!( + micro, 20.0, + "micro_control_h_value must stay pinned to MoonSize::Xs, got {micro}" + ); + assert_eq!( + action, 24.0, + "action_control_h_value must read CONTROL_TIER (Sm), got {action}" + ); } -// --- Compact density: one size system ------------------------------------- +// --- One size system --------------------------------------------------------------------------- -use super::{font_w, line_px, t_body, t_body_lg, t_caption, t_title, tier_font_base, ui_px}; +use super::{ + BODY_TEXT, CONTROL_TIER, body_font_base, font_w, line_px, t_body, t_body_lg, t_caption, + t_title, ui_px, +}; use gpui::px; -/// `design.rs:tier_text_value` must preserve Standard's legacy results after the tier rebase. +/// `design.rs:t_body` and its siblings must render what MoonUI's font channel rendered for the +/// reviewed design: the text sizes the rest of the interface was measured against. /// -/// Breakage: restoring `font_delta` as the tier step, or changing the caption/body/title steps, -/// shifts roughly 400 Standard text call sites even though Standard was promised unchanged. +/// Breakage: changing `BODY_TEXT`, `DESIGN_FONT_DELTA` or the caption/body/title steps shifts +/// roughly 400 text call sites away from the design. #[gpui::test] -fn standard_text_is_unchanged_by_the_rebase(cx: &mut gpui::TestAppContext) { - for zoom in [0.75, 1.0, 1.25, 1.5] { - cx.update(|cx| { - moon_ui::MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation( - UiThemeMode::Dark, - UiDensity::Standard, - zoom, - ), - cx, - ); - }); - let (body, caption, body_lg, title, line, legacy) = cx.update(|cx| { - let tokens = moon_ui::MoonTheme::active_tokens(cx); - ( - t_body(cx), - t_caption(cx), - t_body_lg(cx), - t_title(cx), - line_px(cx, 14.0), - ( - px(tokens.font(11.0)), - px(tokens.font(9.0)), - px(tokens.font(12.0)), - px(tokens.font(14.0)), - px(tokens.line_height(14.0)), - ), - ) - }); - assert_eq!( - (body, caption, body_lg, title, line), - legacy, - "Standard zoom {zoom}" +fn body_text_matches_the_legacy_font_channel_at_the_design(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + moon_ui::MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), + cx, ); - } + }); + let (body, caption, body_lg, title, line, legacy) = cx.update(|cx| { + let tokens = moon_ui::MoonTheme::active_tokens(cx); + ( + t_body(cx), + t_caption(cx), + t_body_lg(cx), + t_title(cx), + line_px(cx, 14.0), + ( + px(tokens.font(11.0)), + px(tokens.font(9.0)), + px(tokens.font(12.0)), + px(tokens.font(14.0)), + px(tokens.line_height(14.0)), + ), + ) + }); + assert_eq!((body, caption, body_lg, title, line), legacy); } -/// `design.rs:t_body` and its sibling tiers must render from MoonUI's density control metric. +/// `design.rs:BODY_TEXT` is the control tier's own font, and the `t_*` helpers and `font_w` derive +/// from it, so text sits on the same size system as the controls beside it. /// -/// Breakage: pinning a `t_*` helper to a literal or leaving `font_w` on the legacy width scale -/// makes Compact text disagree with the controls it sits beside. +/// Breakage: pinning a `t_*` helper to a literal, or leaving `font_w` on another width scale, +/// makes text disagree with the controls it sits beside. #[gpui::test] -fn t_body_is_the_tier_font_at_every_density(cx: &mut gpui::TestAppContext) { - for (density, expected_font) in [ - (UiDensity::Compact, 12.0), - (UiDensity::Standard, 14.0), - (UiDensity::Large, 16.0), - ] { - for zoom in [0.75, 1.0, 1.5] { - cx.update(|cx| { - moon_ui::MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation( - UiThemeMode::Dark, - density, - zoom, - ), - cx, - ); - }); - let (body, caption, title, width, expected) = cx.update(|cx| { - ( - t_body(cx), - t_caption(cx), - t_title(cx), - font_w(cx, 11.0), - ( - ui_px(cx, expected_font), - ui_px(cx, expected_font - 2.0), - ui_px(cx, expected_font + 3.0), - ), - ) - }); - assert_eq!(body, expected.0, "{density:?} zoom {zoom}: body"); - assert_eq!(caption, expected.1, "{density:?} zoom {zoom}: caption"); - assert_eq!(title, expected.2, "{density:?} zoom {zoom}: title"); - assert_eq!( - width, - f32::from(body), - "{density:?} zoom {zoom}: body width" - ); - } - } +fn t_body_is_the_control_tiers_font(cx: &mut gpui::TestAppContext) { + assert_eq!(BODY_TEXT, CONTROL_TIER.control_metrics().font_size); + cx.update(|cx| { + moon_ui::MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), + cx, + ); + }); + let (body, caption, title, width, expected) = cx.update(|cx| { + ( + t_body(cx), + t_caption(cx), + t_title(cx), + font_w(cx, 11.0), + (ui_px(cx, 14.0), ui_px(cx, 12.0), ui_px(cx, 17.0)), + ) + }); + assert_eq!(body, expected.0, "body"); + assert_eq!(caption, expected.1, "caption"); + assert_eq!(title, expected.2, "title"); + assert_eq!(width, f32::from(body), "body width"); } -/// `design.rs:tier_font_base` must invert MoonUI's font channel at each supported density. +/// `design.rs:body_font_base` must invert MoonUI's font channel. /// /// Breakage: clamping MoonUI's font transform or dropping the helper's UI-value wrapping puts -/// table cells and header pills back off-tier without changing their call sites. +/// table cells and header pills off the body size without changing their call sites. #[gpui::test] -fn tier_font_base_round_trips_through_the_font_channel(cx: &mut gpui::TestAppContext) { - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - cx.update(|cx| { - moon_ui::MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, density, 1.0), - cx, - ); - }); - let (rendered, body) = cx.update(|cx| { - ( - moon_ui::MoonTheme::active_tokens(cx).font(tier_font_base(cx, 0.0)), - f32::from(t_body(cx)), - ) - }); - assert!( - (rendered - body).abs() < 0.01, - "{density:?}: {rendered} != {body}" +fn body_font_base_round_trips_through_the_font_channel(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + moon_ui::MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), + cx, ); - } + }); + let (rendered, body) = cx.update(|cx| { + ( + moon_ui::MoonTheme::active_tokens(cx).font(body_font_base(cx, 0.0)), + f32::from(t_body(cx)), + ) + }); + assert!((rendered - body).abs() < 0.01, "{rendered} != {body}"); } diff --git a/crates/moon-ui-gpui/src/panels/alerts/controls.rs b/crates/moon-ui-gpui/src/panels/alerts/controls.rs index ff6a1d62e..6b7736cdf 100644 --- a/crates/moon-ui-gpui/src/panels/alerts/controls.rs +++ b/crates/moon-ui-gpui/src/panels/alerts/controls.rs @@ -40,7 +40,7 @@ impl AlertsPanel { div().w(px(90.0)).child( MoonInput::new("alerts-coin") .state(&self.coin_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) // A filter with no ✕ can only be undone by holding backspace. .cleanable(true), ), diff --git a/crates/moon-ui-gpui/src/panels/assets/settings.rs b/crates/moon-ui-gpui/src/panels/assets/settings.rs index 6d4e149dd..6282fb056 100644 --- a/crates/moon-ui-gpui/src/panels/assets/settings.rs +++ b/crates/moon-ui-gpui/src/panels/assets/settings.rs @@ -165,14 +165,13 @@ fn settings_content_width(cx: &App) -> f32 { let label_width = design::ui_text_width_zoomed( cx, &t!(GROUP_BY_VENUE_LABEL), - crate::panels::common::checkbox_metrics(cx).font, + crate::panels::common::CHECKBOX_METRICS.font, COMPACT_CHECKBOX_WEIGHT, false, ); let checkbox_leading = f32::from(design::ui_px( cx, - crate::panels::common::checkbox_metrics(cx).mark - + crate::panels::common::checkbox_metrics(cx).gap, + crate::panels::common::CHECKBOX_METRICS.mark + crate::panels::common::CHECKBOX_METRICS.gap, )); let group_content = group_width.max(checkbox_leading + label_width); title_width.max(group_content + popup_group_inset_px(cx)) diff --git a/crates/moon-ui-gpui/src/panels/assets/spot_order.rs b/crates/moon-ui-gpui/src/panels/assets/spot_order.rs index 8fb1ed752..3480c059c 100644 --- a/crates/moon-ui-gpui/src/panels/assets/spot_order.rs +++ b/crates/moon-ui-gpui/src/panels/assets/spot_order.rs @@ -316,7 +316,7 @@ fn dialog_body( t!("assets.order_dialog.size", coin = draft.coin.as_str()).to_string(), MoonInput::new("assets-spot-order-size") .state(&draft.size_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), Some(max), )) @@ -326,7 +326,7 @@ fn dialog_body( t!("assets.order_dialog.price").to_string(), MoonInput::new("assets-spot-order-price") .state(&draft.price_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), current, )) diff --git a/crates/moon-ui-gpui/src/panels/assets/table.rs b/crates/moon-ui-gpui/src/panels/assets/table.rs index 1fe9fd31b..d56eeb569 100644 --- a/crates/moon-ui-gpui/src/panels/assets/table.rs +++ b/crates/moon-ui-gpui/src/panels/assets/table.rs @@ -1165,7 +1165,7 @@ fn coin_cell( el.child( MoonText::new("SELL") .color(MoonTone::Info.color(p)) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 14.0)) .weight(600.0) .mono(true) .uppercase(false) diff --git a/crates/moon-ui-gpui/src/panels/assets/wallets.rs b/crates/moon-ui-gpui/src/panels/assets/wallets.rs index c9b55eebf..ee7cfa3d2 100644 --- a/crates/moon-ui-gpui/src/panels/assets/wallets.rs +++ b/crates/moon-ui-gpui/src/panels/assets/wallets.rs @@ -377,7 +377,7 @@ impl AssetsView { body = body.child( MoonInput::new("transfer-amount") .state(&input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ); } diff --git a/crates/moon-ui-gpui/src/panels/chart/arb_open.rs b/crates/moon-ui-gpui/src/panels/chart/arb_open.rs index 7f6a1ceb7..48879f84f 100644 --- a/crates/moon-ui-gpui/src/panels/chart/arb_open.rs +++ b/crates/moon-ui-gpui/src/panels/chart/arb_open.rs @@ -18,15 +18,26 @@ use super::ChartPanel; use crate::controls::coin_search; impl ChartPanel { - /// Where the chart slot sits in the window, in LOGICAL pixels, and the scale it draws at. + /// Where the chart slot sits in the window, in the CHART's logical pixels, the scale it draws + /// at, and the window's content zoom. /// - /// THE one place the two coordinate systems meet: the caption pass works in window logical - /// pixels, the panel's own input and layout in slot ones. Both the press and the cursor zones - /// go through this, so they cannot drift apart again. - pub(super) fn chart_origin_logical(&self) -> Option<((f32, f32), f32)> { + /// THE one place the coordinate systems meet: the caption pass works in the chart's logical + /// pixels (the platform's, so the chart keeps device density under UI zoom), the panel's own + /// input in slot device pixels, and GPUI overlays in the window's content pixels — the chart's + /// divided by the zoom. Both the press and the cursor zones go through this, so they cannot + /// drift apart again. + pub(super) fn chart_origin_logical(&self) -> Option<((f32, f32), f32, f32)> { let (bounds, sf, _) = self.chart.slot_geometry()?; let sf = sf.max(0.1); - Some(((f32::from(bounds.origin.x), f32::from(bounds.origin.y)), sf)) + let zoom = self.chart.slot_content_zoom().max(0.1); + Some(( + ( + f32::from(bounds.origin.x) * zoom, + f32::from(bounds.origin.y) * zoom, + ), + sf, + zoom, + )) } /// Transparent zones over the venue names, so the pointer changes over a clickable one. @@ -42,13 +53,14 @@ impl ChartPanel { /// Lay one transparent pointing-hand zone over every rectangle a pane published. /// /// The shared half of the two overlays — the arbitrage names and the market buttons — because - /// what differs between them is only which rectangles they ask for. Both work in the WINDOW's - /// logical pixels while this overlay is laid out inside the chart SLOT, so the slot's own - /// position comes off here: the exact inverse of what a press does, from the same helper, which - /// is what keeps the cursor and the click on the same rectangle. + /// what differs between them is only which rectangles they ask for. Both work in the CHART's + /// logical pixels while this overlay is laid out inside the chart SLOT in content pixels, so + /// the slot's own position comes off and the zoom divides here: the exact inverse of what a + /// press does, from the same helper, which is what keeps the cursor and the click on the same + /// rectangle. /// /// Args: - /// rects: What one pane offers, in the window's logical pixels. + /// rects: What one pane offers, in the chart's logical pixels. /// /// Returns: /// One absolutely-placed zone per rectangle, carrying no handlers. @@ -57,7 +69,7 @@ impl ChartPanel { rects: impl Fn(usize) -> Vec<(f32, f32, f32, f32)>, ) -> Vec
{ let mut out = Vec::new(); - let Some((origin, _)) = self.chart_origin_logical() else { + let Some((origin, _, zoom)) = self.chart_origin_logical() else { return out; }; for pane in 0..self.chart.pane_count() { @@ -68,10 +80,10 @@ impl ChartPanel { out.push( div() .absolute() - .left(px(x - origin.0)) - .top(px(y - origin.1)) - .w(px(w)) - .h(px(h)) + .left(px((x - origin.0) / zoom)) + .top(px((y - origin.1) / zoom)) + .w(px(w / zoom)) + .h(px(h / zoom)) .cursor(CursorStyle::PointingHand), ); } @@ -121,7 +133,7 @@ impl ChartPanel { // this point is in the SLOT's device pixels. So the conversion is both: scale down, then // add where the slot sits in the window. Missing the origin put every hit off by exactly // the panel's position, which on a chart under a header and a toolbar is a long way down. - let Some((origin, sf)) = self.chart_origin_logical() else { + let Some((origin, sf, _)) = self.chart_origin_logical() else { return false; }; let (lx, ly) = (pos.0 / sf + origin.0, pos.1 / sf + origin.1); diff --git a/crates/moon-ui-gpui/src/panels/chart/filter_headers.rs b/crates/moon-ui-gpui/src/panels/chart/filter_headers.rs index 3a047e5e8..25ebf178a 100644 --- a/crates/moon-ui-gpui/src/panels/chart/filter_headers.rs +++ b/crates/moon-ui-gpui/src/panels/chart/filter_headers.rs @@ -14,7 +14,7 @@ impl ChartPanel { let Some(pane) = self.input.pane_at(pos.0, pos.1) else { return false; }; - let Some((origin, scale)) = self.chart_origin_logical() else { + let Some((origin, scale, _)) = self.chart_origin_logical() else { return false; }; let mut cfg = self.effective_labels(cx); diff --git a/crates/moon-ui-gpui/src/panels/chart/market_actions.rs b/crates/moon-ui-gpui/src/panels/chart/market_actions.rs index 6ddecd24c..d81167417 100644 --- a/crates/moon-ui-gpui/src/panels/chart/market_actions.rs +++ b/crates/moon-ui-gpui/src/panels/chart/market_actions.rs @@ -30,17 +30,20 @@ use crate::Backend; /// Convert the chart's measured caption size and line box to MoonUI tier-segment inputs. /// -/// Tier segments use only UI zoom, so the legacy font delta must never enter the calculation. -/// The line box matches the caption layout's measured size plus four logical pixels. +/// The caption is drawn by the chart at the monitor's density — its size is in the chart's own +/// logical pixels — while the segment is a GPUI element scaled by the window content zoom. So the +/// segment is handed the size divided by that zoom and lands on the same device pixels. The +/// legacy font delta must never enter the calculation. The line box matches the caption layout's +/// measured size plus four logical pixels. /// /// Args: -/// cx: Application context providing the active UI zoom. -/// size: Caption font size already measured in logical pixels. +/// cx: Application context providing the active theme's content zoom. +/// size: Caption font size as the chart measured it, in its own logical pixels. /// /// Returns: /// Unscaled font size and line height for a tier-sized button segment. fn action_label_metrics(cx: &App, size: f32) -> (f32, f32) { - let zoom = crate::design::ui_value(cx, 1.0); + let zoom = moon_ui::MoonTheme::content_zoom(cx).max(0.1); (size / zoom, (size + 4.0) / zoom) } @@ -128,10 +131,11 @@ impl ChartPanel { if !self.market_actions_pushed { return Vec::new(); } - // The published rectangles are in the WINDOW's logical pixels and this overlay is laid out - // inside the chart SLOT, so the slot's own position comes off — the same conversion the - // arbitrage cursor zones make, through the same helper. - let Some((origin, _)) = self.chart_origin_logical() else { + // The published rectangles are in the CHART's logical pixels and this overlay is laid out + // inside the chart SLOT in content pixels, so the slot's own position comes off and the + // zoom divides — the same conversion the arbitrage cursor zones make, through the same + // helper. + let Some((origin, _, zoom)) = self.chart_origin_logical() else { return Vec::new(); }; let mut out: Vec = Vec::new(); @@ -171,10 +175,10 @@ impl ChartPanel { out.push( div() .absolute() - .left(px(button.x - origin.0)) - .top(px(button.y - origin.1)) - .w(px(button.w)) - .h(px(button.h)) + .left(px((button.x - origin.0) / zoom)) + .top(px((button.y - origin.1) / zoom)) + .w(px(button.w / zoom)) + .h(px(button.h / zoom)) // The chart's own input lies UNDER this overlay and reads a left press as a // trading gesture. `MoonButton` stops propagation only while it is disabled // — so an enabled one would place an order under the control that was @@ -194,7 +198,7 @@ impl ChartPanel { .variant(variant) .selected(selected) .disabled(!button.enabled) - .bounds(MoonRect::new(0.0, 0.0, button.w, button.h)) + .bounds(MoonRect::new(0.0, 0.0, button.w / zoom, button.h / zoom)) .on_click(move |event, window, app| { let (market, coin) = (market.clone(), coin.clone()); // The lock is the one control that ASKS. Closing it is a choice diff --git a/crates/moon-ui-gpui/src/panels/chart/market_actions/tests.rs b/crates/moon-ui-gpui/src/panels/chart/market_actions/tests.rs index 6d9ba77cb..f84a7996e 100644 --- a/crates/moon-ui-gpui/src/panels/chart/market_actions/tests.rs +++ b/crates/moon-ui-gpui/src/panels/chart/market_actions/tests.rs @@ -1,32 +1,28 @@ //! Chart-caption regression coverage for the MoonUI tier-segment boundary. -use moon_core::config::{UiDensity, UiThemeMode}; +use moon_core::config::UiThemeMode; use moon_ui::MoonTheme; -/// Restoring the legacy inverse makes captions shrink by the density delta inside their measured -/// rectangles. Assert the actual tier forward transform preserves both glyph and line-box sizes. +/// The chart draws its captions at device density while the action segments are GPUI elements +/// under the window content zoom, so a segment must be handed the caption size divided by that +/// zoom to land on the same device pixels. Dividing by the token scale (always 1 now) or by +/// nothing puts the labels off their measured rectangles at every zoom but 100%. #[gpui::test] -fn chart_action_labels_match_measured_sizes_at_every_density_and_zoom( - cx: &mut gpui::TestAppContext, -) { - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - for zoom in [0.75, 1.0, 1.5] { - cx.update(|cx| { - MoonTheme::install_config( - crate::startup::moon_theme_config_for_presentation( - UiThemeMode::Dark, - density, - zoom, - ), - cx, +fn chart_action_labels_land_on_the_chart_caption_at_every_zoom(cx: &mut gpui::TestAppContext) { + for zoom in [0.75, 1.0, 1.5] { + cx.update(|cx| { + MoonTheme::install_config( + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, zoom), + cx, + ); + for size in [8.0, 15.0, 32.0] { + let (font, line) = super::action_label_metrics(cx, size); + assert!((font * zoom - size).abs() < 0.0001, "zoom {zoom}: font"); + assert!( + (line * zoom - (size + 4.0)).abs() < 0.0001, + "zoom {zoom}: line" ); - let tokens = MoonTheme::active_tokens(cx); - for size in [8.0, 15.0, 32.0] { - let (font, line) = super::action_label_metrics(cx, size); - assert!((tokens.ui(font) - size).abs() < 0.0001); - assert!((tokens.ui(line) - (size + 4.0)).abs() < 0.0001); - } - }); - } + } + }); } } diff --git a/crates/moon-ui-gpui/src/panels/chart/render.rs b/crates/moon-ui-gpui/src/panels/chart/render.rs index bf712ec63..b6f1bad9a 100644 --- a/crates/moon-ui-gpui/src/panels/chart/render.rs +++ b/crates/moon-ui-gpui/src/panels/chart/render.rs @@ -46,7 +46,9 @@ impl Render for ChartPanel { self.chart.set_scene_visible(true); self.chart .set_market_source(Some(self.backend.read(cx).session.market_source())); - let ppp = window.scale_factor(); + // The chart keeps the monitor's density under UI zoom: its sizes go through the platform + // factor alone, which is the window's effective factor with the content zoom taken out. + let ppp = window.scale_factor() / window.content_zoom(); // Cache DPI for the data-prepare path, which has no Window. DPI changes infrequently. self.last_ppp = ppp; self.chart.set_last_ppp(ppp); @@ -461,15 +463,20 @@ impl Render for ChartPanel { canvas( move |bounds, _, _| bounds, move |bounds, _, window, cx| { - let sf = window.scale_factor(); + // The probe is platform space: the storms add its rectangle to the + // window's screen origin or scale it by its factor, so content-space + // bounds go in multiplied by the content zoom, with the zoom taken out + // of the factor. + let zoom = window.content_zoom(); + let sf = window.scale_factor() / zoom; let firetest_probe = crate::firetest::ChartProbe::new( crate::window::windowing::window_hwnd(window), f32::from(window.window_bounds().get_bounds().origin.x), f32::from(window.window_bounds().get_bounds().origin.y), - f32::from(bounds.origin.x), - f32::from(bounds.origin.y), - f32::from(bounds.size.width), - f32::from(bounds.size.height), + f32::from(bounds.origin.x) * zoom, + f32::from(bounds.origin.y) * zoom, + f32::from(bounds.size.width) * zoom, + f32::from(bounds.size.height) * zoom, sf, ); if is_main { diff --git a/crates/moon-ui-gpui/src/panels/chart/render_input.rs b/crates/moon-ui-gpui/src/panels/chart/render_input.rs index c2f6c6f01..0b1046144 100644 --- a/crates/moon-ui-gpui/src/panels/chart/render_input.rs +++ b/crates/moon-ui-gpui/src/panels/chart/render_input.rs @@ -51,10 +51,13 @@ fn press_count( // `bounds()`, not `window_bounds()`: the latter is the RESTORE rectangle, which for a maximized // window names a position it is not at — and this position is compared against a mark every // window in the process shares. + // The mark is screen space, so the content-space press is multiplied by the zoom before the + // window's screen origin is added; the close button that records the mark converts the same way. let origin = window.bounds().origin; + let zoom = window.content_zoom(); let pos = ( - f32::from(origin.x + position.x), - f32::from(origin.y + position.y), + f32::from(origin.x) + f32::from(position.x) * zoom, + f32::from(origin.y) + f32::from(position.y) * zoom, ); let count = this.click_series.observe(button, native, now, pos); // A press still parked where a × was clicked belongs to that closing, however many charts the diff --git a/crates/moon-ui-gpui/src/panels/chart/volume_menu.rs b/crates/moon-ui-gpui/src/panels/chart/volume_menu.rs index 2958ca547..3e2ff10e5 100644 --- a/crates/moon-ui-gpui/src/panels/chart/volume_menu.rs +++ b/crates/moon-ui-gpui/src/panels/chart/volume_menu.rs @@ -88,7 +88,7 @@ impl ChartPanel { // The caption pass places its blocks in the WINDOW's logical pixels while this point is in // the slot's device pixels — the same conversion the arbitrage click documents, and getting // it wrong opens the menu for whatever block sits under the mis-scaled point. - let Some((origin, sf)) = self.chart_origin_logical() else { + let Some((origin, sf, _)) = self.chart_origin_logical() else { return false; }; let (lx, ly) = (local_pos.0 / sf + origin.0, local_pos.1 / sf + origin.1); diff --git a/crates/moon-ui-gpui/src/panels/common.rs b/crates/moon-ui-gpui/src/panels/common.rs index 44444988e..e582ef114 100644 --- a/crates/moon-ui-gpui/src/panels/common.rs +++ b/crates/moon-ui-gpui/src/panels/common.rs @@ -350,31 +350,14 @@ pub(crate) struct CheckboxMetrics { pub(crate) font: f32, } -/// Resolves the active density to the same box, gap and font used by `MoonCheckbox`. -/// -/// Args: -/// cx: Application context used to read the active density tier. -/// -/// Returns: -/// Unscaled metrics for use with [`design::ui_px`] and [`design::ui_text_width_zoomed`]. -pub(crate) fn checkbox_metrics(cx: &App) -> CheckboxMetrics { - let tokens = moon_ui::MoonTheme::active_tokens(cx); - let tier = tokens.tier().nearest(&[ - moon_ui::MoonSize::Xs, - moon_ui::MoonSize::Sm, - moon_ui::MoonSize::Md, - ]); - let control = tier.control_metrics(); - CheckboxMetrics { - mark: match tier { - moon_ui::MoonSize::Xs => 12.0, - moon_ui::MoonSize::Sm => 16.0, - _ => 20.0, - }, - gap: control.gap, - font: control.font_size, - } -} +/// The box, gap and font `MoonCheckbox` draws at [`design::CONTROL_TIER`]: a 16px mark with the +/// tier's gap and control font, unscaled, for use with [`design::ui_px`] and +/// [`design::ui_text_width_zoomed`]. +pub(crate) const CHECKBOX_METRICS: CheckboxMetrics = CheckboxMetrics { + mark: 16.0, + gap: design::CONTROL_TIER.control_metrics().gap, + font: design::CONTROL_TIER.control_metrics().font_size, +}; /// `MoonCheckbox` label weight (medium), as GPUI's numeric font weight. pub(crate) const COMPACT_CHECKBOX_WEIGHT: f32 = 500.0; @@ -750,7 +733,7 @@ pub(crate) fn pinned_scope_label( .font_family(design::mono()) .text_size(design::ui_px( cx, - design::button_tier(cx).control_metrics().font_size, + design::CONTROL_TIER.control_metrics().font_size, )) .text_color(rgb(design::chrome_label_color(p))) .child(div().flex_none().child(design::PINNED_SCOPE_GLYPH)) 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 7264de377..af060a6ee 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 @@ -61,7 +61,7 @@ const LINE_BOX: f32 = 1.3; /// /// The popup mixes two scales that MoonUI moves independently: its captions and values are raw /// GPUI text on the terminal's text channel (`design::t_caption` / `design::t_body`, which grow -/// with the density tier's `design::tier_text_step`, +3 at Standard), while its +/// with the body text's step above the base text, `design::BODY_TEXT - base_text`, +3), while its /// `MoonCheckbox`, `MoonButton` steppers and the ▶ box are MoonUI widgets on the UI scale. A column sized on one /// of the two is wrong whenever the other is larger: on the UI scale the text outgrew its column /// at the stock config, and on the font scale a widened UI would push the widgets out. 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 a52368ecc..387170bd6 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/interactions.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/interactions.rs @@ -1223,7 +1223,7 @@ impl CoreStatusView { .child( MoonInput::new("core-status-fleet-named-input") .state(&field) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .child( div() diff --git a/crates/moon-ui-gpui/src/panels/core_status/server_view.rs b/crates/moon-ui-gpui/src/panels/core_status/server_view.rs index 1cdd69f2a..84e864aff 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/server_view.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/server_view.rs @@ -503,7 +503,7 @@ fn server_row( } } }) - .child(server_identity(group, edit_input, w, weak_view, p, app)) + .child(server_identity(group, edit_input, w, weak_view, p)) .child(ip_column(group, masked, w, p)) .child(div().flex_1()) .child(metric_cell( @@ -932,7 +932,6 @@ fn worst_by_level( /// w: Shared column widths, supplying the name column. /// weak_view: Non-owning panel handle for the rename callback. /// p: Active Moon palette. -/// app: Application context used to select the inline editor's density tier. /// /// Returns: /// A fixed-width identity element; the name keeps priority over the masked IP. @@ -942,7 +941,6 @@ fn server_identity( w: ByIpWidths, weak_view: &WeakEntity, p: MoonPalette, - app: &App, ) -> AnyElement { let key = group.key; if let Some(state) = edit_input { @@ -957,7 +955,7 @@ fn server_identity( key.tree_id() ))) .state(&state) - .size(design::input_tier(app)), + .size(design::INPUT_SIZE), ) .into_any_element(); } diff --git a/crates/moon-ui-gpui/src/panels/core_status/update_menu.rs b/crates/moon-ui-gpui/src/panels/core_status/update_menu.rs index 45f8a0413..fadc7d038 100644 --- a/crates/moon-ui-gpui/src/panels/core_status/update_menu.rs +++ b/crates/moon-ui-gpui/src/panels/core_status/update_menu.rs @@ -425,7 +425,7 @@ fn open_named_dialog( .child( MoonInput::new("core-update-named-input") .state(&field) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .child(div().text_color(moon(p.text_muted)).child(hint.clone())), ) diff --git a/crates/moon-ui-gpui/src/panels/detects/cards.rs b/crates/moon-ui-gpui/src/panels/detects/cards.rs index af13c9738..45c704247 100644 --- a/crates/moon-ui-gpui/src/panels/detects/cards.rs +++ b/crates/moon-ui-gpui/src/panels/detects/cards.rs @@ -310,7 +310,7 @@ fn core_badge(it: &DetectItem, color: u32) -> MoonBadge { fn coin_text(it: &DetectItem, p: MoonPalette, cx: &App) -> MoonText { MoonText::new(it.base.clone()) .color(p.text) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 15.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 15.0)) .weight(600.0) .mono(true) .uppercase(false) @@ -320,7 +320,7 @@ fn coin_text(it: &DetectItem, p: MoonPalette, cx: &App) -> MoonText { fn muted(text: String, p: MoonPalette, cx: &App) -> MoonText { MoonText::new(text) .color(p.text_muted) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 11.0)) .mono(true) .uppercase(false) } @@ -329,7 +329,7 @@ fn muted(text: String, p: MoonPalette, cx: &App) -> MoonText { fn soft(text: String, p: MoonPalette, cx: &App) -> MoonText { MoonText::new(text) .color(p.text_soft) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 11.0)) .mono(true) .uppercase(false) } @@ -350,7 +350,7 @@ fn delta_chip(val: f32, over: bool, decimals: usize, p: MoonPalette, cx: &App) - }; let text = MoonText::new(label) .color(col) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 11.0)) .weight(700.0) .mono(true) .uppercase(false); diff --git a/crates/moon-ui-gpui/src/panels/label_edit/body.rs b/crates/moon-ui-gpui/src/panels/label_edit/body.rs index aa41faa97..064ac2c46 100644 --- a/crates/moon-ui-gpui/src/panels/label_edit/body.rs +++ b/crates/moon-ui-gpui/src/panels/label_edit/body.rs @@ -80,7 +80,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl div().flex_1().child( MoonInput::new("le-name") .state(&name_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child({ diff --git a/crates/moon-ui-gpui/src/panels/log/view.rs b/crates/moon-ui-gpui/src/panels/log/view.rs index 3800571db..44e431213 100644 --- a/crates/moon-ui-gpui/src/panels/log/view.rs +++ b/crates/moon-ui-gpui/src/panels/log/view.rs @@ -66,7 +66,7 @@ impl Render for LogPanel { div().w(px(180.0)).child( MoonInput::new("log-query") .state(&self.query) - .size(crate::design::input_tier(cx)) + .size(crate::design::INPUT_SIZE) .cleanable(true), ), ) diff --git a/crates/moon-ui-gpui/src/panels/news/mod.rs b/crates/moon-ui-gpui/src/panels/news/mod.rs index 197fec56c..bcd1d453e 100644 --- a/crates/moon-ui-gpui/src/panels/news/mod.rs +++ b/crates/moon-ui-gpui/src/panels/news/mod.rs @@ -1246,7 +1246,7 @@ impl Render for NewsView { div().w(design::font_w_px(cx, 150.0)).flex_none().child( MoonInput::new("news-search") .state(&self.query) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ) 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 51b527f70..ea2ec315b 100644 --- a/crates/moon-ui-gpui/src/panels/order_edit/body.rs +++ b/crates/moon-ui-gpui/src/panels/order_edit/body.rs @@ -130,7 +130,7 @@ 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) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ), ) @@ -188,7 +188,7 @@ pub(super) fn dialog_body(state: &Entity, cx: &mut App) -> AnyEl host.child( MoonInput::new(id) .state(input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .disabled(!enabled) .mono(true), ) diff --git a/crates/moon-ui-gpui/src/panels/report/controls.rs b/crates/moon-ui-gpui/src/panels/report/controls.rs index bef9a845d..9118677d9 100644 --- a/crates/moon-ui-gpui/src/panels/report/controls.rs +++ b/crates/moon-ui-gpui/src/panels/report/controls.rs @@ -631,7 +631,7 @@ impl ReportPanel { .child( MoonInput::new("rep-strategy-mask") .state(&self.strategy_name_mask_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ) .into_any_element() diff --git a/crates/moon-ui-gpui/src/panels/report/render.rs b/crates/moon-ui-gpui/src/panels/report/render.rs index 7d38d268e..760433726 100644 --- a/crates/moon-ui-gpui/src/panels/report/render.rs +++ b/crates/moon-ui-gpui/src/panels/report/render.rs @@ -320,7 +320,7 @@ impl Render for ReportPanel { div().w(design::ui_px(cx, 90.0)).child( MoonInput::new("rep-coin") .state(&self.coin) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ) diff --git a/crates/moon-ui-gpui/src/screener/view.rs b/crates/moon-ui-gpui/src/screener/view.rs index ecd4e1451..f9ace62e0 100644 --- a/crates/moon-ui-gpui/src/screener/view.rs +++ b/crates/moon-ui-gpui/src/screener/view.rs @@ -638,7 +638,7 @@ impl ScreenerView { div().w(px(90.0)).child( MoonInput::new("scr-coin") .state(&self.coin_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ) @@ -647,7 +647,7 @@ impl ScreenerView { div().w(px(90.0)).child( MoonInput::new("scr-dvol") .state(&self.dvol_input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ) diff --git a/crates/moon-ui-gpui/src/settings/apply.rs b/crates/moon-ui-gpui/src/settings/apply.rs index ac094428f..ec2356b5e 100644 --- a/crates/moon-ui-gpui/src/settings/apply.rs +++ b/crates/moon-ui-gpui/src/settings/apply.rs @@ -284,9 +284,8 @@ impl SettingsView { // avoid a second publish when the struct branch below already published one for the same // save (a membership edit bundled with a structural one). let membership_changed = membership_changed(&before.servers, &after.servers); - let ui_theme_changed = before.ui_density != after.ui_density - || before.ui_theme_mode != after.ui_theme_mode - || before.ui_scale != after.ui_scale; + let ui_theme_changed = + before.ui_theme_mode != after.ui_theme_mode || before.ui_scale != after.ui_scale; if ui_theme_changed { crate::install_moon_theme_for_config(&after, cx); diff --git a/crates/moon-ui-gpui/src/settings/badges.rs b/crates/moon-ui-gpui/src/settings/badges.rs index e36a674dd..df6cf2993 100644 --- a/crates/moon-ui-gpui/src/settings/badges.rs +++ b/crates/moon-ui-gpui/src/settings/badges.rs @@ -354,7 +354,7 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-ord-{idx}"))) .state(&row.ordinal) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ), ) @@ -362,7 +362,7 @@ impl SettingsView { div().flex_none().w(px(120.0)).child( MoonInput::new(SharedString::from(format!("badge-name-{idx}"))) .state(&row.name) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ), ) @@ -376,7 +376,7 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-code-{idx}"))) .state(&row.code) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ), ) @@ -391,7 +391,7 @@ impl SettingsView { .child( MoonInput::new(SharedString::from(format!("badge-codeshort-{idx}"))) .state(&row.code_short) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true), ), ) diff --git a/crates/moon-ui-gpui/src/settings/common.rs b/crates/moon-ui-gpui/src/settings/common.rs index 999a8fe8e..31bbf98c5 100644 --- a/crates/moon-ui-gpui/src/settings/common.rs +++ b/crates/moon-ui-gpui/src/settings/common.rs @@ -87,7 +87,7 @@ pub(super) fn slider_row( let min = format(*range.start()); let max = format(*range.end()); let val = format(val); - let endpoint_font = design::tier_font_size(cx) - 2.0; + let endpoint_font = design::BODY_TEXT - 2.0; let scale_w = design::ui_text_width_zoomed(cx, &min, endpoint_font, 400.0, true) + design::ui_text_width_zoomed(cx, &max, endpoint_font, 400.0, true) + design::ui_value(cx, 10.0); diff --git a/crates/moon-ui-gpui/src/settings/connections/columns.rs b/crates/moon-ui-gpui/src/settings/connections/columns.rs index cc5922765..dc5b17864 100644 --- a/crates/moon-ui-gpui/src/settings/connections/columns.rs +++ b/crates/moon-ui-gpui/src/settings/connections/columns.rs @@ -42,13 +42,20 @@ pub(super) const CONN_INDENT_BORDER: f32 = 1.0; /// The three indent parts sum to [`CONN_TABLE_INSET`]; `tests` proves it. pub(super) const CONN_INDENT_PAD: f32 = CONN_TABLE_INSET - CONN_INDENT_MARGIN - CONN_INDENT_BORDER; -/// Budget for 21 Geist Mono glyphs at the most demanding normalized body size (Compact's -/// 12px body and 1.0 text scale), plus 2px rounding room. The endpoint cell has no copy affix -/// or horizontal padding. Keep its smaller basis so narrow-window shrink weights do not change. -const ENDPOINT_CONTENT_CAP: f32 = 153.2; +/// Width of 21 Geist Mono glyphs (the longest IPv4 endpoint) at the body size, normalized by the +/// text ratio the column's `TextScaled` policy multiplies back in (`tokens.font(10) / 10` at the +/// design's font delta). The endpoint cell has no copy affix or horizontal padding. +const ENDPOINT_CONTENT: f32 = + 21.0 * 0.6 * crate::design::BODY_TEXT * 10.0 / (10.0 + crate::design::DESIGN_FONT_DELTA); -/// Five glyphs (`8/8`, space and caret) at the largest density's 16px reference font, -/// MoonUI's 14px trigger padding and 2px rounding room. The trigger scales this budget itself. +/// [`ENDPOINT_CONTENT`] plus 2px rounding room: the cap the column grows to and no further, so a +/// wide window hands the rest to the name column. The basis stays the bare content so narrow-window +/// shrink weights sit just under it. +const ENDPOINT_CONTENT_CAP: f32 = ENDPOINT_CONTENT + 2.0; + +/// Five glyphs (`8/8`, space and caret) at MoonUI's 16px `Md` reference font — a ceiling above +/// the design's own control font — MoonUI's 14px trigger padding and 2px rounding room. The +/// trigger scales this budget itself. const DATA_TRIGGER_BASIS: f32 = 5.0 * 0.6 * 16.0 + 14.0 + 2.0; /// Where a column's header label sits over the control below it. @@ -206,7 +213,7 @@ const CONN_COLS: [ConnCol; 14] = [ id: "h-endpoint", label: Some("conn.col.endpoint"), tip: Some("conn.tip.endpoint"), - basis: 140.0, + basis: ENDPOINT_CONTENT, grow: true, max: Some(ENDPOINT_CONTENT_CAP), width: ConnColWidth::TextScaled, diff --git a/crates/moon-ui-gpui/src/settings/connections/columns/tests.rs b/crates/moon-ui-gpui/src/settings/connections/columns/tests.rs index 59bb20902..a978eb1b9 100644 --- a/crates/moon-ui-gpui/src/settings/connections/columns/tests.rs +++ b/crates/moon-ui-gpui/src/settings/connections/columns/tests.rs @@ -142,7 +142,8 @@ fn caps_match_the_text_column_contract_at_each_font_scale() { for column in ConnColId::ALL { let expected_cap = match column { ConnColId::Key => Some(260.0), - ConnColId::Endpoint => Some(153.2), + // 21 glyphs at 0.6 em of the 14px body, normalized by the 1.3 text ratio, plus 2. + ConnColId::Endpoint => Some(21.0 * 0.6 * 14.0 * 10.0 / 13.0 + 2.0), ConnColId::Group => Some(140.0), _ => None, }; @@ -183,32 +184,32 @@ fn caps_match_the_text_column_contract_at_each_font_scale() { /// Raising the endpoint cap back to 220 wastes name-column space; lowering it below the /// longest IPv4 endpoint truncates legal addresses even when the window has spare width. -/// Exercise the actual presentation tokens so a density mapping change cannot hide either. +/// Exercise the actual presentation tokens so a change to the design's size system cannot hide +/// either; the zoom loop pins that the UI scale never reaches the token channels. #[test] fn endpoint_cap_fits_ipv4_without_consuming_spare_name_width() { - use moon_core::config::{UiDensity, UiThemeMode}; + use moon_core::config::UiThemeMode; let mut required_reference = 0.0_f32; - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - for zoom in [0.75, 1.0, 1.5] { - let tokens = crate::startup::moon_theme_config_for_presentation( - UiThemeMode::Dark, - density, - zoom, - ) - .dark; - let scale = tokens.font(10.0) / 10.0; - let body_size = tokens.ui(tokens.tier().control_metrics().font_size); - let content = "255.255.255.255:65535".chars().count() as f32 * 0.6 * body_size; - required_reference = required_reference.max(content / scale); - let cap = ConnColId::Endpoint - .max_width(MicroTriggerMetrics { - scale, - min_width: 0.0, - }) - .expect("endpoint must leave spare width for name"); - assert!(cap >= content, "IPv4 clipped at {density:?}/{zoom}"); - } + for zoom in [0.75, 1.0, 1.5] { + let tokens = + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, zoom).dark; + assert_eq!( + tokens.font(10.0), + 13.0, + "zoom {zoom} must travel as window zoom, not through the tokens" + ); + let scale = tokens.font(10.0) / 10.0; + let body_size = tokens.ui(tokens.tier().control_metrics().font_size); + let content = "255.255.255.255:65535".chars().count() as f32 * 0.6 * body_size; + required_reference = required_reference.max(content / scale); + let cap = ConnColId::Endpoint + .max_width(MicroTriggerMetrics { + scale, + min_width: 0.0, + }) + .expect("endpoint must leave spare width for name"); + assert!(cap >= content, "IPv4 clipped at zoom {zoom}"); } assert!(ConnColId::Endpoint.spec().max.unwrap() <= required_reference + 2.01); assert!( @@ -217,34 +218,25 @@ fn endpoint_cap_fits_ipv4_without_consuming_spare_name_width() { ); } -/// Restoring Data's 52px basis truncates the count with its caret at Standard and Large. +/// Restoring Data's 52px basis truncates the count with its caret. /// The independent budget follows MoonUI's five mono glyphs and 14px visual padding, /// including its font-channel fitting pass as well as the tier-sized rendered label. #[test] -fn data_trigger_fits_every_count_at_each_density_and_zoom() { - use moon_core::config::{UiDensity, UiThemeMode}; +fn data_trigger_fits_every_count_at_each_zoom() { + use moon_core::config::UiThemeMode; - for density in [UiDensity::Compact, UiDensity::Standard, UiDensity::Large] { - for zoom in [0.75, 1.0, 1.5] { - let tokens = crate::startup::moon_theme_config_for_presentation( - UiThemeMode::Dark, - density, - zoom, - ) - .dark; - let reference_font = tokens.tier().control_metrics().font_size; - let fitting_font = tokens.font(reference_font); - let rendered_font = tokens.ui(reference_font); - let trigger_width = ConnColId::Data.spec().basis * fitting_font / reference_font; - for on in 0..=8 { - let label = format!("{on}/8 \u{25be}"); - let required = label.chars().count() as f32 * 0.6 * fitting_font.max(rendered_font) - + tokens.ui(14.0); - assert!( - trigger_width >= required, - "{on}/8 clipped at {density:?}/{zoom}" - ); - } + for zoom in [0.75, 1.0, 1.5] { + let tokens = + crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, zoom).dark; + let reference_font = tokens.tier().control_metrics().font_size; + let fitting_font = tokens.font(reference_font); + let rendered_font = tokens.ui(reference_font); + let trigger_width = ConnColId::Data.spec().basis * fitting_font / reference_font; + for on in 0..=8 { + let label = format!("{on}/8 \u{25be}"); + let required = label.chars().count() as f32 * 0.6 * fitting_font.max(rendered_font) + + tokens.ui(14.0); + assert!(trigger_width >= required, "{on}/8 clipped at zoom {zoom}"); } } } diff --git a/crates/moon-ui-gpui/src/settings/connections/table.rs b/crates/moon-ui-gpui/src/settings/connections/table.rs index 752475399..01a46f169 100644 --- a/crates/moon-ui-gpui/src/settings/connections/table.rs +++ b/crates/moon-ui-gpui/src/settings/connections/table.rs @@ -314,7 +314,7 @@ fn paste_key_affix( .child( MoonText::new("⧉") .color(p.text_muted) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 11.0)) .mono(true) .uppercase(false) .render(), diff --git a/crates/moon-ui-gpui/src/settings/general.rs b/crates/moon-ui-gpui/src/settings/general.rs index 65ccbc19d..c5df3b84a 100644 --- a/crates/moon-ui-gpui/src/settings/general.rs +++ b/crates/moon-ui-gpui/src/settings/general.rs @@ -1,24 +1,23 @@ //! General-tab editor for personal and machine settings in `settings.toml`. -//! Changes remain in `Backend.preview`. UI theme, density, zoom, control zones and the Main-window +//! Changes remain in `Backend.preview`. UI theme, zoom, control zones and the Main-window //! idle timeout are consumed live from that draft and roll back when Settings closes unsaved; //! other settings take effect after saving and reconciling the relevant runtime state. use gpui::*; use moon_ui::{ - MoonButton, MoonButtonSize, MoonCheckbox, MoonPalette, MoonSegmentItem, MoonSegmentedControl, - MoonSelect, MoonSliderState, MoonTooltipView, StyledExt, h_flex, rgba_from, v_flex, + MoonButton, MoonButtonSize, MoonCheckbox, MoonPalette, MoonSelect, MoonSliderState, + MoonTooltipView, StyledExt, h_flex, rgba_from, v_flex, }; use rust_i18n::t; use super::SettingsView; use crate::{Backend, design}; -use moon_core::config::UiDensity; /// Zoom endpoints shared by the slider state and its displayed captions. -const UI_ZOOM_RANGE: std::ops::RangeInclusive = 0.75..=1.50; - -/// The three settings choices, in the order shown by the segmented control. -const DENSITIES: [UiDensity; 3] = [UiDensity::Compact, UiDensity::Standard, UiDensity::Large]; +/// +/// The range the slider offers, not a bound on the setting: a hand-edited value outside it stays +/// valid (`repair_ui_scale` touches only values that cannot mean anything). +const UI_ZOOM_RANGE: std::ops::RangeInclusive = 0.50..=2.00; /// One bold caption beside a MoonUI select, the General tab's shape for an enum setting. /// @@ -195,13 +194,13 @@ impl SettingsView { rgba_from(p.text_muted, 1.0) }; // All four controls reserve the widest double-arrow label at the active tier. - let font = design::tier_font_size(cx); + let font = design::BODY_TEXT; let label_w = ["<<", ">>"] .into_iter() .map(|label| design::ui_text_width_zoomed(cx, label, font, 400.0, false)) .fold(0.0_f32, f32::max); let button_w = label_w - + 2.0 * design::ui_value(cx, design::button_tier(cx).control_metrics().pad_x) + + 2.0 * design::ui_value(cx, design::CONTROL_TIER.control_metrics().pad_x) + 2.0; // MoonUI draws a one-pixel border on each side, independent of zoom. let btn = |suffix: &'static str, label: &'static str, delta: i32| { MoonButton::new(SharedString::from(format!("{id}{suffix}"))) @@ -230,51 +229,7 @@ impl SettingsView { .child(btn("+large", ">>", large)) } - /// Store the selected density in the draft and reinstall the theme for live preview. - fn set_ui_density(&mut self, density: UiDensity, cx: &mut Context) { - self.backend.update(cx, |b, bcx| { - if let Some(p) = b.preview.as_mut() { - if p.ui_density != density { - p.ui_density = density; - crate::install_moon_theme_for_config(p, bcx); - bcx.notify(); - } - } - }); - cx.notify(); - } - - /// Build the localized three-way density choice from the current settings draft. - fn density_control(&self, cx: &Context) -> impl IntoElement { - let b = self.backend.read(cx); - let selected = b.preview.as_ref().unwrap_or(&b.config).ui_density; - let labels = [ - t!("iface.density_compact").to_string(), - t!("iface.density_standard").to_string(), - t!("iface.density_large").to_string(), - ]; - let view = cx.entity(); - v_flex() - .gap(design::ui_px(cx, 4.0)) - .font_family(design::ui_font()) - .child(t!("iface.density").to_string()) - .child( - MoonSegmentedControl::new("ui-density") - .items(DENSITIES.into_iter().zip(labels).map(|(density, label)| { - MoonSegmentItem::new("", label) - .fit_width(cx, 90.0, 180.0) - .selected(selected == density) - })) - .on_click(move |index, _, _, app| { - if let Some(&density) = DENSITIES.get(index) { - view.update(app, |this, cx| this.set_ui_density(density, cx)); - } - }) - .render(), - ) - } - - /// Build the General tab for UI theme, density and zoom, locale, chart grouping, control zones, + /// Build the General tab for UI theme and zoom, locale, chart grouping, control zones, /// Main-window idle closing, and file-log retention settings. /// /// Args: @@ -313,7 +268,7 @@ impl SettingsView { v_flex() .w_full() .gap_1() - // UI theme, density and zoom are personal settings in settings.toml. The chart theme + // UI theme and zoom are personal settings in settings.toml. The chart theme // is edited on the Interface tab and stored in theme.toml. The selector's own // behaviour -- live preview and rebuilding the per-mode editors -- lives with its // state in `settings/mod.rs`, beside every other dropdown on this tab. @@ -328,12 +283,6 @@ impl SettingsView { &t!("iface.light_theme_hint"), muted, )) - .child(self.density_control(cx)) - .child(settings_hint( - "iface.density_hint", - &t!("iface.density_hint"), - muted, - )) .child(super::slider_row( &t!("iface.ui_zoom"), &self.ui_zoom, @@ -341,6 +290,11 @@ impl SettingsView { zoom_text, cx, )) + .child(settings_hint( + "iface.ui_zoom_hint", + &t!("iface.ui_zoom_hint"), + muted, + )) .child(super::separator(p, cx)) // Interface locale selector. .child(labeled_select("general.language", &self.lang, 220.0, cx)) diff --git a/crates/moon-ui-gpui/src/settings/general/tests.rs b/crates/moon-ui-gpui/src/settings/general/tests.rs index faf4ad7c3..7876e51f4 100644 --- a/crates/moon-ui-gpui/src/settings/general/tests.rs +++ b/crates/moon-ui-gpui/src/settings/general/tests.rs @@ -1,21 +1,22 @@ -//! Density presentation and zoom-caption regressions for the General tab. +//! Zoom presentation and zoom-caption regressions for the General tab. use super::zoom_text; -use moon_core::config::{UiDensity, UiThemeMode}; +use moon_core::config::UiThemeMode; use moon_ui::{MoonSize, MoonTheme}; /// Catches removing percentage conversion or leaking floating step noise into zoom captions. #[test] fn zoom_captions_show_percentages_at_endpoints_and_steps() { - for (scale, text) in [(0.75, "75%"), (1.0, "100%"), (1.05, "105%"), (1.50, "150%")] { + for (scale, text) in [(0.5, "50%"), (1.0, "100%"), (1.05, "105%"), (2.0, "200%")] { assert_eq!(zoom_text(scale), text); } } -/// Catches omitting the font scale or scaling density's font delta twice during theme construction. -/// At 125% zoom, Standard's 10px base plus 3px density adjustment must render as 16.25px. +/// Catches the slider's scale reaching MoonUI's token multipliers again instead of the window +/// content zoom: at 125% the tokens must still hand every design value through unscaled, or +/// the window would scale twice. #[test] -fn density_and_zoom_scale_installed_text_proportionally() { +fn zoom_is_installed_on_the_window_not_the_tokens() { for mode in [ UiThemeMode::Dark, UiThemeMode::Graphite, @@ -23,23 +24,18 @@ fn density_and_zoom_scale_installed_text_proportionally() { UiThemeMode::DarkExperimental, UiThemeMode::LightExperimental, ] { - for (density, tier, text_px) in [ - (UiDensity::Compact, MoonSize::Xs, 12.5), - (UiDensity::Standard, MoonSize::Sm, 16.25), - (UiDensity::Large, MoonSize::Md, 20.0), - ] { - let theme = MoonTheme::from_config(crate::startup::moon_theme_config_for_presentation( - mode, density, 1.25, - )); - let tokens = if mode.is_light() { - &theme.config.light - } else { - &theme.config.dark - }; - assert_eq!(tokens.tier(), tier); - assert_eq!(tokens.font(10.0), text_px); - assert_eq!(tokens.ui(20.0), 25.0); - } + let theme = MoonTheme::from_config(crate::startup::moon_theme_config_for_presentation( + mode, 1.25, + )); + let tokens = if mode.is_light() { + &theme.config.light + } else { + &theme.config.dark + }; + assert_eq!(tokens.zoom(), 1.25); + assert_eq!(tokens.tier(), MoonSize::Sm); + assert_eq!(tokens.font(10.0), 13.0); + assert_eq!(tokens.ui(20.0), 20.0); } } diff --git a/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs b/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs index ee02e38c7..6f1795f93 100644 --- a/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs +++ b/crates/moon-ui-gpui/src/settings/hotkeys/tab.rs @@ -124,7 +124,7 @@ fn muted_line(text: String, p: &MoonPalette, cx: &App) -> impl IntoElement { .wrap() // The one size the whole tab uses: titles, surfaces, captions and the header alike. A // caption one step smaller was tried and read as a different font. - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text_muted) .render() } @@ -138,9 +138,8 @@ fn muted_line(text: String, p: &MoonPalette, cx: &App) -> impl IntoElement { /// move the column. Per-glyph cached in `design::ui_text_width`, so one Settings render costs a /// hash lookup per character, not a shaping call. fn scope_column_px(cx: &App) -> Pixels { - let width = |text: &str| { - design::ui_text_width_zoomed(cx, text, design::tier_font_size(cx), 400.0, false) - }; + let width = + |text: &str| design::ui_text_width_zoomed(cx, text, design::BODY_TEXT, 400.0, false); let widest = registry::rows() .iter() .filter_map(|row| match row { @@ -217,7 +216,7 @@ impl SettingsView { MoonText::new(t!("hotkeys.group.builtin").to_string()) .uppercase(false) .mono(false) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text) .render(), ) @@ -327,7 +326,7 @@ impl SettingsView { .uppercase(false) .mono(false) .wrap() - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text_muted) .render(), ) @@ -348,7 +347,7 @@ impl SettingsView { .uppercase(false) .mono(false) .wrap() - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(color) .render() .into_any_element() @@ -483,7 +482,7 @@ impl SettingsView { .uppercase(false) .mono(row.mono_title) .wrap() - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(if row.muted { p.text_muted } else { p.text }) .render(), ) @@ -714,7 +713,7 @@ impl SettingsView { MoonText::new(t!("hotkeys.move_kind.title").to_string()) .uppercase(false) .mono(false) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 12.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 12.0)) .color(p.text_muted) .render() .into_any_element() @@ -1072,7 +1071,7 @@ impl SettingsView { MoonText::new("->") .uppercase(false) .mono(true) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text_muted) .render(), ) @@ -1149,7 +1148,7 @@ impl SettingsView { MoonText::new("->") .uppercase(false) .mono(true) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text_muted) .render(), ) @@ -1266,7 +1265,7 @@ impl SettingsView { MoonText::new(t!("hotkeys.pull.gestures").to_string()) .uppercase(false) .mono(false) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 14.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 14.0)) .color(p.text) .render() .into_any_element(), diff --git a/crates/moon-ui-gpui/src/settings/mod.rs b/crates/moon-ui-gpui/src/settings/mod.rs index 8c1931b25..b4535a84a 100644 --- a/crates/moon-ui-gpui/src/settings/mod.rs +++ b/crates/moon-ui-gpui/src/settings/mod.rs @@ -644,7 +644,6 @@ fn settings_sig(b: &Backend) -> u64 { cfg.chart_stack_height.hash(&mut h); cfg.log_to_file.hash(&mut h); cfg.log_retention_days.hash(&mut h); - cfg.ui_density.hash(&mut h); cfg.ui_theme_mode.hash(&mut h); cfg.ui_scale.to_bits().hash(&mut h); cfg.hotkeys.hash(&mut h); @@ -862,7 +861,6 @@ fn draft_sig(cfg: &AppConfig) -> u64 { cfg.log_to_file.hash(&mut h); cfg.log_retention_days.hash(&mut h); cfg.chart_memory_percent.hash(&mut h); - cfg.ui_density.hash(&mut h); cfg.ui_scale.to_bits().hash(&mut h); h.finish() diff --git a/crates/moon-ui-gpui/src/settings/security.rs b/crates/moon-ui-gpui/src/settings/security.rs index c74429308..a389e9d3a 100644 --- a/crates/moon-ui-gpui/src/settings/security.rs +++ b/crates/moon-ui-gpui/src/settings/security.rs @@ -259,7 +259,7 @@ fn password_row( div().w(design::font_w_px(cx, FIELD_W)).child( MoonInput::new(id) .state(state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .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 diff --git a/crates/moon-ui-gpui/src/settings/telegram.rs b/crates/moon-ui-gpui/src/settings/telegram.rs index 856b3203b..93875fb26 100644 --- a/crates/moon-ui-gpui/src/settings/telegram.rs +++ b/crates/moon-ui-gpui/src/settings/telegram.rs @@ -287,7 +287,7 @@ impl SettingsView { div().w(design::font_w_px(cx, TOKEN_FIELD_W)).child( MoonInput::new("telegram-token") .state(&self.telegram.token) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true) .mask_toggle(), ), diff --git a/crates/moon-ui-gpui/src/settings/telegram/access.rs b/crates/moon-ui-gpui/src/settings/telegram/access.rs index a41fb713b..4b37fe999 100644 --- a/crates/moon-ui-gpui/src/settings/telegram/access.rs +++ b/crates/moon-ui-gpui/src/settings/telegram/access.rs @@ -164,7 +164,7 @@ impl SettingsView { MoonInput::new("tg-chat-name") .state(&self.telegram.name) .placeholder(t!("telegram.access_name_placeholder").to_string()) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ); if !owner { card = card.child(self.telegram_core_access(chat, cx)); @@ -298,7 +298,7 @@ impl SettingsView { .child( MoonInput::new("tg-core-search") .state(&self.telegram.search) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .placeholder(t!("telegram.access_search").to_string()), ) .child( diff --git a/crates/moon-ui-gpui/src/settings/telegram/core_section.rs b/crates/moon-ui-gpui/src/settings/telegram/core_section.rs index 6a798b511..a0a05e79b 100644 --- a/crates/moon-ui-gpui/src/settings/telegram/core_section.rs +++ b/crates/moon-ui-gpui/src/settings/telegram/core_section.rs @@ -443,7 +443,7 @@ impl SettingsView { let p = MoonPalette::active(cx); let mut input = MoonInput::new(id) .state(state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .disabled(!can_edit) .mono(true); if masked { diff --git a/crates/moon-ui-gpui/src/settings/telegram/core_section/login_steps.rs b/crates/moon-ui-gpui/src/settings/telegram/core_section/login_steps.rs index d73163947..f94e69716 100644 --- a/crates/moon-ui-gpui/src/settings/telegram/core_section/login_steps.rs +++ b/crates/moon-ui-gpui/src/settings/telegram/core_section/login_steps.rs @@ -658,7 +658,7 @@ impl SettingsView { ) -> impl IntoElement { let mut input = MoonInput::new(id) .state(state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .mono(true); if masked { input = input.mask_toggle(); diff --git a/crates/moon-ui-gpui/src/shell/core_settings_popup/general.rs b/crates/moon-ui-gpui/src/shell/core_settings_popup/general.rs index b5a20cae8..02bb1dcc4 100644 --- a/crates/moon-ui-gpui/src/shell/core_settings_popup/general.rs +++ b/crates/moon-ui-gpui/src/shell/core_settings_popup/general.rs @@ -240,7 +240,7 @@ pub(super) fn general_tab( } else { MoonInput::new("core-bl-text") .state(editors.input) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .into_any_element() }; let risks = popup_group( @@ -382,7 +382,7 @@ pub(super) fn general_tab( .items_center() .gap(design::ui_px(cx, 6.0)) .child(caption(t!("core_settings.lev_config").to_string(), p, cx)) - .children(stretch_field(widgets, "gen-lev-config", cx)), + .children(stretch_field(widgets, "gen-lev-config")), ), ); diff --git a/crates/moon-ui-gpui/src/shell/core_settings_popup/widgets.rs b/crates/moon-ui-gpui/src/shell/core_settings_popup/widgets.rs index 542d59f2c..1312606de 100644 --- a/crates/moon-ui-gpui/src/shell/core_settings_popup/widgets.rs +++ b/crates/moon-ui-gpui/src/shell/core_settings_popup/widgets.rs @@ -335,7 +335,7 @@ pub(super) fn num(widgets: &SettingsWidgets, id: &'static str, cx: &App) -> Opti .child( MoonInput::new(SharedString::from(id)) .state(&f.state) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .into_any_element(), ) @@ -359,15 +359,10 @@ pub(super) fn slider(widgets: &SettingsWidgets, id: &'static str) -> Option Option { +pub(super) fn stretch_field(widgets: &SettingsWidgets, id: &'static str) -> Option { let f = widgets.field(id)?; Some( div() @@ -375,7 +370,7 @@ pub(super) fn stretch_field( .child( MoonInput::new(SharedString::from(id)) .state(&f.state) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .into_any_element(), ) diff --git a/crates/moon-ui-gpui/src/shell/quiet_popup.rs b/crates/moon-ui-gpui/src/shell/quiet_popup.rs index f774bc482..19ba944ff 100644 --- a/crates/moon-ui-gpui/src/shell/quiet_popup.rs +++ b/crates/moon-ui-gpui/src/shell/quiet_popup.rs @@ -160,7 +160,7 @@ impl Shell { div().w(design::font_w_px(cx, TIME_FIELD_W)).child( MoonInput::new("quiet-from") .state(&self.quiet_from_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) .child(quiet_field_label(t!("quiet.to").to_string(), cx)) @@ -168,7 +168,7 @@ impl Shell { div().w(design::font_w_px(cx, TIME_FIELD_W)).child( MoonInput::new("quiet-to") .state(&self.quiet_to_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ), ) @@ -200,7 +200,7 @@ impl Shell { MoonInput::new("quiet-charts") .state(&self.quiet_charts_input) .placeholder(t!("quiet.charts_hint").to_string()) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .child( div() diff --git a/crates/moon-ui-gpui/src/shell/ticker.rs b/crates/moon-ui-gpui/src/shell/ticker.rs index cde740505..8bc2b7033 100644 --- a/crates/moon-ui-gpui/src/shell/ticker.rs +++ b/crates/moon-ui-gpui/src/shell/ticker.rs @@ -207,7 +207,7 @@ impl Shell { .child( MoonInput::new("header-ticker-query") .state(&self.ticker_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ) .child(list), ) diff --git a/crates/moon-ui-gpui/src/startup.rs b/crates/moon-ui-gpui/src/startup.rs index faa6de121..fbffed555 100644 --- a/crates/moon-ui-gpui/src/startup.rs +++ b/crates/moon-ui-gpui/src/startup.rs @@ -254,50 +254,36 @@ pub(crate) fn moon_theme_config_for_mode(mode: UiThemeMode) -> MoonThemeConfig { theme } -/// Map persisted density onto MoonUI's tier and remaining non-tier text metrics. +/// Map the persisted presentation onto MoonUI's theme: the palette by `mode`, the design's own +/// tier and text delta fixed, and `ui_scale` installed as the window content zoom. /// -/// UI zoom scales geometry, text and the density's font delta proportionally. Standard retains -/// its +3 logical pixels at 100% zoom. The login window and shell share this mapping. +/// The zoom scales every window as a whole — text, geometry, images, hit areas — so both token +/// multipliers stay at 1.0 and every design value reaches MoonUI unscaled; see +/// [`crate::design::CONTROL_TIER`]. MoonUI replaces an impossible zoom with 1.0 itself. The login +/// window and the shell share this mapping. /// /// Args: /// mode: Interface palette to use. -/// density: Control tier and unscaled font delta. -/// ui_scale: Zoom multiplier; non-finite or non-positive values fall back to 1.0. +/// ui_scale: Window content zoom; non-finite or non-positive values fall back to 1.0. /// /// Returns: -/// A theme with matching geometry and text zoom on both palettes. +/// A theme carrying the zoom on both palettes. pub(crate) fn moon_theme_config_for_presentation( mode: UiThemeMode, - density: moon_core::config::UiDensity, ui_scale: f32, ) -> MoonThemeConfig { - use moon_core::config::UiDensity; - use moon_ui::MoonSize; - let ui_scale = if ui_scale.is_finite() && ui_scale > 0.0 { - ui_scale - } else { - 1.0 - }; - let (tier, font_delta) = match density { - UiDensity::Compact => (MoonSize::Xs, 0.0), - UiDensity::Standard => (MoonSize::Sm, 3.0), - UiDensity::Large => (MoonSize::Md, 6.0), - }; - let mut theme = moon_theme_config_for_mode(mode) - .with_tier(tier) - .with_font_delta(font_delta * ui_scale) - .with_ui_scale(ui_scale); - theme.dark.scale.font = ui_scale; - theme.light.scale.font = ui_scale; - theme + moon_theme_config_for_mode(mode) + .with_tier(crate::design::CONTROL_TIER) + .with_font_delta(crate::design::DESIGN_FONT_DELTA) + .with_zoom(ui_scale) } -/// Return the theme for the full configuration, including density and UI zoom. +/// Return the theme for the full configuration, including the UI scale. pub(crate) fn moon_theme_config_for(cfg: &AppConfig) -> MoonThemeConfig { - moon_theme_config_for_presentation(cfg.ui_theme_mode, cfg.ui_density, cfg.ui_scale) + moon_theme_config_for_presentation(cfg.ui_theme_mode, cfg.ui_scale) } -/// Install the configured palette, density and zoom into the active MoonUI theme. +/// Install the configured palette and UI scale into the active MoonUI theme. pub(crate) fn install_moon_theme_for_config(cfg: &AppConfig, cx: &mut App) { MoonTheme::install_config(moon_theme_config_for(cfg), cx); } diff --git a/crates/moon-ui-gpui/src/startup/fixture.rs b/crates/moon-ui-gpui/src/startup/fixture.rs index 0cb0eca12..faa2eb9f0 100644 --- a/crates/moon-ui-gpui/src/startup/fixture.rs +++ b/crates/moon-ui-gpui/src/startup/fixture.rs @@ -85,16 +85,29 @@ where let _ = std::fs::write(¬e, format!("--fixture {name}: {error}\n")); eprintln!("--fixture {name}: {error} (см. {})", note.display()); })?; - // A layout the bench should open with. The bench's `cfg/` is fresh on every run, so a - // setting that lives in `layout.toml` — a bottom-volume style, a band height — can only reach - // it by hand through the popup, which a scripted or blind run cannot do. `MOON_FIXTURE_LAYOUT` - // names a file to copy in BEFORE the configuration is read; the rest of the bench is untouched. - if let Some(from) = std::env::var_os("MOON_FIXTURE_LAYOUT") { - let to = moon_core::config::paths::layout_path(); + // A layout or settings file the bench should open with. The bench's `cfg/` is fresh on every + // run, so a setting that lives in `layout.toml` — a bottom-volume style, a band height — or in + // `settings.toml` — the UI scale a scripted run is to be checked at — can only reach it by + // hand, which a scripted or blind run cannot do. `MOON_FIXTURE_LAYOUT` and + // `MOON_FIXTURE_SETTINGS` each name a file to copy in BEFORE the configuration is read; the + // rest of the bench is untouched. + for (var, to) in [ + ( + "MOON_FIXTURE_LAYOUT", + moon_core::config::paths::layout_path(), + ), + ( + "MOON_FIXTURE_SETTINGS", + moon_core::config::paths::settings_path(), + ), + ] { + let Some(from) = std::env::var_os(var) else { + continue; + }; if let Err(error) = std::fs::copy(&from, &to) { let note = std::env::temp_dir().join("moonterminal-fixture-error.log"); let message = format!( - "--fixture {name}: MOON_FIXTURE_LAYOUT {} -> {}: {error} + "--fixture {name}: {var} {} -> {}: {error} ", std::path::Path::new(&from).display(), to.display() diff --git a/crates/moon-ui-gpui/src/startup/tests.rs b/crates/moon-ui-gpui/src/startup/tests.rs index 81e884e7c..affa11ded 100644 --- a/crates/moon-ui-gpui/src/startup/tests.rs +++ b/crates/moon-ui-gpui/src/startup/tests.rs @@ -7,41 +7,36 @@ use std::time::{Duration, Instant}; use super::{ReportRevisionDecision, ReportRevisionGate, TickEdges, consume_report_commit}; -/// Removing either font multiplier assignment or leaving the density delta unscaled makes -/// non-tier text ignore zoom or grow out of proportion to tier-sized controls. +/// Catches `startup.rs:moon_theme_config_for_presentation` putting the UI scale back into MoonUI's +/// token multipliers, or dropping the design's fixed tier and text delta: the scale must travel +/// as the window content zoom alone, or every window would scale twice. #[test] -fn presentation_zoom_scales_text_and_density_on_both_palettes() { - use moon_core::config::{UiDensity, UiThemeMode}; +fn presentation_installs_the_scale_as_window_zoom_only() { + use moon_core::config::UiThemeMode; + use moon_ui::MoonSize; for mode in [UiThemeMode::Dark, UiThemeMode::Graphite, UiThemeMode::Light] { - for (density, delta) in [ - (UiDensity::Compact, 0.0), - (UiDensity::Standard, 4.5), - (UiDensity::Large, 9.0), - ] { - let theme = super::moon_theme_config_for_presentation(mode, density, 1.5); - for scale in [theme.dark.scale, theme.light.scale] { - assert_eq!(scale.ui, 1.5); - assert_eq!(scale.font, 1.5); - assert_eq!(scale.font_delta, delta); - } + let theme = super::moon_theme_config_for_presentation(mode, 1.5); + for scale in [theme.dark.scale, theme.light.scale] { + assert_eq!(scale.zoom, 1.5); + assert_eq!(scale.ui, 1.0); + assert_eq!(scale.font, 1.0); + assert_eq!(scale.font_delta, 3.0); + assert_eq!(scale.tier, MoonSize::Sm); } } } -/// Bypassing the shared scale guard lets invalid settings poison text metrics even when -/// MoonUI's geometry setter falls back, leaving text and controls at different zoom levels. +/// Catches bypassing MoonUI's zoom guard: an invalid stored scale must fall back to 1.0 on both +/// palettes rather than reach the windows. #[test] -fn presentation_zoom_rejects_non_finite_and_non_positive_scales() { - use moon_core::config::{UiDensity, UiThemeMode}; +fn presentation_rejects_non_finite_and_non_positive_scales() { + use moon_core::config::UiThemeMode; for zoom in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, -1.0] { - let theme = - super::moon_theme_config_for_presentation(UiThemeMode::Dark, UiDensity::Standard, zoom); + let theme = super::moon_theme_config_for_presentation(UiThemeMode::Dark, zoom); for scale in [theme.dark.scale, theme.light.scale] { - assert_eq!(scale.ui, 1.0); - assert_eq!(scale.font, 1.0); - assert_eq!(scale.font_delta, 3.0); + assert_eq!(scale.zoom, 1.0); } } } diff --git a/crates/moon-ui-gpui/src/startup/unlock.rs b/crates/moon-ui-gpui/src/startup/unlock.rs index 9f3276308..c28207f89 100644 --- a/crates/moon-ui-gpui/src/startup/unlock.rs +++ b/crates/moon-ui-gpui/src/startup/unlock.rs @@ -198,17 +198,13 @@ fn fail_to_start(error: anyhow::Error, cx: &mut App) { /// Install the interface theme for a window shown before the configuration is available. /// /// Reads the same plaintext `settings.toml` values the shell uses, so the login window matches the -/// user's theme choice and font size instead of flashing defaults and then re-themeing. +/// user's theme choice and UI scale instead of flashing defaults and then re-themeing. fn install_login_theme(cx: &mut App) { let prefs = moon_core::config::presentation_prefs(); // The mapping itself lives once, beside the shell's own call site. rust_i18n::set_locale(prefs.language.code()); moon_ui::MoonTheme::install_config( - super::moon_theme_config_for_presentation( - prefs.ui_theme_mode, - prefs.ui_density, - prefs.ui_scale, - ), + super::moon_theme_config_for_presentation(prefs.ui_theme_mode, prefs.ui_scale), cx, ); } diff --git a/crates/moon-ui-gpui/src/strategies/params.rs b/crates/moon-ui-gpui/src/strategies/params.rs index 5cd2f4f7d..65f8c2a35 100644 --- a/crates/moon-ui-gpui/src/strategies/params.rs +++ b/crates/moon-ui-gpui/src/strategies/params.rs @@ -1132,7 +1132,7 @@ impl StrategiesView { .child( MoonInput::new("strat-list-tokens") .state(&content_input) - .size(design::input_tier(cx)), + .size(design::INPUT_SIZE), ), ) }) @@ -1899,7 +1899,7 @@ impl StrategiesView { ); let mut input = MoonInput::new(SharedString::from(format!("field-input-{row_id}"))) .state(&state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .tone(MoonTone::Warning) .selected(dirty || differ) .disabled(!active); @@ -2049,7 +2049,7 @@ impl StrategiesView { let mut input = MoonInput::new(SharedString::from(format!("field-input-{row_id}"))) .state(&state) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) // No colour case here: a colour field draws its own input in the arm // above, so this one only ever renders free text. .tone(if rejected { diff --git a/crates/moon-ui-gpui/src/strategies/settings.rs b/crates/moon-ui-gpui/src/strategies/settings.rs index 60c9e86c9..6169d983b 100644 --- a/crates/moon-ui-gpui/src/strategies/settings.rs +++ b/crates/moon-ui-gpui/src/strategies/settings.rs @@ -12,8 +12,8 @@ use moon_core::config::layout::{ STRATEGIES_TREE_TEXT_STEP_MIN, WindowLayout, clamp_strategies_tree_text_step, }; use moon_ui::{ - MoonCheckbox, MoonGroupBox, MoonPalette, MoonPopover, MoonPopoverPlacement, MoonSize, - MoonStepper, MoonTheme, h_flex, v_flex, + MoonCheckbox, MoonGroupBox, MoonPalette, MoonPopover, MoonPopoverPlacement, MoonStepper, + MoonTheme, h_flex, v_flex, }; use rust_i18n::t; @@ -367,25 +367,16 @@ fn settings_content_width(cx: &App) -> f32 { .fold(0.0_f32, f32::max); let checkbox_leading = f32::from(design::ui_px( cx, - crate::panels::common::checkbox_metrics(cx).mark - + crate::panels::common::checkbox_metrics(cx).gap, + crate::panels::common::CHECKBOX_METRICS.mark + crate::panels::common::CHECKBOX_METRICS.gap, )); let text_step_label_width = checkbox_face_width(cx, &t!("strat.settings.text_step"), 400.0); - // Reserve the density tier's full stepper width; buttons and value cell share UI zoom. - let tier = tokens - .tier() - .nearest(&[MoonSize::Xs, MoonSize::Sm, MoonSize::Md]); - let height = tier.control_metrics().height; - // Match the stepper's compact/normal value-width ratios; all widths follow UI zoom. The - // gap between the checkbox mark and its label follows the density tier as well. - let ratio = if tier == MoonSize::Md { - 64.0 / 26.0 - } else { - 52.0 / 22.0 - }; - let stepper_reserve_px = tokens.ui(height * 2.0 - + (height * ratio).round() - + crate::panels::common::checkbox_metrics(cx).gap); + // Reserve the stepper's full width at the control tier; buttons and value cell share UI zoom. + let height = design::CONTROL_TIER.control_metrics().height; + // Match the stepper's value-width ratio at that tier; all widths follow UI zoom. The gap + // between the checkbox mark and its label is the control tier's as well. + let ratio = 52.0 / 22.0; + let stepper_reserve_px = tokens + .ui(height * 2.0 + (height * ratio).round() + crate::panels::common::CHECKBOX_METRICS.gap); settings_content_width_value( title_width, group_width, @@ -423,7 +414,7 @@ fn popup_text_width(cx: &App, text: &str, base_size: f32, weight: f32) -> f32 { /// Measure text set in the density-selected checkbox label's face, in the popup's proportional family. /// /// That face follows the UI zoom but not the legacy font-delta channel (see -/// [`crate::panels::common::checkbox_metrics`]), so it is measured apart from +/// [`crate::panels::common::CHECKBOX_METRICS`]), so it is measured apart from /// [`popup_text_width`], which adds that delta. /// /// Args: @@ -437,7 +428,7 @@ fn checkbox_face_width(cx: &App, text: &str, weight: f32) -> f32 { design::ui_text_width_zoomed( cx, text, - crate::panels::common::checkbox_metrics(cx).font, + crate::panels::common::CHECKBOX_METRICS.font, weight, false, ) @@ -507,7 +498,7 @@ fn tree_text_step_row( .truncate() .text_size(design::ui_px( cx, - crate::panels::common::checkbox_metrics(cx).font, + crate::panels::common::CHECKBOX_METRICS.font, )) .child(t!("strat.settings.text_step").to_string()), ) diff --git a/crates/moon-ui-gpui/src/strategies/tree/checks.rs b/crates/moon-ui-gpui/src/strategies/tree/checks.rs index a3cbffcfc..b184ffa95 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/checks.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/checks.rs @@ -12,7 +12,6 @@ //! Both boxes are built here so their size and id convention cannot drift apart, and both //! stage through [`stage_value`], the one rule deciding what a click leaves in `staged`. -use gpui::App; use moon_ui::{MoonCheckbox, MoonTone}; use crate::design; @@ -33,16 +32,15 @@ mod tests; /// Args: /// id: Element id, derived by the caller from the row's own stable node id. /// checked: Value to display; both callers are controlled, so the widget stores nothing. -/// cx: Application context used to read the active density tier. /// /// Returns: /// The configured checkbox, still needing its `on_change`. -pub(super) fn row_checkbox(id: SharedString, checked: bool, cx: &App) -> MoonCheckbox { +pub(super) fn row_checkbox(id: SharedString, checked: bool) -> MoonCheckbox { MoonCheckbox::new(id) .checked(checked) .tone(MoonTone::Positive) // Fixed tree rows and invisible bulk slots share this same tier-following column. - .size(design::choice_tier(cx)) + .size(design::CONTROL_TIER) } /// Choose checkbox tone and whether the core-state dot is green, without changing staging. @@ -93,7 +91,6 @@ pub(super) fn stage_value(clicked: bool, server: bool) -> Option { /// core: Core owning the row. /// path: Folder segments, empty for the core root. /// checked: The row's current switch. -/// cx: Application context used to read the active density tier. /// /// Returns: /// The checkbox wrapped in the press-swallowing container the surrounding row needs. @@ -103,7 +100,6 @@ pub(super) fn bulk_check( core: CoreId, path: Vec, checked: bool, - cx: &App, ) -> AnyElement { let view = view.clone(); div() @@ -118,7 +114,7 @@ pub(super) fn bulk_check( app.stop_propagation(); }) .child( - row_checkbox(SharedString::from(format!("chk:{row_id}")), checked, cx) + row_checkbox(SharedString::from(format!("chk:{row_id}")), checked) // The reported value is ignored on purpose: the painted box is derived from child // strategy checkboxes, so the view recomputes the next bulk value itself and cannot // follow a stale widget value. @@ -143,18 +139,16 @@ pub(super) fn bulk_check( /// /// Args: /// row_id: The row's own tree id, reused so the widget keeps this node's identity. -/// cx: Application context used to read the active density tier. /// /// Returns: /// The invisible checkbox that reserves the column's width. -pub(super) fn bulk_check_slot(row_id: &SharedString, cx: &App) -> AnyElement { +pub(super) fn bulk_check_slot(row_id: &SharedString) -> AnyElement { div() .flex_none() .invisible() .child(row_checkbox( SharedString::from(format!("chk:{row_id}")), false, - cx, )) .into_any_element() } diff --git a/crates/moon-ui-gpui/src/strategies/tree/dialogs.rs b/crates/moon-ui-gpui/src/strategies/tree/dialogs.rs index 3407c78af..2d9e43d95 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/dialogs.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/dialogs.rs @@ -240,19 +240,17 @@ fn tree_op_dialog_width(window: &Window, cx: &App) -> f32 { /// Args: /// id: Element id for the name field (`create-name`, `folder-name`, or `rename-name`). /// input: Shared input state already constructed for this dialog opening. -/// cx: Application context used to select the input's density tier. /// /// Returns: -/// A full-width, shrinkable slot containing the density-tier name field. +/// A full-width, shrinkable slot containing the name field. fn tree_op_name_input( id: impl Into, input: &Entity, - cx: &App, ) -> impl IntoElement { div() .w_full() .min_w_0() - .child(MoonInput::new(id).state(input).size(design::input_tier(cx))) + .child(MoonInput::new(id).state(input).size(design::INPUT_SIZE)) } /// Build the tree-op dialog body for the current `TreeOp`. @@ -359,7 +357,7 @@ fn op_dialog_body( ), ); if let Some(input) = input { - body = body.child(tree_op_name_input("create-name", &input, cx)); + body = body.child(tree_op_name_input("create-name", &input)); } Some(body.into_any_element()) } @@ -375,14 +373,14 @@ fn op_dialog_body( .child(t!("dialogs.into_prefix", path = target_label).to_string()), ); if let Some(input) = input { - body = body.child(tree_op_name_input("folder-name", &input, cx)); + body = body.child(tree_op_name_input("folder-name", &input)); } Some(body.into_any_element()) } TreeOp::RenameFolder { .. } | TreeOp::RenameStrategy { .. } => { let mut body = v_flex().w_full().min_w_0().gap_2(); if let Some(input) = input { - body = body.child(tree_op_name_input("rename-name", &input, cx)); + body = body.child(tree_op_name_input("rename-name", &input)); } Some(body.into_any_element()) } @@ -433,7 +431,7 @@ fn op_dialog_body( .mono(false) .uppercase(false) .color(p.text_soft) - .rendered_metrics(design::tier_text_metrics(cx, -2.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, -2.0, 11.0)) .render(), ); let mut list = v_flex() @@ -469,7 +467,7 @@ fn op_dialog_body( .mono(true) .uppercase(false) .color(p.text) - .rendered_metrics(design::tier_text_metrics(cx, 0.0, 11.0)) + .rendered_metrics(design::text_metrics(cx, 0.0, 11.0)) .render(), ), ), diff --git a/crates/moon-ui-gpui/src/strategies/tree/mod.rs b/crates/moon-ui-gpui/src/strategies/tree/mod.rs index 27d162c6f..95e7eecc6 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/mod.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/mod.rs @@ -190,7 +190,7 @@ impl StrategiesView { div().flex_1().min_w(design::ui_px(cx, SEARCH_MIN_W)).child( MoonInput::new("strat-search") .state(&self.search) - .size(design::input_tier(cx)) + .size(design::INPUT_SIZE) .cleanable(true), ), ) diff --git a/crates/moon-ui-gpui/src/strategies/tree/moon.rs b/crates/moon-ui-gpui/src/strategies/tree/moon.rs index 48a431f8d..899392dee 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/moon.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/moon.rs @@ -1415,7 +1415,7 @@ fn exchange_row( .uppercase(false) .color(p.text_soft) .weight(600.0) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ), @@ -1528,7 +1528,7 @@ fn counts_slot(text: String, width: f32, color: u32, step: f32, app: &App) -> im .mono(true) .uppercase(false) .color(color) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ) @@ -1719,11 +1719,11 @@ fn core_folder_row( }) .child(match check_target.filter(|_| fill.has_contents()) { Some((core, path, checked)) => { - checks::bulk_check(view, &check_row_id, core, path, checked, app) + checks::bulk_check(view, &check_row_id, core, path, checked) } // Reserved rather than omitted, so this row's caption stays on the same control column // as every sibling at its depth. - None => checks::bulk_check_slot(&check_row_id, app), + None => checks::bulk_check_slot(&check_row_id), }) .child( div().flex_1().min_w_0().truncate().child( @@ -1732,7 +1732,7 @@ fn core_folder_row( .uppercase(false) .color(color) .weight(weight) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ), @@ -1746,7 +1746,7 @@ fn core_folder_row( .mono(false) .uppercase(false) .color(p.amber) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ), @@ -1908,7 +1908,7 @@ fn deleted_strategy_row( .mono(true) .uppercase(false) .color(p.text_muted) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ), @@ -1966,7 +1966,7 @@ fn deleted_strategy_row( .mono(true) .uppercase(false) .color(p.text_muted) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ) @@ -2049,7 +2049,7 @@ fn strategy_row( // replaces them. A tone change rather than a badge: the row keeps its shape, // and the tree stays scannable while several rows are marked. .color(if cut { p.text_muted } else { p.text }) - .font_size(design::tier_font_base(app, step)) + .font_size(design::body_font_base(app, step)) .line_height(ROW_LINE_BASE + step) .render(), ), @@ -2121,7 +2121,6 @@ fn strategy_row( checks::row_checkbox( SharedString::from(format!("chk:{}", id_strat(core, id))), val, - app, ) .tone(tone) .on_change(move |ch: &bool, _window, app| { 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 949f17f67..df2c23ecd 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs @@ -285,7 +285,7 @@ impl StrategiesView { fn pane_footer_label_width(&mut self, staged: usize, cx: &App) -> f32 { let locale = rust_i18n::locale(); let locale: &str = &locale; - let font_size = design::button_tier(cx).control_metrics().font_size; + let font_size = design::CONTROL_TIER.control_metrics().font_size; // Fold the density tier (via its control font) and the UI-zoom size the measurement // actually uses: `text_metrics_key` is the font channel, so without the `ui()` bits a // density or zoom change would keep a stale width for every already-cached row. @@ -397,7 +397,7 @@ fn footer_label_width(cx: &App, staged: usize) -> f32 { design::ui_text_width_zoomed( cx, label, - design::button_tier(cx).control_metrics().font_size, + design::CONTROL_TIER.control_metrics().font_size, FOOTER_LABEL_WEIGHT, false, ) @@ -410,7 +410,7 @@ fn footer_label_width(cx: &App, staged: usize) -> f32 { width += design::ui_text_width_zoomed( cx, &t!("strat.staged", n = staged), - design::button_tier(cx).control_metrics().font_size, + design::CONTROL_TIER.control_metrics().font_size, FOOTER_LABEL_WEIGHT, true, ); diff --git a/crates/moon-ui-gpui/src/window/windowing.rs b/crates/moon-ui-gpui/src/window/windowing.rs index d67106e7b..a1326954b 100644 --- a/crates/moon-ui-gpui/src/window/windowing.rs +++ b/crates/moon-ui-gpui/src/window/windowing.rs @@ -736,10 +736,13 @@ pub(crate) fn window_geom_rect(window: &Window, cx: &App) -> moon_core::config:: /// The logical width a window currently OCCUPIES, for responsive chrome. /// -/// `bounds()`, not `window_bounds()`: the latter is the RESTORE rectangle, which for a maximized -/// or fullscreen window names the size it will return to rather than the width being laid out. -/// One home for that distinction, because five surfaces ask the same question and a window that -/// now OPENS maximized would lay out against the wrong number from its very first frame. +/// `viewport_size()`, not `window_bounds()`: the latter is the RESTORE rectangle, which for a +/// maximized or fullscreen window names the size it will return to rather than the width being +/// laid out. And the viewport rather than `bounds()`, because layout happens in content space: +/// under UI zoom the window has fewer content pixels than screen pixels, and the thresholds must +/// compare against the width the row is actually laid out in. One home for that distinction, +/// because five surfaces ask the same question and a window that now OPENS maximized would lay +/// out against the wrong number from its very first frame. /// /// # Arguments /// @@ -747,9 +750,9 @@ pub(crate) fn window_geom_rect(window: &Window, cx: &App) -> moon_core::config:: /// /// # Returns /// -/// The width in logical pixels. +/// The width in content pixels. pub(crate) fn responsive_width(window: &Window) -> f32 { - f32::from(window.bounds().size.width) + f32::from(window.viewport_size().width) } /// Turn remembered window-state flags into the bounds a window should open with. diff --git a/crates/moon-ui-gpui/tests/theme_contract/analytics.rs b/crates/moon-ui-gpui/tests/theme_contract/analytics.rs index 22408c800..8f505f51b 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/analytics.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/analytics.rs @@ -1074,7 +1074,7 @@ 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::button_tier(cx).control_metrics().pad_x", + "design::CONTROL_TIER.control_metrics().pad_x", ".max(design::ui_value(cx, 72.0))", ".width(tab_width)", ".label(title)", @@ -1086,7 +1086,7 @@ fn analytics_tabs_and_core_caption_follow_their_content() { } assert!( body.contains("design::ui_text_width_zoomed(") - && body.contains("design::tier_font_size(cx)") + && body.contains("design::BODY_TEXT") && 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" ); @@ -1115,7 +1115,7 @@ fn analytics_tabs_and_core_caption_follow_their_content() { toolbar.contains("crate::controls::CORE_COMBO_TRIGGER_W") && core_combo.contains(".trigger_width_scaled(CORE_COMBO_TRIGGER_W)") && body.contains( - "let font_size = design::button_tier(cx).control_metrics().font_size.max(1.0);" + "let font_size = design::CONTROL_TIER.control_metrics().font_size.max(1.0);" ) && body.contains("design::ui_value(cx, font_size) / font_size") && body.contains("* action_trigger_scale"), diff --git a/crates/moon-ui-gpui/tests/theme_contract/chart.rs b/crates/moon-ui-gpui/tests/theme_contract/chart.rs index 5aecf6804..6f413e8bd 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/chart.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/chart.rs @@ -1123,7 +1123,7 @@ fn chart_label_name_budget_uses_the_button_s_ui_scale() { let source = code_only(&read_src("chart_tabs/labels_popup/mod.rs")); let budget = braced_body(&source, "pub(super) fn name_budget("); assert!( - budget.contains("design::button_tier(cx).control_metrics().pad_x"), + budget.contains("design::CONTROL_TIER.control_metrics().pad_x"), "name_budget must subtract MoonButton padding with the UI scale" ); } diff --git a/crates/moon-ui-gpui/tests/theme_contract/density.rs b/crates/moon-ui-gpui/tests/theme_contract/density.rs deleted file mode 100644 index 031771b2c..000000000 --- a/crates/moon-ui-gpui/tests/theme_contract/density.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Static contracts for the terminal-owned compact density migration. -//! -//! These assertions land with batch 6 because the bans deliberately stay red until the earlier -//! batches finish moving text and controls onto the density-tier UI channel. - -use super::support::{Path, braced_body, code_only, read_src, rust_sources}; - -/// Terminal production code must not return to the legacy font channel outside its one mirror. -/// -/// Breakage: adding `tokens.font(` or `design::font_value(` to a terminal surface makes Compact -/// text ignore the neighboring control tier, recreating the uneven rows this migration removes. -#[test] -fn terminal_text_uses_no_legacy_font_channel_outside_documented_mirrors() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); - let mut sources = Vec::new(); - rust_sources(&root, &mut sources); - let violations: Vec = sources - .iter() - .filter(|path| path.file_name().is_some_and(|name| name != "tests.rs")) - .filter_map(|path| { - let rel = path - .strip_prefix(&root) - .ok()? - .to_string_lossy() - .replace('\\', "/"); - let code = code_only(&std::fs::read_to_string(path).ok()?); - (!matches!(rel.as_str(), "design.rs" | "settings/connections/table.rs") - && (code.contains("tokens.font(") || code.contains("design::font_value("))) - .then_some(rel) - }) - .collect(); - assert!( - violations.is_empty(), - "legacy font-channel calls outside documented mirrors: {violations:?}" - ); -} - -/// Settings and Analytics tabs must use their density default instead of custom pixel metrics. -/// -/// Breakage: restoring `MoonButtonSize::Custom` pins a tab's text and height while its neighbors -/// resize, so Compact rows again look mismatched and stop aligning. -#[test] -fn settings_and_analytics_tabs_do_not_pin_custom_button_metrics() { - for path in ["settings/render.rs", "analytics/toolbar.rs"] { - assert!( - !code_only(&read_src(path)).contains("MoonButtonSize::Custom {"), - "{path} must use the density tier instead of a custom button size" - ); - } -} - -/// Ordinary controls must not pin themselves to the Standard tier. -/// -/// Breakage: restoring `MoonSize::Sm` keeps a 24px/14px control beside Compact's 20px/12px -/// controls, producing the same visibly uneven toolbar and header rows. -#[test] -fn ordinary_controls_do_not_pin_the_standard_tier() { - for path in [ - "controls/metric.rs", - "controls/toolbar.rs", - "chrome/terminal_chrome.rs", - ] { - assert!( - !code_only(&read_src(path)).contains("MoonSize::Sm"), - "{path} must follow the density tier instead of pinning MoonSize::Sm" - ); - } -} - -/// Toolbar strip text must carry the tier's rendered metrics rather than a font-channel label. -/// -/// Breakage: removing `rendered_metrics(design::tier_text_metrics(` makes Size and Sell captions -/// use a legacy size, so their row no longer aligns with the preset controls at Compact. -#[test] -fn toolbar_strip_text_uses_rendered_tier_metrics() { - let toolbar = read_src("controls/toolbar.rs"); - let strip_text = code_only(braced_body(&toolbar, "fn strip_text(")); - assert!( - strip_text.contains(".rendered_metrics(design::tier_text_metrics("), - "controls/toolbar.rs:strip_text must use rendered tier metrics" - ); -} - -/// Catches `design.rs:toolbar_height` reverting to `fit_h_value`, which leaves Compact chrome at -/// 32px instead of 28px while Standard and Large deceptively remain unchanged. -#[test] -fn toolbar_adapter_uses_the_compact_tier_band() { - let design = read_src("design.rs"); - let toolbar = code_only(braced_body(&design, "pub fn toolbar_height(")); - assert!( - toolbar.contains("t.fit_band(t.tier_band_base(TOOLBAR_H, |m| m.height), 13.0)"), - "toolbar_height must use the compact-aware tier band rather than the legacy fit_h_value" - ); -} - -/// Catches retaining an explicit Orders row-height pin, which makes Compact Orders rows 25px -/// instead of the 21px density default while Standard and Large mask the regression. -#[test] -fn orders_table_uses_the_density_aware_row_default() { - let orders = code_only(&read_src("panels/orders/table.rs")); - assert!( - !orders.contains(".row_height(design::TABLE_ROW_H)"), - "Orders must omit the legacy row-height pin so MoonDataTable resolves the active tier" - ); -} diff --git a/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs b/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs index 28aa57d6c..6d1d528fe 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs @@ -114,7 +114,7 @@ fn pinned_scope_host_keeps_its_tooltip() { /// either, since neither can be checked against the private `MoonButtonMetrics` directly. /// /// Mutation: re-fork them — give `glyph_btn_w` its own literal instead of delegating, or have -/// `action_control_h_value` stop reading `button_tier(cx).control_metrics().height`. Consequence: +/// `action_control_h_value` stop reading `CONTROL_TIER.control_metrics().height`. Consequence: /// the pinned chip (sized off `glyph_btn_w`) stops matching the density-tier controls standing /// beside it in the same row — a drift neither function's own body would ever reveal by itself. #[test] @@ -123,9 +123,9 @@ fn action_control_h_value_agrees_with_glyph_btn_w() { let action = braced_body(&design, "fn action_control_h_value("); let glyph = braced_body(&design, "fn glyph_btn_w("); assert!( - action.contains("button_tier(cx).control_metrics().height") + action.contains("CONTROL_TIER.control_metrics().height") && glyph.contains("action_control_h_value(cx)"), - "glyph_btn_w must delegate to action_control_h_value, which must read the density \ + "glyph_btn_w must delegate to action_control_h_value, which must read the control \ tier's own control_metrics — anything else re-forks the two Action metrics this goal \ exists to unify" ); diff --git a/crates/moon-ui-gpui/tests/theme_contract/fonts.rs b/crates/moon-ui-gpui/tests/theme_contract/fonts.rs index a6f8bc087..41812eb95 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/fonts.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/fonts.rs @@ -349,7 +349,7 @@ fn width_measurements_agree_with_their_rendered_family_and_cache_key() { let signature = code_only(braced_body(&wrap_fit, "pub(crate) fn signature(")); assert!( signature.contains("design::text_metrics_key_zoomed(") - && signature.contains("design::button_tier(cx).control_metrics().font_size,") + && signature.contains("design::CONTROL_TIER.control_metrics().font_size,") && signature.contains("400.0,") && signature.contains("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 57d4fc6d3..399bd6dbe 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/main.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/main.rs @@ -18,7 +18,6 @@ mod analytics; mod chart; mod core_pick; mod core_status; -mod density; mod detects; mod dock_chrome; mod fonts; diff --git a/crates/moon-ui-gpui/tests/theme_contract/shell.rs b/crates/moon-ui-gpui/tests/theme_contract/shell.rs index 6c33089f4..e7834c3c8 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/shell.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/shell.rs @@ -1253,7 +1253,7 @@ fn toolbar_launcher_labels_are_measured_and_all_or_none() { let button = fn_body(&text, "fn open_window_button("); assert!(measure.contains("design::ui_text_width_zoomed(")); - assert!(measure.contains("design::tier_font_size(cx)")); + assert!(measure.contains("design::BODY_TEXT")); assert!(measure.contains("TOOLBAR_LAUNCHER_TEXT_WEIGHT")); assert!( measure.contains("false,"), diff --git a/crates/moon-ui-gpui/tests/theme_contract/strategies.rs b/crates/moon-ui-gpui/tests/theme_contract/strategies.rs index b6d0a22e6..14ba807d8 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/strategies.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/strategies.rs @@ -900,18 +900,16 @@ fn strategies_settings_own_restore_persistence_and_reveal_visibility() { let measured_width = code_only(braced_body(&settings, "fn settings_content_width(")); let popup_measurement = code_only(braced_body(&settings, "fn popup_text_width(")); assert!(popup_measurement.contains("design::ui_text_width(")); - assert!(measured_width.contains("checkbox_metrics(cx).mark")); - assert!(measured_width.contains("checkbox_metrics(cx).gap")); + assert!(measured_width.contains("CHECKBOX_METRICS.mark")); + assert!(measured_width.contains("CHECKBOX_METRICS.gap")); assert!(measured_width.contains("strat.settings.text_step")); - assert!(measured_width.contains(".tier()")); - assert!(measured_width.contains(".nearest(&[MoonSize::Xs, MoonSize::Sm, MoonSize::Md])")); - assert!(measured_width.contains("tier.control_metrics().height")); - // The reserve is the tier's two square buttons plus the ratio-derived value cell plus the - // density checkbox gap, all under one `tokens.ui(..)`; pinned piecewise so rustfmt's line + assert!(measured_width.contains("design::CONTROL_TIER.control_metrics().height")); + // The reserve is the control tier's two square buttons plus the ratio-derived value cell plus + // the checkbox gap, all under one `tokens.ui(..)`; pinned piecewise so rustfmt's line // breaking cannot red this contract. - assert!(measured_width.contains("tokens.ui(height * 2.0")); + assert!(measured_width.contains(".ui(height * 2.0")); assert!(measured_width.contains("+ (height * ratio).round()")); - assert!(measured_width.contains("+ crate::panels::common::checkbox_metrics(cx).gap)")); + assert!(measured_width.contains("+ crate::panels::common::CHECKBOX_METRICS.gap)")); assert!(measured_width.contains("popup_group_inset_px(cx)")); assert!(!settings.contains("const CONTENT_WIDTH")); for tuner_symbol in [ diff --git a/crates/moon-ui-gpui/tests/theme_contract/theme_selector.rs b/crates/moon-ui-gpui/tests/theme_contract/theme_selector.rs index 30206672c..3eadd9200 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/theme_selector.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/theme_selector.rs @@ -16,13 +16,13 @@ fn graphite_startup_mapping_installs_the_graphite_dark_theme() { } /// Catches `startup/unlock.rs:install_login_theme` returning to a separate two-arm mapping. -/// Login and the running application must share mode, density and zoom presentation mapping. +/// Login and the running application must share the mode and zoom presentation mapping. #[test] fn login_theme_reuses_the_application_theme_mapping_for_all_three_modes() { let unlock = code_only(&read_src("startup/unlock.rs")); let install = braced_body(&unlock, "fn install_login_theme(cx: &mut App)"); assert!(install.contains("moon_theme_config_for_presentation(")); - for preference in ["prefs.ui_theme_mode", "prefs.ui_density", "prefs.ui_scale"] { + for preference in ["prefs.ui_theme_mode", "prefs.ui_scale"] { assert!(install.contains(preference)); } let startup = code_only(&read_src("startup.rs")); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9c4e8b144..8fbec6f6f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -858,6 +858,29 @@ FireTest не читает исходники и не проверяет арх native input, counters/CPU/GPU/RAM. Статические запреты вида "не рендерить меню как child панели" живут в `tests/theme_contract/`, а не внутри runtime-сценария. +### Масштаб интерфейса + +Один пользовательский масштаб (`ui_scale`, Settings → General → «Масштаб интерфейса», 50–200 %) +устанавливается как content zoom окна: `startup::moon_theme_config_for_presentation` кладёт его в +`MoonScale::zoom`, а `MoonRoot` каждого окна применяет его через `Window::set_content_zoom`. Zoom +входит в `scale_factor` окна, поэтому масштабируется каждый пиксель — текст, геометрия, картинки, +hit-area, сохранённые размеры доков — без участия компонентов. Токены MoonUI при этом остаются на +масштабе дизайна: `scale.ui == scale.font == 1.0`, `tier = Sm`, `font_delta = 3`; +`design::ui_px` — identity-адаптер над токенами, а `design::CONTROL_TIER` / `BODY_TEXT` / +`INPUT_SIZE` — единая система размеров, от которой считаются контролы, текст и chrome-полосы. +Настройки плотности (Compact / Standard / Large) больше нет: ступенчатая смена tier'ов подменяла +дизайн другим, а не масштабировала его, и не сохраняла пропорции. + +График в zoom не участвует: `chartdx` берёт размер render target из полного `scale_factor` +(слот действительно занимает `bounds × factor` device-пикселей), а свои размеры — толщину линий, +свечи, оси, подписи — из `scale_factor / content_zoom`, то есть из платформенного DPI. Текстовый +слой графика живёт в его собственных логических пикселях и переходит в content-пиксели GPUI +только в `chartdx::text::content_px` / `chart_metrics`; оверлеи над геометрией графика +(`panels/chart/arb_open.rs`, `market_actions.rs`) делят на zoom в одном месте — +`chart_origin_logical` / `MoonTheme::content_zoom`. Экранные координаты (`window.bounds()`, +размещение окон, FireTest-probe) остаются платформенными; `windowing::responsive_width` читает +`viewport_size()`, то есть content-пиксели. + ## Окна Терминал использует собственную шапку и borderless/CSD поведение. Проверять отдельно: diff --git a/docs/FIRETEST.md b/docs/FIRETEST.md index 9a6358fc4..32bb71819 100644 --- a/docs/FIRETEST.md +++ b/docs/FIRETEST.md @@ -42,6 +42,9 @@ $env:MOON_FIRETEST_MARKET = "ACEUSDT" - `MOON_FIXTURE_DIR` — где искать наборы, если не рядом с exe и не в корне репозитория. - `MOON_FIXTURE_FIGURES=0` — не класть фигуры на стенд. Это переключатель для A/B-замера: прогон с фигурами и без них отличается только им, поэтому разница в счётчиках — цена самих фигур. +- `MOON_FIXTURE_LAYOUT` / `MOON_FIXTURE_SETTINGS` — файлы `layout.toml` / `settings.toml`, которые + копируются в стенд до чтения конфигурации: так прогон открывается с нужной раскладкой или, + например, при `ui_scale = 1.5`, чтобы проверить ввод и оверлеи под масштабом интерфейса. Подробности устройства и перегенерации набора — во внутренней документации. diff --git a/locales/interface.yml b/locales/interface.yml index dcf35fe65..742ca0c92 100644 --- a/locales/interface.yml +++ b/locales/interface.yml @@ -38,30 +38,14 @@ iface.sec_font: ru: "График: шрифт подписей" en: "Chart: label font" es: "Gráfico: fuente de etiquetas" -iface.density: - ru: "Плотность" - en: "Density" - es: "Densidad" -iface.density_compact: - ru: "Компакт" - en: "Compact" - es: "Compacta" -iface.density_standard: - ru: "Стандарт" - en: "Standard" - es: "Estándar" -iface.density_large: - ru: "Крупно" - en: "Large" - es: "Grande" -iface.density_hint: - ru: "Размер элементов и текста интерфейса. Меняется сразу." - en: "Interface control and text size. Applies instantly." - es: "Tamaño de controles y texto de la interfaz. Se aplica al instante." iface.ui_zoom: ru: "Масштаб интерфейса" en: "UI zoom" es: "Zoom de la interfaz" +iface.ui_zoom_hint: + ru: "Масштабирует весь интерфейс пропорционально; график остаётся в своём масштабе. Применяется, когда ползунок отпущен." + en: "Scales the whole interface proportionally; the chart keeps its own scale. Applies when the slider is released." + es: "Escala toda la interfaz proporcionalmente; el gráfico conserva su propia escala. Se aplica al soltar el control." iface.label_font_delta: ru: "Коррекция размера подписей линий и курсора (px)" en: "Order-line & cursor label size adjustment (px)" From acc9695e27749a7be078f38af01c7f59a62bf18d Mon Sep 17 00:00:00 2001 From: ThisMad Date: Sat, 19 Sep 2026 19:05:27 +0200 Subject: [PATCH 2/3] fix(chart): keep the pointer and overlays on the window's factor under zoom Two clean-context reviews of the scale change. The chart-input half had the engine's factor (device pixels per chart-design pixel, UI zoom excluded) where the window's (device per content pixel) belongs, so at any zoom but 100 % the crosshair, drags and presses landed at `1 / zoom` of the pointer and the corner overlays and hover cards sat `zoom` times too far from the pane: - `chart_local_from_window_pos` crosses with `slot_scale_factor()`, the window's factor as the last frame saw it; - the six input handlers hand the container `last_ppp`, the chart-design factor its pane layout and thresholds are drawn with; - the pane close, pin, lock and broom buttons, the zone markers, the logo and the news, warning and trade hover cards divide device pixels by the window's factor to land in content space. Also from the reviews: first-open window sizes and minimums (login, detached and chart windows, expert settings, report) are design pixels and now multiply the content zoom in; four contracts that still guard the size system return as `theme_contract/size_system.rs`; stale "density" wording in comments reads "control tier"; a tautological assertion is dropped. An engine test pins the two factors and the pointer crossing. --- crates/moon-ui-gpui/src/analytics/toolbar.rs | 4 +- .../src/analytics/tuner/time/sliders.rs | 2 +- .../chart_tabs/main_stack/empty/arrange.rs | 2 +- .../src/chart_tabs/main_stack/empty/detect.rs | 2 +- crates/moon-ui-gpui/src/chart_tabs/windows.rs | 6 +- crates/moon-ui-gpui/src/chartdx/engine.rs | 18 +++- .../moon-ui-gpui/src/chartdx/engine/tests.rs | 48 +++++++++++ crates/moon-ui-gpui/src/chrome/quiet.rs | 6 +- .../src/chrome/terminal_chrome.rs | 2 +- .../moon-ui-gpui/src/controls/manual_strat.rs | 2 +- crates/moon-ui-gpui/src/controls/toolbar.rs | 14 ++-- crates/moon-ui-gpui/src/core_expert.rs | 6 +- crates/moon-ui-gpui/src/design.rs | 21 +++-- crates/moon-ui-gpui/src/design/tests.rs | 6 +- crates/moon-ui-gpui/src/panels/chart/news.rs | 15 ++-- .../moon-ui-gpui/src/panels/chart/render.rs | 30 ++++--- .../src/panels/chart/render_input.rs | 32 ++++--- .../src/panels/chart/trade_history_hover.rs | 14 ++-- crates/moon-ui-gpui/src/panels/chart/warn.rs | 10 +-- crates/moon-ui-gpui/src/panels/common.rs | 4 +- .../moon-ui-gpui/src/panels/report/window.rs | 6 +- .../src/settings/connections/columns.rs | 4 +- crates/moon-ui-gpui/src/settings/general.rs | 2 +- crates/moon-ui-gpui/src/shell/ticker.rs | 4 +- .../moon-ui-gpui/src/strategies/settings.rs | 4 +- .../src/strategies/tree/checks.rs | 2 +- .../moon-ui-gpui/src/strategies/tree/moon.rs | 2 +- .../src/strategies/tree/pane_cache.rs | 6 +- crates/moon-ui-gpui/src/strategies/tree/ui.rs | 2 +- crates/moon-ui-gpui/src/window/detached.rs | 4 +- crates/moon-ui-gpui/src/window/login.rs | 8 +- .../tests/theme_contract/chart.rs | 2 +- .../tests/theme_contract/dock_chrome.rs | 4 +- .../moon-ui-gpui/tests/theme_contract/main.rs | 1 + .../tests/theme_contract/shell.rs | 2 +- .../tests/theme_contract/size_system.rs | 83 +++++++++++++++++++ .../tests/theme_contract/strategies.rs | 2 +- .../tests/theme_contract/theme.rs | 5 +- 38 files changed, 279 insertions(+), 108 deletions(-) create mode 100644 crates/moon-ui-gpui/tests/theme_contract/size_system.rs diff --git a/crates/moon-ui-gpui/src/analytics/toolbar.rs b/crates/moon-ui-gpui/src/analytics/toolbar.rs index 7fe70978f..f9d658d30 100644 --- a/crates/moon-ui-gpui/src/analytics/toolbar.rs +++ b/crates/moon-ui-gpui/src/analytics/toolbar.rs @@ -450,7 +450,7 @@ impl AnalyticsView { if t == Tab::Calendar { row = row.child(design::chrome_divider(cx, p)); } - // Title width plus the density-tier button's own pad_x on both sides, with a floor so + // Title width plus the control-tier button's own pad_x on both sides, with a floor so // a short label still has a useful click target. let tab_width = (design::ui_text_width_zoomed(cx, &title, design::BODY_TEXT, 400.0, false) @@ -524,7 +524,7 @@ impl AnalyticsView { } // Keep the selector widths and their internal gaps together. One additional gap belongs to // the caption, so the whole semantic group moves to the next line before any control is - // clipped. Density-tier triggers scale those design-reference widths through `tokens.ui` + // clipped. Control-tier triggers scale those design-reference widths through `tokens.ui` // at the button tier's control font, not the legacy 10.5px font channel. let action_trigger_scale = { let font_size = design::CONTROL_TIER.control_metrics().font_size.max(1.0); diff --git a/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs b/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs index 4e6d69ead..d8836897c 100644 --- a/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs +++ b/crates/moon-ui-gpui/src/analytics/tuner/time/sliders.rs @@ -440,7 +440,7 @@ impl AnalyticsView { // Gap 4 (not 6) plus a checkbox-wide lead-in: together they reproduce the grid // row's checkbox column, so the slider labels stay under the field names. The // lead-in is an invisible, tier-following checkbox rather than a stated width, so it - // keeps MoonUI's own box size at any density and UI zoom; hidden, it takes layout but + // keeps MoonUI's own box size at the control tier and any UI zoom; hidden, it takes layout but // paints nothing. .gap(design::ui_px(cx, 4.0)) .opacity(if dim { 0.5 } else { 1.0 }) diff --git a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs index 7bdf546b6..4b6782263 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/arrange.rs @@ -262,7 +262,7 @@ fn row( .items_center() .gap(design::ui_px(cx, ROW_GAP)) .child( - // The face a density-selected checkbox gives its label — its size, soft text — so this row and + // The face a control-tier checkbox gives its label — its size, soft text — so this row and // the rule's switch under it read as one list rather than as a heading over a note. div() .flex_1() diff --git a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs index 1649d0d80..56bc669bd 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/main_stack/empty/detect.rs @@ -499,7 +499,7 @@ fn field_row( .items_center() .gap(design::ui_px(cx, FIELD_GAP)) .child( - // The face the density-selected checkbox beside these gives its label, on and off: its size, + // The face the control-tier checkbox beside these gives its label, on and off: its size, // soft text, and the same fade when dead. div() .flex_1() diff --git a/crates/moon-ui-gpui/src/chart_tabs/windows.rs b/crates/moon-ui-gpui/src/chart_tabs/windows.rs index c93f2dc80..852136e18 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/windows.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/windows.rs @@ -227,12 +227,14 @@ impl ChartTabs { } else { crate::window::windowing::cascade_origin_on(origin, display_id, cx) }; + // A first-open size is design pixels; the window API takes the platform's. + let zoom = moon_ui::MoonTheme::content_zoom(cx); let fallback = crate::window::windowing::detached_fallback_bounds( owner, display_id, size( - px(FIRST_DETACH_GEOM.w as f32), - px(FIRST_DETACH_GEOM.h as f32), + px(FIRST_DETACH_GEOM.w as f32 * zoom), + px(FIRST_DETACH_GEOM.h as f32 * zoom), ), cx, ); diff --git a/crates/moon-ui-gpui/src/chartdx/engine.rs b/crates/moon-ui-gpui/src/chartdx/engine.rs index e3ab06864..dfd7c56a6 100644 --- a/crates/moon-ui-gpui/src/chartdx/engine.rs +++ b/crates/moon-ui-gpui/src/chartdx/engine.rs @@ -218,6 +218,15 @@ impl ChartEngine { self.data.borrow().content_zoom } + /// Device pixels per content pixel at the last frame: the window's effective factor, which is + /// what a content-space position (a pointer, a GPUI element) crosses by to reach the chart's + /// device pixels. Distinct from [`Self::slot_geometry`]'s factor, which is device pixels per + /// chart-design pixel and excludes the UI zoom. + pub fn slot_scale_factor(&self) -> f32 { + let data = self.data.borrow(); + data.last_ppp * data.content_zoom + } + pub fn slot_dev_size(&self) -> (u32, u32) { let data = self.data.borrow(); (data.w.max(1), data.h.max(1)) @@ -227,11 +236,18 @@ impl ChartEngine { self.data.borrow().w.max(1) as f32 } + /// Map a content-space window position onto the chart's device pixels, and say whether it + /// is inside the slot. + /// + /// Both the position and the slot bounds are content pixels, so the crossing uses the + /// window's effective factor ([`Self::slot_scale_factor`]); the chart-design factor would land + /// the pointer at `1 / zoom` of where it is under UI zoom. pub fn chart_local_from_window_pos( &self, pos: gpui::Point, ) -> Option<((f32, f32), bool)> { - let (bounds, sf, _) = self.slot_geometry()?; + let (bounds, _, _) = self.slot_geometry()?; + let sf = self.slot_scale_factor(); let lx = f32::from(pos.x) - f32::from(bounds.origin.x); let ly = f32::from(pos.y) - f32::from(bounds.origin.y); let w = f32::from(bounds.size.width); diff --git a/crates/moon-ui-gpui/src/chartdx/engine/tests.rs b/crates/moon-ui-gpui/src/chartdx/engine/tests.rs index 0cff44098..bc3d37ea2 100644 --- a/crates/moon-ui-gpui/src/chartdx/engine/tests.rs +++ b/crates/moon-ui-gpui/src/chartdx/engine/tests.rs @@ -28,3 +28,51 @@ fn pause_and_live_preserve_time_zoom_through_the_next_prepare() { } } } + +/// Catches `data_state/state.rs:apply_slot_geometry` collapsing the frame's two factors back into +/// one (the chart would zoom with the interface, or draw into a wrong-sized target), and +/// `engine.rs:chart_local_from_window_pos` crossing with the chart-design factor instead of the +/// window's (under UI zoom the pointer would land at `1 / zoom` of where it is). +#[test] +fn slot_geometry_keeps_device_density_while_the_pointer_crosses_with_the_windows_factor() { + use gpui::{Bounds, GpuFrameInfo, point, px, size}; + use std::time::Instant; + + let now = 1_700_000_000_000.0; + let mut engine = ChartEngine::new(now, ChartTheme::default()); + engine.open(42, "TESTUSDT"); + engine.data.borrow_mut().frame(GpuFrameInfo { + now: Instant::now(), + bounds: Bounds { + origin: point(px(10.0), px(20.0)), + size: size(px(200.0), px(100.0)), + }, + scale_factor: 3.0, + content_zoom: 1.5, + presentable: true, + }); + + let (bounds, ppp, (w, h)) = engine.slot_geometry().expect("the frame installed a slot"); + assert_eq!( + f32::from(bounds.origin.x), + 10.0, + "slot bounds stay content space" + ); + assert_eq!( + (w, h), + (600, 300), + "the target is the slot in device pixels: bounds × 3.0" + ); + assert_eq!( + ppp, 2.0, + "the chart's own factor excludes the zoom: 3.0 / 1.5" + ); + assert_eq!(engine.slot_content_zoom(), 1.5); + assert_eq!(engine.slot_scale_factor(), 3.0); + + let ((x, y), within) = engine + .chart_local_from_window_pos(point(px(60.0), px(70.0))) + .expect("the frame installed a slot"); + assert_eq!((x, y), (150.0, 150.0), "(60 − 10) × 3 and (70 − 20) × 3"); + assert!(within); +} diff --git a/crates/moon-ui-gpui/src/chrome/quiet.rs b/crates/moon-ui-gpui/src/chrome/quiet.rs index a57ab9da8..7aabe0d25 100644 --- a/crates/moon-ui-gpui/src/chrome/quiet.rs +++ b/crates/moon-ui-gpui/src/chrome/quiet.rs @@ -14,7 +14,7 @@ //! and that should read as a caution the moment the header is glanced at. //! //! The label is the component's own, coloured through `MoonToggle::label_color`. Its size is the -//! density tier's and therefore follows UI zoom only, so it is measured with +//! control tier's and therefore follows UI zoom only, so it is measured with //! [`design::ui_text_width_zoomed`], not the font-scaled caption steps. use gpui::prelude::FluentBuilder; @@ -49,7 +49,7 @@ pub(crate) fn header_quiet_width(cx: &App) -> f32 { + design::ui_value(cx, m.gap) + design::ui_value(cx, m.track_width) + design::ui_value(cx, design::CHROME_GAP) - // The gear is `popup_gear_trigger`, icon-only at the density tier, so MoonUI draws it + // The gear is `popup_gear_trigger`, icon-only at the control tier, so MoonUI draws it // square and its width IS its height. + design::action_control_h_value(cx) } @@ -127,7 +127,7 @@ pub(crate) fn header_quiet_cluster( crate::shell::quiet_popup::CONTENT_W, open, content, - // Density-tier icon-only gear: this cluster sits in the header row, not a dense strip. + // Control-tier icon-only gear: this cluster sits in the header row, not a dense strip. popup_gear_trigger("header-quiet-gear", t!("quiet.gear_tip").to_string(), open), move |open, window, cx| { shell.update(cx, |s, cx| s.set_quiet_settings_open(open, window, cx)); diff --git a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs index 2927c7662..d715b69c0 100644 --- a/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs +++ b/crates/moon-ui-gpui/src/chrome/terminal_chrome.rs @@ -738,7 +738,7 @@ fn core_selector( p: MoonPalette, cx: &App, ) -> AnyElement { - // The pill follows the density-tier ordinary-control height so it shares one top/bottom edge + // The pill follows the control-tier ordinary-control height so it shares one top/bottom edge // with the neighbouring buttons; its content width is still capped below so a long // user-defined name cannot displace the header's right-hand readouts. let b = backend.read(cx); diff --git a/crates/moon-ui-gpui/src/controls/manual_strat.rs b/crates/moon-ui-gpui/src/controls/manual_strat.rs index b2e44528b..3fff5ba9e 100644 --- a/crates/moon-ui-gpui/src/controls/manual_strat.rs +++ b/crates/moon-ui-gpui/src/controls/manual_strat.rs @@ -110,7 +110,7 @@ const REDUCED_PILL_MAX_W: f32 = 120.0; /// `chrome::quiet::header_quiet_width`, which reads the new metrics on its own. const HEADER_OTHER_SECTIONS_W: f32 = 806.0; -/// Width of the "MS" toggle (track, gap and label) at the density-default tier. +/// Width of the "MS" toggle (track, gap and label) at the control tier. /// /// The label is measured in the mono family because the MS toggle sets no `.mono()` and /// `MoonToggle` defaults `mono: true`. diff --git a/crates/moon-ui-gpui/src/controls/toolbar.rs b/crates/moon-ui-gpui/src/controls/toolbar.rs index c1a3fcdd3..9cd8867eb 100644 --- a/crates/moon-ui-gpui/src/controls/toolbar.rs +++ b/crates/moon-ui-gpui/src/controls/toolbar.rs @@ -28,11 +28,11 @@ mod tests; /// Base gap between the Sell caption and its first percentage cell. const SELL_CAPTION_GAP: f32 = 8.0; -/// Caption for a preset group, muted, sharing the density-tier body size with the chips beside it. +/// Caption for a preset group, muted, sharing the control-tier body size with the chips beside it. /// /// `MoonText` with [`design::text_metrics`] rather than a hand-rolled text div: it applies /// the theme's mono family and already-scaled metrics, so the caption lands on the same size and -/// baseline as the neighbouring density-tier controls. Colour is the remaining caption cue. +/// baseline as the neighbouring control-tier controls. Colour is the remaining caption cue. /// /// The text is a literal, not `t!`: `Size`/`Sell` are on the deliberately-untranslated list /// (`locales/README.md`), as are the neighbouring `Lev`/`SL`/`TP`. The tooltip is translated, the @@ -50,7 +50,7 @@ fn strip_caption(text: impl Into, p: MoonPalette, cx: &App) -> imp /// Args: /// text: Caption or readout text rendered at the toolbar's shared text scale. /// color: Active palette color for the text's semantic role. -/// cx: Application context used to resolve density-tier text metrics. +/// cx: Application context used to resolve control-tier text metrics. /// /// Returns: /// A non-shrinking monospaced toolbar text element. @@ -100,7 +100,7 @@ fn captioned_strip( /// (`MoonButton::width`) puts the value into `px(..)` verbatim, so a raw width would squeeze a /// label that grows with the legacy font-delta channel — the same ailment the preset cells had. /// -/// Icon-only launchers use [`design::glyph_btn_w`], which follows the density tier. The +/// Icon-only launchers use [`design::glyph_btn_w`], which follows the control tier. The /// [`ICON_BTN_W`] constant is retained only because a contract test pins its name. const LEV_W: f32 = 61.6; /// Base width of the stop-loss metric button. @@ -126,7 +126,7 @@ const SELL_CAPTION: &str = "Sell"; /// Stable unit for group-local manual order-size equivalents. const SIZE_UNIT: &str = "USDT eq."; -/// Measure one complete localized launcher button at density-tier geometry. +/// Measure one complete localized launcher button at control-tier geometry. /// /// 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 @@ -134,7 +134,7 @@ const SIZE_UNIT: &str = "USDT eq."; /// 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 plus its density-tier gap ([`design::action_icon_reservation`]), both insets, and +/// leading icon plus its control-tier gap ([`design::action_icon_reservation`]), both insets, and /// the two border pixels. The button is never allowed to become narrower than /// [`design::glyph_btn_w`]. /// @@ -1254,7 +1254,7 @@ pub fn toolbar( /// backend: Shared terminal state passed to the destination. /// open: Singleton-window entry point invoked by the click. /// p: Active palette used for icon and text colors. -/// cx: Application context used to resolve the density-tier icon-only width. +/// cx: Application context used to resolve the control-tier icon-only width. /// /// Returns: /// One rendered compact launcher button. diff --git a/crates/moon-ui-gpui/src/core_expert.rs b/crates/moon-ui-gpui/src/core_expert.rs index f64d4c44d..20ad1da28 100644 --- a/crates/moon-ui-gpui/src/core_expert.rs +++ b/crates/moon-ui-gpui/src/core_expert.rs @@ -414,10 +414,12 @@ pub(crate) fn open( return; } let saved = backend.read(cx).layout.core_expert_window; + // The default and minimum sizes are design pixels; the window API takes the platform's. + let zoom = moon_ui::MoonTheme::content_zoom(cx); let bounds = saved.map_or( Bounds { origin: point(px(140.0), px(100.0)), - size: size(px(DEFAULT_SIZE.0), px(DEFAULT_SIZE.1)), + size: size(px(DEFAULT_SIZE.0 * zoom), px(DEFAULT_SIZE.1 * zoom)), }, |g| Bounds { origin: point(px(g.x as f32), px(g.y as f32)), @@ -439,7 +441,7 @@ pub(crate) fn open( saved, crate::window::windowing::reachable_window_bounds(bounds, display_id, None, cx), ), - Some(size(px(MIN_SIZE.0), px(MIN_SIZE.1))), + Some(size(px(MIN_SIZE.0 * zoom), px(MIN_SIZE.1 * zoom))), owner, ); opts.display_id = display_id; diff --git a/crates/moon-ui-gpui/src/design.rs b/crates/moon-ui-gpui/src/design.rs index 97de8df8a..743e2ff48 100644 --- a/crates/moon-ui-gpui/src/design.rs +++ b/crates/moon-ui-gpui/src/design.rs @@ -75,7 +75,7 @@ pub const MOON_SCROLLBAR_OVERLAY_W: f32 = 8.0; /// /// Pass this value directly to `MoonDisclosure`: its `caret_box` applies `tokens.ui(...)`, so /// passing [`ui_px`] would apply the UI scale twice. The caret therefore follows the UI slider, -/// matching raw text sized through [`t_body`] (the density-tier channel) and chrome such as +/// matching raw text sized through [`t_body`] (the body-text channel) and chrome such as /// [`vline`]. pub const DISCLOSURE_GLYPH: f32 = 11.0; @@ -90,7 +90,7 @@ pub const GLYPH_CLOSE: &str = "✕"; /// Base (unscaled) glyph edge for a passive disclosure caret whose enclosing row owns the click. /// /// Pass this value directly to `MoonDisclosure`; its `caret_box` applies the UI scale. This keeps -/// the marker on the UI slider, matching [`t_body`]'s density-tier channel. +/// the marker on the UI slider, matching [`t_body`]'s body-text channel. pub const DISCLOSURE_GLYPH_MARKER: f32 = 9.0; /// Base (unscaled) square box around either disclosure caret. @@ -982,7 +982,7 @@ pub fn footer_tone_color(p: MoonPalette, tone: MoonTone) -> u32 { /// /// Exists so a caller measuring text it drew with `t_body()` + [`mono`] cannot reach for /// `t_body(cx)` as the size argument: that value is ALREADY scaled, and passing it in would scale -/// twice. These five helpers measure TERMINAL text, which now follows the density tier; plain +/// twice. These five helpers measure TERMINAL text, which follows the control tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { ui_text_width_zoomed(cx, text, BODY_TEXT, weight, true) @@ -990,7 +990,7 @@ pub fn mono_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// Width of mono text drawn at the terminal's caption size — the [`t_caption`] partner of /// [`mono_body_text_width`], and it exists for the same reason: `t_caption(cx)` is already scaled, -/// so passing it in would scale twice. Measures TERMINAL text on the density tier; plain +/// so passing it in would scale twice. Measures TERMINAL text on the control tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { ui_text_width_zoomed(cx, text, BODY_TEXT - 2.0, weight, true) @@ -998,7 +998,7 @@ pub fn mono_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// Width of mono text drawn at the terminal's title size — the [`t_title`] partner of /// [`mono_body_text_width`], and it exists for the same reason: `t_title(cx)` is already scaled, -/// so passing it in would scale twice. Measures TERMINAL text on the density tier; plain +/// so passing it in would scale twice. Measures TERMINAL text on the control tier; plain /// [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal text. pub fn mono_title_text_width(cx: &App, text: &str, weight: f32) -> f32 { ui_text_width_zoomed(cx, text, BODY_TEXT + 3.0, weight, true) @@ -1009,7 +1009,7 @@ pub fn mono_title_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// /// 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. Measures TERMINAL text on the density +/// every proportional string and drifts the column it sizes. Measures TERMINAL text on the control /// tier; plain [`ui_text_width`] stays on the legacy font channel for mirroring MoonUI-internal /// text. pub fn ui_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { @@ -1018,7 +1018,7 @@ pub fn ui_body_text_width(cx: &App, text: &str, weight: f32) -> f32 { /// 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`]. Measures -/// TERMINAL text on the density tier; plain [`ui_text_width`] stays on the legacy font channel for +/// TERMINAL text on the control tier; plain [`ui_text_width`] stays on the legacy font channel for /// mirroring MoonUI-internal text. pub fn ui_caption_text_width(cx: &App, text: &str, weight: f32) -> f32 { ui_text_width_zoomed(cx, text, BODY_TEXT - 2.0, weight, false) @@ -1210,7 +1210,7 @@ pub fn text_metrics_key(cx: &App, base_font_size: f32, weight: f32, mono: bool) /// Identity of the typography a `tokens.ui`-channel text measurement was taken under. /// /// The [`ui_text_width_zoomed`] partner of [`text_metrics_key`]: for text that follows only the UI -/// zoom, not the legacy font-delta channel — density-tier control labels, and the terminal's own +/// zoom, not the legacy font-delta channel — control-tier labels, and the terminal's own /// `t_*` text after it moved onto the tier. Resolves through [`measure_font_at`] at /// [`ui_px`]`(size)` instead of [`measure_font`]. /// @@ -1509,8 +1509,7 @@ pub fn table_head_base_px(cx: &App) -> Pixels { /// Return the current text-container width scale relative to the theme base. /// /// [`ui_value`] of [`BODY_TEXT`], divided by [`base_text`]: 14 / 11 at the default typography. -/// Identical to MoonUI's `font_width_scale` at -/// Standard; Compact and Large follow the density tier, not `font_delta`. +/// Identical to MoonUI's `font_width_scale` at the design's font delta. /// /// Args: /// cx: Application context used to read active theme tokens. @@ -1727,7 +1726,7 @@ pub fn logo_glow_frame_w(_cx: &App, width: f32) -> Pixels { /// Vertical 1px group separator. /// /// The height goes through `ui()`, matching MoonUI, which draws its own separators the same way -/// (the brand cluster in `MoonWindowFrame` is one). Density-tier terminal text also tracks `ui()` +/// (the brand cluster in `MoonWindowFrame` is one). Control-tier terminal text also tracks `ui()` /// via [`t_body`]; MoonUI-internal font-channel text still tracks `font_delta`, so the /// rule can sit beside a MoonUI row that grew on that channel. The 1px width stays raw, also /// matching MoonUI, since a hairline must not thicken with the font. diff --git a/crates/moon-ui-gpui/src/design/tests.rs b/crates/moon-ui-gpui/src/design/tests.rs index 35ec23c89..926d64d48 100644 --- a/crates/moon-ui-gpui/src/design/tests.rs +++ b/crates/moon-ui-gpui/src/design/tests.rs @@ -351,10 +351,7 @@ fn micro_control_h_value_stays_below_the_control_row(cx: &mut gpui::TestAppConte // --- One size system --------------------------------------------------------------------------- -use super::{ - BODY_TEXT, CONTROL_TIER, body_font_base, font_w, line_px, t_body, t_body_lg, t_caption, - t_title, ui_px, -}; +use super::{body_font_base, font_w, line_px, t_body, t_body_lg, t_caption, t_title, ui_px}; use gpui::px; /// `design.rs:t_body` and its siblings must render what MoonUI's font channel rendered for the @@ -397,7 +394,6 @@ fn body_text_matches_the_legacy_font_channel_at_the_design(cx: &mut gpui::TestAp /// makes text disagree with the controls it sits beside. #[gpui::test] fn t_body_is_the_control_tiers_font(cx: &mut gpui::TestAppContext) { - assert_eq!(BODY_TEXT, CONTROL_TIER.control_metrics().font_size); cx.update(|cx| { moon_ui::MoonTheme::install_config( crate::startup::moon_theme_config_for_presentation(UiThemeMode::Dark, 1.0), diff --git a/crates/moon-ui-gpui/src/panels/chart/news.rs b/crates/moon-ui-gpui/src/panels/chart/news.rs index dea470730..d08816f08 100644 --- a/crates/moon-ui-gpui/src/panels/chart/news.rs +++ b/crates/moon-ui-gpui/src/panels/chart/news.rs @@ -281,7 +281,7 @@ impl ChartPanel { /// GPUI repaints at a fraction of that, so a mark-anchored card would visibly lag behind it. pub(super) fn news_card( &self, - ppp: f32, + sf: f32, ctrl: bool, palette: MoonPalette, cx: &App, @@ -291,14 +291,15 @@ impl ChartPanel { } let hover = self.news.hover.as_ref()?; let (cursor_x, _) = self.input.cursor?; - let ppp = ppp.max(0.1); + let sf = sf.max(0.1); let slot = self.chart.slot_dev_size(); - let slot_w = slot.0 as f32 / ppp; - let slot_h = slot.1 as f32 / ppp; + let slot_w = slot.0 as f32 / sf; + let slot_h = slot.1 as f32 / sf; let card_w = f32::from(design::ui_px(cx, CARD_W)); - // Device pixels → the slot's logical pixels the overlay is laid out in. Keep the card fully - // inside the slot, centred on the cursor when there is room. - let left = (cursor_x / ppp - card_w * 0.5).clamp(0.0, (slot_w - card_w).max(0.0)); + // Device pixels → the slot's content pixels the overlay is laid out in, through the + // window's effective factor. Keep the card fully inside the slot, centred on the cursor + // when there is room. + let left = (cursor_x / sf - card_w * 0.5).clamp(0.0, (slot_w - card_w).max(0.0)); // Sit above the marks' row: the axis gutter, then the gem at its HOVERED size, then a gap. // Derived from the same constants the geometry uses, so the card cannot drift from the marks. let axis_h = if self.time_axis_visible { diff --git a/crates/moon-ui-gpui/src/panels/chart/render.rs b/crates/moon-ui-gpui/src/panels/chart/render.rs index b6f1bad9a..3a88ae50b 100644 --- a/crates/moon-ui-gpui/src/panels/chart/render.rs +++ b/crates/moon-ui-gpui/src/panels/chart/render.rs @@ -52,6 +52,10 @@ impl Render for ChartPanel { // Cache DPI for the data-prepare path, which has no Window. DPI changes infrequently. self.last_ppp = ppp; self.chart.set_last_ppp(ppp); + // Device pixels per content pixel: what the engine's device-pixel geometry is divided + // by to place a GPUI overlay in the slot. `ppp` above would land it at `zoom` times the + // distance under UI zoom. + let sf = window.scale_factor(); let palette = MoonPalette::active(cx); self.chart.set_ui_palette(palette); // Bootstrap only: chartdx refines this from real `gpu_canvas.frame()` cadence, @@ -186,7 +190,7 @@ impl Render for ChartPanel { // and collect these positions once for the overlay list. let close_btns: Vec<(usize, f32, f32)> = axis_panes .iter() - .map(|(idx, rect, _)| (*idx, (rect.x + rect.w) / ppp, rect.y / ppp)) + .map(|(idx, rect, _)| (*idx, (rect.x + rect.w) / sf, rect.y / sf)) .collect(); // Cursor-only motion is handled by the chart-slot hitbox below. It updates retained // gpu_canvas cursor/readout directly and does not notify the GPUI tree. @@ -211,8 +215,8 @@ impl Render for ChartPanel { ( *idx, self.chart.pane_pinned(*idx), - rect.x / ppp + axis_off, - rect.y / ppp, + rect.x / sf + axis_off, + rect.y / sf, ) }) .collect(); @@ -224,7 +228,7 @@ impl Render for ChartPanel { let lock_btns: Vec<(usize, f32, f32)> = if self.compare_eligible { axis_panes .iter() - .map(|(idx, rect, _)| (*idx, rect.x / ppp + axis_off, rect.y / ppp)) + .map(|(idx, rect, _)| (*idx, rect.x / sf + axis_off, rect.y / sf)) .collect() } else { Vec::new() @@ -234,7 +238,7 @@ impl Render for ChartPanel { let broom_btns: Vec<(usize, f32, f32)> = if self.compare_eligible && compare_anchor { axis_panes .iter() - .map(|(idx, rect, _)| (*idx, rect.x / ppp + axis_off, rect.y / ppp)) + .map(|(idx, rect, _)| (*idx, rect.x / sf + axis_off, rect.y / sf)) .collect() } else { Vec::new() @@ -245,7 +249,7 @@ impl Render for ChartPanel { // broom pane, whose book covers the whole slot: there is no boundary left to draw, and a // strip on the right would name one where the whole pane trades. Tuple fields are // (idx, logical left, logical top, logical width, logical height), converted from axis_panes - // device pixels by dividing by ppp, like the close buttons. + // device pixels by dividing by the window's factor `sf`, like the close buttons. // `!self.historical` for the reason the strip exists at all: it marks where an // order-placement click lands, and a historical viewer places no orders. Shading a strip // for a gesture that was just removed would leave the window still saying "trading here". @@ -260,7 +264,7 @@ impl Render for ChartPanel { // The rectangle the CLICKS use, converted to logical pixels — shading anything // else would promise a boundary the hit test does not honour. let zone = self.control_zone_of(*rect); - (*idx, zone.x / ppp, zone.y / ppp, zone.w / ppp, zone.h / ppp) + (*idx, zone.x / sf, zone.y / sf, zone.w / sf, zone.h / sf) }) .collect() } else { @@ -276,16 +280,16 @@ impl Render for ChartPanel { // pointer events, so a mark can slide out from under a resting cursor. let news_ctrl = window.modifiers().secondary(); self.revalidate_news_hover(cx); - let news_card = self.news_card(ppp, news_ctrl, palette, cx); + let news_card = self.news_card(sf, news_ctrl, palette, cx); // Warning badges: same re-validate (the chart scrolls between events); the card is Ctrl-gated // like the news card, reusing the same live modifier state. self.revalidate_warn_hover(cx); - let warn_card = self.warn_card(ppp, news_ctrl, palette, cx); + let warn_card = self.warn_card(sf, news_ctrl, palette, cx); // Trade arrows: same re-validate, and for a stronger reason — an arrow is anchored to a // PRICE, so it slides out from under a resting cursor on a Y auto-fit as well as on a // scroll. This card needs no modifier (see `trade_history_hover`). self.revalidate_trade_hover(cx); - let trade_card = self.trade_hover_card(ppp, palette, cx); + let trade_card = self.trade_hover_card(sf, palette, cx); // Per-figure settings panel, opened from a right-click on a figure. Rendered here beside // the other overlays rather than as a context menu: it holds swatches and switches, which // a menu of text items cannot draw. @@ -336,7 +340,7 @@ impl Render for ChartPanel { // Placeholder lockup width for this pane. The share and its bounds are brand geometry, so // they live with the artwork in `design`, not as three literals in a panel. - let logo_w = ((slot_w as f32 / ppp) * crate::design::CHART_LOGO_SLOT_SHARE).clamp( + let logo_w = ((slot_w as f32 / sf) * crate::design::CHART_LOGO_SLOT_SHARE).clamp( crate::design::CHART_LOGO_MIN_W, crate::design::CHART_LOGO_MAX_W, ); @@ -468,7 +472,7 @@ impl Render for ChartPanel { // bounds go in multiplied by the content zoom, with the zoom taken out // of the factor. let zoom = window.content_zoom(); - let sf = window.scale_factor() / zoom; + let platform_sf = window.scale_factor() / zoom; let firetest_probe = crate::firetest::ChartProbe::new( crate::window::windowing::window_hwnd(window), f32::from(window.window_bounds().get_bounds().origin.x), @@ -477,7 +481,7 @@ impl Render for ChartPanel { f32::from(bounds.origin.y) * zoom, f32::from(bounds.size.width) * zoom, f32::from(bounds.size.height) * zoom, - sf, + platform_sf, ); if is_main { if let Some(probe) = firetest_probe { diff --git a/crates/moon-ui-gpui/src/panels/chart/render_input.rs b/crates/moon-ui-gpui/src/panels/chart/render_input.rs index 0b1046144..a4dfbec73 100644 --- a/crates/moon-ui-gpui/src/panels/chart/render_input.rs +++ b/crates/moon-ui-gpui/src/panels/chart/render_input.rs @@ -180,7 +180,7 @@ fn wheel_mode(modifiers: Modifiers) -> input::WheelMode { pub(super) fn scroll_wheel( this: &mut ChartPanel, e: &ScrollWheelEvent, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { if cx.has_active_drag() { @@ -195,7 +195,9 @@ pub(super) fn scroll_wheel( if this.orderbook_only { return; } - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; let Some((pos, within)) = this.chart_local(e.position) else { return; }; @@ -277,7 +279,9 @@ pub(super) fn mouse_down_left( window, cx, ); - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; let Some((pos, within)) = this.chart_local(e.position) else { return; }; @@ -479,7 +483,7 @@ pub(super) fn mouse_down_left( pub(super) fn mouse_up_left( this: &mut ChartPanel, e: &MouseUpEvent, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { // Before every early return below: the release ends the gesture whichever branch takes it. @@ -504,7 +508,9 @@ pub(super) fn mouse_up_left( if release_order_drag(this, TradeMouseButton::Left, cx) { return; } - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; let fb = this.chart.slot_dev_width(); let changed = { let input = &mut this.input; @@ -540,7 +546,9 @@ pub(super) fn mouse_down_right( // pointer leaving the slot gets no release at all, and clearing it there would have to trust // the fork's non-client mouse moves, which report no pressed button DURING a live drag. this.suppress_rmb_up = false; - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; let Some((pos, within)) = this.chart_local(e.position) else { return; }; @@ -656,7 +664,7 @@ pub(super) fn mouse_down_right( pub(super) fn mouse_up_right( this: &mut ChartPanel, e: &MouseUpEvent, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { // A right-button drag zooms Y and is paced like any other, but `mouse_button(Right, false, ..)` @@ -672,7 +680,9 @@ pub(super) fn mouse_up_right( if this.window_pos_in_control_zone(e.position, cx) { return; } - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; let fb = this.chart.slot_dev_width(); let changed = { let input = &mut this.input; @@ -766,7 +776,7 @@ pub(super) fn mouse_down_middle( pub(super) fn mouse_move( this: &mut ChartPanel, e: &MouseMoveEvent, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { if cx.has_active_drag() { @@ -864,7 +874,9 @@ pub(super) fn mouse_move( return; } crate::diag::bump(&crate::diag::CHART_MOUSE_MOVE_ENTITY); - let sf = window.scale_factor(); + // Chart-design factor: the input container lays the panes out with the engine's geometry, + // which excludes the UI zoom (`render.rs` caches it beside `set_last_ppp`). + let sf = this.last_ppp; this.input.sync_pressed( e.pressed_button == Some(MouseButton::Left), e.pressed_button == Some(MouseButton::Right), diff --git a/crates/moon-ui-gpui/src/panels/chart/trade_history_hover.rs b/crates/moon-ui-gpui/src/panels/chart/trade_history_hover.rs index adc739df4..2266d9a03 100644 --- a/crates/moon-ui-gpui/src/panels/chart/trade_history_hover.rs +++ b/crates/moon-ui-gpui/src/panels/chart/trade_history_hover.rs @@ -256,7 +256,7 @@ impl ChartPanel { /// its card sits on a fixed axis row; a price-anchored card can be pushed against any edge. /// /// Args: - /// ppp: Device pixels per logical pixel for the current window. + /// sf: Device pixels per content pixel — the window's effective factor, UI zoom included. /// palette: Active theme colours. /// cx: Application context used for scaled design tokens and translated labels. /// @@ -264,16 +264,16 @@ impl ChartPanel { /// The positioned card, or `None` when no hovered trade can be displayed. pub(super) fn trade_hover_card( &self, - ppp: f32, + sf: f32, palette: MoonPalette, cx: &App, ) -> Option { let hover = self.trade_hover.hover.as_ref()?; let (cursor_x, cursor_y) = self.input.cursor?; - let ppp = ppp.max(0.1); + let sf = sf.max(0.1); let slot = self.chart.slot_dev_size(); - let slot_w = slot.0 as f32 / ppp; - let slot_h = slot.1 as f32 / ppp; + let slot_w = slot.0 as f32 / sf; + let slot_h = slot.1 as f32 / sf; let gap = f32::from(design::ui_px(cx, CARD_GAP)); // Insets have to fit before they can be honoured: on a pane narrower than two insets, a // fixed inset would leave a negative width. The slot always wins over the preferred size. @@ -321,8 +321,8 @@ impl ChartPanel { // Device pixels → the slot's logical pixels the overlay is laid out in. Prefer the side of // the cursor with room, so the card never covers the arrow the user is pointing at. - let cursor_x = cursor_x / ppp; - let cursor_y = cursor_y / ppp; + let cursor_x = cursor_x / sf; + let cursor_y = cursor_y / sf; let left = if cursor_x + gap + card_w + inset <= slot_w { cursor_x + gap } else { diff --git a/crates/moon-ui-gpui/src/panels/chart/warn.rs b/crates/moon-ui-gpui/src/panels/chart/warn.rs index f4ef58194..ba7e61362 100644 --- a/crates/moon-ui-gpui/src/panels/chart/warn.rs +++ b/crates/moon-ui-gpui/src/panels/chart/warn.rs @@ -282,7 +282,7 @@ impl ChartPanel { /// The hover card, or `None` unless a badge is hovered AND Ctrl (Command on macOS) is held. pub(super) fn warn_card( &self, - ppp: f32, + sf: f32, ctrl: bool, palette: MoonPalette, cx: &App, @@ -293,12 +293,12 @@ impl ChartPanel { let hover = self.warn.hover.as_ref()?; let (cursor_x, _) = self.input.cursor?; let (cluster, mark) = self.warn.items.get(hover.nearest)?; - let ppp = ppp.max(0.1); + let sf = sf.max(0.1); let slot = self.chart.slot_dev_size(); - let slot_w = slot.0 as f32 / ppp; - let slot_h = slot.1 as f32 / ppp; + let slot_w = slot.0 as f32 / sf; + let slot_h = slot.1 as f32 / sf; let card_w = f32::from(design::ui_px(cx, CARD_W)); - let left = (cursor_x / ppp - card_w * 0.5).clamp(0.0, (slot_w - card_w).max(0.0)); + let left = (cursor_x / sf - card_w * 0.5).clamp(0.0, (slot_w - card_w).max(0.0)); let axis_h = if self.time_axis_visible { moon_chart::TIME_AXIS_H } else { diff --git a/crates/moon-ui-gpui/src/panels/common.rs b/crates/moon-ui-gpui/src/panels/common.rs index e582ef114..fc1e31278 100644 --- a/crates/moon-ui-gpui/src/panels/common.rs +++ b/crates/moon-ui-gpui/src/panels/common.rs @@ -618,8 +618,8 @@ pub(crate) fn popup_gear_trigger( /// Builds the ⚙ that opens a settings popup on a fixed-height chrome strip. /// -/// Same as [`popup_gear_trigger`] but pinned to `MoonSize::Xs` so it stays dense regardless of -/// the app's density setting. +/// Same as [`popup_gear_trigger`] but pinned to `MoonSize::Xs`, the dense strip's own tier rather +/// than [`design::CONTROL_TIER`]. pub(crate) fn popup_gear_trigger_dense( id: impl Into, tooltip: impl Into, diff --git a/crates/moon-ui-gpui/src/panels/report/window.rs b/crates/moon-ui-gpui/src/panels/report/window.rs index 350c29e82..8bd1f7c60 100644 --- a/crates/moon-ui-gpui/src/panels/report/window.rs +++ b/crates/moon-ui-gpui/src/panels/report/window.rs @@ -244,9 +244,11 @@ pub fn open_scoped( display.as_ref().map(|display| display.visible_bounds()), saved_origin_is_visible, ); + // The minimum is design pixels; the window API takes the platform's. + let zoom = moon_ui::MoonTheme::content_zoom(cx); let min_size = size( - px(REPORT_WINDOW_MIN_W.min(f32::from(bounds.size.width))), - px(REPORT_WINDOW_MIN_H.min(f32::from(bounds.size.height))), + px((REPORT_WINDOW_MIN_W * zoom).min(f32::from(bounds.size.width))), + px((REPORT_WINDOW_MIN_H * zoom).min(f32::from(bounds.size.height))), ); let mut options = crate::window::windowing::tool_window_options( crate::persistence::panel_meta::panel_title("Report").to_string(), diff --git a/crates/moon-ui-gpui/src/settings/connections/columns.rs b/crates/moon-ui-gpui/src/settings/connections/columns.rs index dc5b17864..15dd89d82 100644 --- a/crates/moon-ui-gpui/src/settings/connections/columns.rs +++ b/crates/moon-ui-gpui/src/settings/connections/columns.rs @@ -94,8 +94,8 @@ pub(super) enum ConnColWidth { /// /// The text columns share it for a second reason: their shrink order at a narrow window /// is decided by their RESOLVED bases, so a mixed policy would let one Font setting overtake - /// another column. Density now selects the text adjustment, while MoonUI still permits - /// custom finite font deltas. A scale-free comparison keeps that ordering independent + /// another column. The design fixes the text adjustment (`DESIGN_FONT_DELTA`), while MoonUI + /// still permits custom finite font deltas. A scale-free comparison keeps that ordering independent /// of either source. TextScaled, } diff --git a/crates/moon-ui-gpui/src/settings/general.rs b/crates/moon-ui-gpui/src/settings/general.rs index c5df3b84a..fb4099814 100644 --- a/crates/moon-ui-gpui/src/settings/general.rs +++ b/crates/moon-ui-gpui/src/settings/general.rs @@ -174,7 +174,7 @@ impl SettingsView { } } - /// Keep equal-width arrows large enough for the active density and zoom. + /// Keep equal-width arrows large enough for the body text at any zoom. /// Build a `<< < value > >>` stepper row with small and large adjustments. /// Shared by second/day counters and the Storage version limit; `adjust` owns clamping. pub(super) fn stepper_controls( diff --git a/crates/moon-ui-gpui/src/shell/ticker.rs b/crates/moon-ui-gpui/src/shell/ticker.rs index 8bc2b7033..3bebb7edc 100644 --- a/crates/moon-ui-gpui/src/shell/ticker.rs +++ b/crates/moon-ui-gpui/src/shell/ticker.rs @@ -158,8 +158,8 @@ impl Shell { + cluster_gap + 1.0 + cluster_gap; - // Start below the same drawn header at Compact, Standard, and Large so the popup never - // overlaps chrome when the density tier changes. + // Start below the drawn header, whatever height the font delta gives it, so the popup + // never overlaps chrome. let top = design::header_height(cx); let overlay = div() .id("header-ticker-popup-box") diff --git a/crates/moon-ui-gpui/src/strategies/settings.rs b/crates/moon-ui-gpui/src/strategies/settings.rs index 6169d983b..516df874c 100644 --- a/crates/moon-ui-gpui/src/strategies/settings.rs +++ b/crates/moon-ui-gpui/src/strategies/settings.rs @@ -411,7 +411,7 @@ fn popup_text_width(cx: &App, text: &str, base_size: f32, weight: f32) -> f32 { design::ui_text_width(cx, text, base_size, weight, false) } -/// Measure text set in the density-selected checkbox label's face, in the popup's proportional family. +/// Measure text set in the control-tier checkbox label's face, in the popup's proportional family. /// /// That face follows the UI zoom but not the legacy font-delta channel (see /// [`crate::panels::common::CHECKBOX_METRICS`]), so it is measured apart from @@ -468,7 +468,7 @@ fn settings_content_width_value( title_w.max(group_content + group_inset_px) } -/// Build the tree text-step row: its label, then a density-sized stepper. +/// Build the tree text-step row: its label, then a control-tier stepper. /// /// A sibling of [`pref_group`]'s checkbox rows rather than a member of [`POPUP_ROWS`] — the /// preference is numeric, not boolean, so it cannot share `PrefRow`'s bool-typed function diff --git a/crates/moon-ui-gpui/src/strategies/tree/checks.rs b/crates/moon-ui-gpui/src/strategies/tree/checks.rs index b184ffa95..4c30de1c1 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/checks.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/checks.rs @@ -134,7 +134,7 @@ pub(super) fn bulk_check( /// /// The reservation is a real checkbox made invisible rather than a width taken from a mirrored /// metric: both visible and hidden boxes use row_checkbox and its tier-following size, so their -/// widths stay identical at every density and UI zoom. Hidden costs nothing beyond layout — GPUI returns before painting the subtree or +/// widths stay identical at every UI zoom. Hidden costs nothing beyond layout — GPUI returns before painting the subtree or /// registering its mouse listeners (`div.rs`), so the reserved column has no hitbox of its own. /// /// Args: diff --git a/crates/moon-ui-gpui/src/strategies/tree/moon.rs b/crates/moon-ui-gpui/src/strategies/tree/moon.rs index 899392dee..ffaf821c0 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/moon.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/moon.rs @@ -83,7 +83,7 @@ const ROW_PAD_BASE: f32 = 4.5; /// Historical custom badge metrics used only when the local tree-text step is positive. /// The zero-step branch uses the shared Xs tier. The Custom box stays on the legacy-tuned /// constants — including font_size — because migrating the font alone would put text on the -/// density tier inside a box that never moved (this task's exception for that split). +/// control tier inside a box that never moved (this task's exception for that split). const BADGE_TINY_H: f32 = 13.0; const BADGE_TINY_RADIUS: f32 = 4.0; const BADGE_TINY_FONT: f32 = 8.5; 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 df2c23ecd..648438dd1 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/pane_cache.rs @@ -286,9 +286,9 @@ impl StrategiesView { let locale = rust_i18n::locale(); let locale: &str = &locale; let font_size = design::CONTROL_TIER.control_metrics().font_size; - // Fold the density tier (via its control font) and the UI-zoom size the measurement - // actually uses: `text_metrics_key` is the font channel, so without the `ui()` bits a - // density or zoom change would keep a stale width for every already-cached row. + // Fold the control tier's font and the UI-zoom size the measurement actually uses: + // `text_metrics_key` is the font channel, so without the `ui()` bits a font or zoom + // change would keep a stale width for every already-cached row. let metrics = design::text_metrics_key(cx, font_size, FOOTER_LABEL_WEIGHT, false) ^ u64::from(design::ui_value(cx, font_size).to_bits()); // Compared field by field rather than against a freshly built key: owning the locale means diff --git a/crates/moon-ui-gpui/src/strategies/tree/ui.rs b/crates/moon-ui-gpui/src/strategies/tree/ui.rs index 879b3e289..c8f50bc14 100644 --- a/crates/moon-ui-gpui/src/strategies/tree/ui.rs +++ b/crates/moon-ui-gpui/src/strategies/tree/ui.rs @@ -531,7 +531,7 @@ impl Render for DragChip { .border_1() .border_color(moon(p.blue)) .text_color(moon(p.text)) - // Raw GPUI text, already scaled on the density-tier channel. + // Raw GPUI text, already scaled on the body-text channel. .text_size(design::t_body_step_px(cx, self.step)) .font_family(design::mono()) .child(self.label.clone()) diff --git a/crates/moon-ui-gpui/src/window/detached.rs b/crates/moon-ui-gpui/src/window/detached.rs index bdf6d488e..fc25f1ff0 100644 --- a/crates/moon-ui-gpui/src/window/detached.rs +++ b/crates/moon-ui-gpui/src/window/detached.rs @@ -676,10 +676,12 @@ pub fn spawn( Bounds { origin, ..bounds } } }; + // A first-open size is design pixels; the window API takes the platform's. + let zoom = moon_ui::MoonTheme::content_zoom(app); let fallback = crate::window::windowing::detached_fallback_bounds( owner, display_id, - size(px(1100.0), px(520.0)), + size(px(1100.0 * zoom), px(520.0 * zoom)), app, ); let bounds = diff --git a/crates/moon-ui-gpui/src/window/login.rs b/crates/moon-ui-gpui/src/window/login.rs index 98c6f302c..b80482add 100644 --- a/crates/moon-ui-gpui/src/window/login.rs +++ b/crates/moon-ui-gpui/src/window/login.rs @@ -467,14 +467,18 @@ pub(crate) fn open( cx: &mut App, done: impl FnOnce(LoginOutcome, &mut App) + 'static, ) -> Option> { + // The prompt's size is design pixels; the window API takes the platform's, so the content + // zoom the theme already carries multiplies in, or a 200 % prompt would open at half its + // designed room every launch. + let zoom = moon_ui::MoonTheme::content_zoom(cx); let bounds = Bounds { origin: point(px(240.0), px(180.0)), - size: size(px(460.0), px(320.0)), + size: size(px(460.0 * zoom), px(320.0 * zoom)), }; let opts = crate::window::windowing::login_window_options( t!("login.window_title").to_string(), WindowBounds::Windowed(bounds), - Some(size(px(380.0), px(260.0))), + Some(size(px(380.0 * zoom), px(260.0 * zoom))), ); let mut done = Some(Box::new(done) as LoginDone); let opened = cx.open_window(opts, |window, cx| { diff --git a/crates/moon-ui-gpui/tests/theme_contract/chart.rs b/crates/moon-ui-gpui/tests/theme_contract/chart.rs index 6f413e8bd..84e812200 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/chart.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/chart.rs @@ -1115,7 +1115,7 @@ fn report_coin_history_load_does_not_change_the_viewport() { } } -/// `labels_popup/mod.rs:name_budget` must subtract UI-scaled density-tier button padding, not +/// `labels_popup/mod.rs:name_budget` must subtract UI-scaled control-tier button padding, not /// Font-scaled padding: replacing the tier's `pad_x` with `design::font_w` makes /// a raised UI delta overflow the chart-label name button while Font remains at its default. #[test] diff --git a/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs b/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs index 6d1d528fe..f04dd4522 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/dock_chrome.rs @@ -109,13 +109,13 @@ fn pinned_scope_host_keeps_its_tooltip() { /// `design::action_control_h_value` and `design::glyph_btn_w` must read the SAME upstream MoonUI /// metric — `glyph_btn_w` now delegates straight to `action_control_h_value`, which itself reads -/// the density tier's `control_metrics().height` rather than a hand-copied literal. Source vs +/// the control tier's `control_metrics().height` rather than a hand-copied literal. Source vs /// source, not a constant compared against its own literal: this is the one oracle available for /// either, since neither can be checked against the private `MoonButtonMetrics` directly. /// /// Mutation: re-fork them — give `glyph_btn_w` its own literal instead of delegating, or have /// `action_control_h_value` stop reading `CONTROL_TIER.control_metrics().height`. Consequence: -/// the pinned chip (sized off `glyph_btn_w`) stops matching the density-tier controls standing +/// the pinned chip (sized off `glyph_btn_w`) stops matching the control-tier controls standing /// beside it in the same row — a drift neither function's own body would ever reveal by itself. #[test] fn action_control_h_value_agrees_with_glyph_btn_w() { diff --git a/crates/moon-ui-gpui/tests/theme_contract/main.rs b/crates/moon-ui-gpui/tests/theme_contract/main.rs index 399bd6dbe..dd0cad066 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/main.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/main.rs @@ -27,6 +27,7 @@ mod report; mod shared_config; mod shell; mod shot; +mod size_system; mod startup; mod strategies; mod telegram_core; diff --git a/crates/moon-ui-gpui/tests/theme_contract/shell.rs b/crates/moon-ui-gpui/tests/theme_contract/shell.rs index e7834c3c8..c4464d9bf 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/shell.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/shell.rs @@ -1170,7 +1170,7 @@ fn leverage_presets_only_stage_values_until_apply() { ); } -/// `controls/toolbar.rs:row_fit` must budget the Profit Monitor launcher; changing its density-tier +/// `controls/toolbar.rs:row_fit` must budget the Profit Monitor launcher; changing its control-tier /// glyph multiplier back to four makes this assertion red and lets the trailing launcher clip at narrow /// Main-window widths. #[test] diff --git a/crates/moon-ui-gpui/tests/theme_contract/size_system.rs b/crates/moon-ui-gpui/tests/theme_contract/size_system.rs new file mode 100644 index 000000000..0e26fa9ad --- /dev/null +++ b/crates/moon-ui-gpui/tests/theme_contract/size_system.rs @@ -0,0 +1,83 @@ +//! Static contracts for the terminal's one size system. +//! +//! The terminal renders one design: ordinary controls on `design::CONTROL_TIER`, text on +//! `design::BODY_TEXT` plus a local step, scaled as a whole by the window content zoom. These +//! contracts keep a surface from forking that system back into a second one. + +use super::support::{Path, braced_body, code_only, read_src, rust_sources}; + +/// Terminal production code must not return to MoonUI's legacy font channel outside its one mirror. +/// +/// Breakage: adding `tokens.font(` or `design::font_value(` to a terminal surface sizes that text +/// on `font_delta` instead of the body size, so it stops agreeing with the controls beside it. +#[test] +fn terminal_text_uses_no_legacy_font_channel_outside_documented_mirrors() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut sources = Vec::new(); + rust_sources(&root, &mut sources); + let violations: Vec = sources + .iter() + .filter(|path| path.file_name().is_some_and(|name| name != "tests.rs")) + .filter_map(|path| { + let rel = path + .strip_prefix(&root) + .ok()? + .to_string_lossy() + .replace('\\', "/"); + let code = code_only(&std::fs::read_to_string(path).ok()?); + (!matches!(rel.as_str(), "design.rs" | "settings/connections/table.rs") + && (code.contains("tokens.font(") || code.contains("design::font_value("))) + .then_some(rel) + }) + .collect(); + assert!( + violations.is_empty(), + "legacy font-channel calls outside documented mirrors: {violations:?}" + ); +} + +/// Settings and Analytics tabs must use the control tier instead of custom pixel metrics. +/// +/// Breakage: restoring `MoonButtonSize::Custom` pins a tab's text and height while its neighbours +/// follow the tier, so the rows stop aligning. +#[test] +fn settings_and_analytics_tabs_do_not_pin_custom_button_metrics() { + for path in ["settings/render.rs", "analytics/toolbar.rs"] { + assert!( + !code_only(&read_src(path)).contains("MoonButtonSize::Custom {"), + "{path} must use the control tier instead of a custom button size" + ); + } +} + +/// Ordinary controls must read the tier through `design::CONTROL_TIER`, never as a literal. +/// +/// Breakage: a literal `MoonSize::Sm` in a chrome or toolbar control is a second spelling of the +/// one size system; the day the constant moves, that control stays behind. +#[test] +fn ordinary_controls_do_not_spell_the_control_tier_as_a_literal() { + for path in [ + "controls/metric.rs", + "controls/toolbar.rs", + "chrome/terminal_chrome.rs", + ] { + assert!( + !code_only(&read_src(path)).contains("MoonSize::Sm"), + "{path} must read design::CONTROL_TIER instead of spelling MoonSize::Sm" + ); + } +} + +/// Toolbar strip text must carry the body text's rendered metrics rather than a font-channel label. +/// +/// Breakage: removing `rendered_metrics(design::text_metrics(` makes the Size and Sell captions +/// use a legacy size, so their row no longer aligns with the preset controls. +#[test] +fn toolbar_strip_text_uses_rendered_text_metrics() { + let toolbar = read_src("controls/toolbar.rs"); + let strip_text = code_only(braced_body(&toolbar, "fn strip_text(")); + assert!( + strip_text.contains(".rendered_metrics(design::text_metrics("), + "controls/toolbar.rs:strip_text must use rendered text metrics" + ); +} diff --git a/crates/moon-ui-gpui/tests/theme_contract/strategies.rs b/crates/moon-ui-gpui/tests/theme_contract/strategies.rs index 14ba807d8..9e9bc8f33 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/strategies.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/strategies.rs @@ -966,7 +966,7 @@ fn strategy_footer_is_one_atomic_action_row() { assert!(!selection.contains("176.0") && !selection.contains("MoonButtonSize::Micro")); assert!( !selection.contains(".size("), - "the selection toolbar's buttons must inherit the app's density tier, never pin one \ + "the selection toolbar's buttons must inherit the control tier, never pin one \ back explicitly" ); assert!( diff --git a/crates/moon-ui-gpui/tests/theme_contract/theme.rs b/crates/moon-ui-gpui/tests/theme_contract/theme.rs index ffad019ae..79ccca56f 100644 --- a/crates/moon-ui-gpui/tests/theme_contract/theme.rs +++ b/crates/moon-ui-gpui/tests/theme_contract/theme.rs @@ -487,8 +487,7 @@ fn header_ticker_deltas_take_their_tone_from_the_shared_delta_mapping() { /// Small buttons beside it — `design::action_control_h_value(cx)` — never a flat copied height. /// /// The future edit this pins against: returning to a copied `SEL_H` constant, which stops following -/// the density tier. The pill would then sit shorter than neighbouring action controls at Compact -/// or Large density. +/// the control tier. The pill would then sit shorter than neighbouring action controls. #[test] fn header_core_pill_shares_the_toolbar_buttons_fit_rule() { let source = read_src("chrome/terminal_chrome.rs"); @@ -502,7 +501,7 @@ fn header_core_pill_shares_the_toolbar_buttons_fit_rule() { assert!( body.contains("let trigger_h = design::action_control_h_value(cx);"), - "chrome/terminal_chrome.rs:{signature} must size the pill trigger from the density-tier action control" + "chrome/terminal_chrome.rs:{signature} must size the pill trigger from the control-tier action control" ); assert!( !body.contains("SEL_H"), From c0f595f1ffe8672c7a580512f4063171e80b5b07 Mon Sep 17 00:00:00 2001 From: ThisMad Date: Mon, 21 Sep 2026 11:51:49 +0200 Subject: [PATCH 3/3] chore(deps): re-pin MoonUI to the window content zoom MoonUI #77 is merged: `Window::set_content_zoom`, `MoonScale::zoom` and the device-pixel centring of the toggle thumb, checkbox mark and radio dot. This branch calls that API, so the committed lock has to name a revision that has it; the previous pin (1767a249) does not compile here. Only the 23 packages sourced from the MoonUI repository move, 1767a249 -> e4f2d4e9. `cargo fetch --locked` and `cargo check -p moon-ui-gpui --locked` pass with no local override. --- Cargo.lock | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22eb75152..34356ccf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3801,7 +3801,7 @@ dependencies = [ [[package]] name = "moon-collections" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "indexmap", "moon-gpui-util", @@ -3850,7 +3850,7 @@ dependencies = [ [[package]] name = "moon-derive-refineable" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "proc-macro2", "quote", @@ -3860,7 +3860,7 @@ dependencies = [ [[package]] name = "moon-gpui" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "accesskit", "anyhow", @@ -3944,7 +3944,7 @@ dependencies = [ [[package]] name = "moon-gpui-linux" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "accesskit", "accesskit_unix", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "moon-gpui-macos" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "accesskit", "accesskit_macos", @@ -4042,7 +4042,7 @@ dependencies = [ [[package]] name = "moon-gpui-macros" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4053,7 +4053,7 @@ dependencies = [ [[package]] name = "moon-gpui-platform" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "console_error_panic_hook", "moon-gpui", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "moon-gpui-shared-string" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "schemars", "serde", @@ -4076,7 +4076,7 @@ dependencies = [ [[package]] name = "moon-gpui-util" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "log", @@ -4086,7 +4086,7 @@ dependencies = [ [[package]] name = "moon-gpui-web" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "console_error_panic_hook", @@ -4110,7 +4110,7 @@ dependencies = [ [[package]] name = "moon-gpui-wgpu" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "bytemuck", @@ -4139,7 +4139,7 @@ dependencies = [ [[package]] name = "moon-gpui-windows" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "accesskit", "accesskit_windows", @@ -4167,7 +4167,7 @@ dependencies = [ [[package]] name = "moon-http-client" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "async-compression", @@ -4187,7 +4187,7 @@ dependencies = [ [[package]] name = "moon-media" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "bindgen", @@ -4202,7 +4202,7 @@ dependencies = [ [[package]] name = "moon-perf" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "moon-collections", "serde", @@ -4212,7 +4212,7 @@ dependencies = [ [[package]] name = "moon-refineable" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "moon-derive-refineable", ] @@ -4220,7 +4220,7 @@ dependencies = [ [[package]] name = "moon-scheduler" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "async-task", "backtrace", @@ -4235,7 +4235,7 @@ dependencies = [ [[package]] name = "moon-sum-tree" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "heapless", "log", @@ -4246,7 +4246,7 @@ dependencies = [ [[package]] name = "moon-ui" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "moon-ui-components", "moon-ui-components-assets", @@ -4255,7 +4255,7 @@ dependencies = [ [[package]] name = "moon-ui-components" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "aho-corasick", "anyhow", @@ -4305,7 +4305,7 @@ dependencies = [ [[package]] name = "moon-ui-components-assets" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "anyhow", "log", @@ -4319,7 +4319,7 @@ dependencies = [ [[package]] name = "moon-ui-components-macros" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "proc-macro2", "quote", @@ -4373,7 +4373,7 @@ dependencies = [ [[package]] name = "moon-util-macros" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#1767a24932009007c869b32a74a4bf46acd41575" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#e4f2d4e915366dae6dd9da9f1f57deb12aa1a524" dependencies = [ "moon-perf", "quote",