Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/moon-ui-gpui/src/chart_tabs/main_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1512,12 +1512,13 @@ impl MainChartStack {
/// back to the raw market key rather than rendering blank.
///
/// Args:
/// _window: Unused; the strip is now in-flow and sizes itself.
/// window: Used only to render the strip through its own palette and scale tokens; the
/// strip is in-flow and still sizes itself.
/// cx: Stack context, used to read each panel and to build the click handlers.
///
/// Returns:
/// The row, or `None` when fewer than two charts are open.
fn render_tab_row(&self, _window: &mut Window, cx: &mut Context<Self>) -> Option<AnyElement> {
fn render_tab_row(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<AnyElement> {
// Vacated slots are retained placeholders in COMPRESS layout — they hold a position, not a
// chart, so they get no tab.
let live: Vec<(CoreId, String, SharedString)> = self
Expand Down Expand Up @@ -1591,6 +1592,12 @@ impl MainChartStack {
});
}
});
// Same treatment as the Main/Add strip above it: MoonUI keys an inactive tab label off
// `text_muted` and offers no per-tab colour prop, so the lift arrives as a palette.
// `render_with_theme`, never `render_with_palette` — the latter substitutes default tokens
// and would drop the user's font delta, shrinking these labels away from `strip_h`.
let strip_palette = moon_ui::MoonPalette::active(cx);
let strip = crate::design::chrome_tab_strip(strip, strip_palette, window, cx);
Some(
div()
.id("main-chart-tab-row")
Expand Down
2 changes: 1 addition & 1 deletion crates/moon-ui-gpui/src/chart_tabs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ fn preferred_auto_workspace_market(
/// font scaling cannot desynchronize the strip and underline from the tabs. With the default
/// `ui = 1` and `font_delta = 2`, it returns the former constant value of 30.
pub(super) fn chart_tab_strip_h(cx: &App) -> f32 {
crate::design::fit_h_value(cx, 28.0, 13.0, 7.5)
crate::design::tab_strip_h_value(cx)
}

/// Identity of a chart tab, ported from egui's `ContainerKind`.
Expand Down
5 changes: 5 additions & 0 deletions crates/moon-ui-gpui/src/chart_tabs/strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ impl Render for ChartTabs {
// The active tab's layout control and adjacent scale dropdown are both per-tab.
let popup_open = self.popup_shows(ChartPopup::Layout);
let p_strip = MoonPalette::active(cx);
// MoonUI keys an inactive tab label off `text_muted` and exposes no per-tab colour prop,
// so the lift is handed in as a palette. `render_with_theme`, never `render_with_palette`:
// the latter substitutes default tokens and would silently drop the user's font delta and
// UI scale, shrinking the labels and pulling the strip off `chart_tab_strip_h`.
let strip = design::chrome_tab_strip(strip, p_strip, window, cx);
let scale_dropdown = crate::controls::scale_dropdown_for_tabs(
cx,
self.active_scale_value(cx),
Expand Down
140 changes: 96 additions & 44 deletions crates/moon-ui-gpui/src/core_expert/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ impl Render for CoreExpertView {
// built below in this same frame.
self.build_editors(window, cx);
let p = MoonPalette::active(cx);
// Built before the tree: it needs `window` and `&mut cx`, which the builder chain below
// cannot lend it while it is also reading `cx` for its own scaled metrics.
let tab_strip = self.tab_strip(window, cx);
let body = self.body(p, window, cx).into_any_element();
let chrome_width = crate::window::windowing::responsive_width(window);
v_flex()
.size_full()
Expand All @@ -50,8 +54,8 @@ impl Render for CoreExpertView {
.track_focus(&self.focus)
.child(title_bar(p, cx))
.child(self.switch_row(p, cx))
.child(self.tab_strip(cx))
.child(self.body(p, cx))
.child(tab_strip)
.child(body)
.child(self.footer(p, cx))
.child(
MoonWindowFrame::tool("core-expert-frame-hit", chrome_width)
Expand Down Expand Up @@ -137,30 +141,85 @@ impl CoreExpertView {
}

/// Moonbot's tab strip, in Moonbot's order.
fn tab_strip(&self, cx: &Context<Self>) -> impl IntoElement {
///
/// Takes `window` because the strip is rendered through a lifted palette rather than the
/// active one: MoonUI keys an inactive tab label off `text_muted`, which sits under the body
/// contrast floor in both stock themes, and `render_with_theme` is the only way to hand it a
/// different palette. `AnyElement` for the same reason the chart strip boxes its own — the
/// returned element would otherwise hold the `&mut cx` borrow the caller still needs.
///
/// Args:
/// window: Window that owns the strip's persistent overflow state.
/// cx: View context used to read the selected tab and render the themed strip.
///
/// Returns:
/// The expert-tab strip in its fixed-height wrapper.
fn tab_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let view = cx.entity();
let selected = self.tab;
let items: Vec<MoonTabItem> = ExpertTab::ALL
.iter()
.map(|tab| MoonTabItem::new(tab.title()).selected(*tab == selected))
.collect();
let strip_h = design::tab_strip_h(cx);
let p = MoonPalette::active(cx);
let strip = MoonTabStrip::new("core-expert-tabs")
.gap(4.0)
.overflow_menu(true)
.items(items)
.on_click(move |ix, _event, _window, app| {
let Some(next) = ExpertTab::at(ix) else {
return;
};
view.update(app, |this, cx| this.set_tab(next, cx));
});
let strip = design::chrome_tab_strip(strip, p, window, cx);
div()
.w_full()
.flex_none()
.h(design::fit_h_px(cx, 28.0, 13.0, 7.5))
.child(
MoonTabStrip::new("core-expert-tabs")
.gap(4.0)
.overflow_menu(true)
.items(items)
.on_click(move |ix, _event, _window, app| {
let Some(next) = ExpertTab::at(ix) else {
return;
};
view.update(app, |this, cx| this.set_tab(next, cx));
})
.render(),
)
.h(strip_h)
.child(strip)
.into_any_element()
}

/// The Hotkeys page's own sub-tab strip, lifted out of [`Self::body`]'s builder chain.
///
/// It lives in its own method for the same reason [`Self::tab_strip`] takes `window`: the
/// strip is rendered through a lifted palette, which needs `window` and `&mut cx`, and the
/// `.children(...)` closure it used to sit inside can capture neither.
///
/// Args:
/// window: Window that owns the strip's persistent overflow state.
/// cx: View context used to read the selected sub-tab and render the themed strip.
///
/// Returns:
/// The Hotkeys sub-tab strip in its fixed-height wrapper.
fn hotkeys_sub_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let view = cx.entity();
let selected = self.hotkeys_sub;
let items: Vec<MoonTabItem> = pages::HotkeysSub::ALL
.iter()
.map(|sub| MoonTabItem::new(sub.title()).selected(*sub == selected))
.collect();
let strip_h = design::fit_h_px(cx, 26.0, 13.0, 7.5);
let p = MoonPalette::active(cx);
let strip = MoonTabStrip::new("core-expert-hotkeys-tabs")
.gap(4.0)
.overflow_menu(true)
.items(items)
.on_click(move |ix, _event, _window, app| {
let Some(next) = pages::HotkeysSub::at(ix) else {
return;
};
view.update(app, |this, cx| this.set_hotkeys_sub(next, cx));
});
let strip = design::chrome_tab_strip(strip, p, window, cx);
div()
.w_full()
.flex_none()
.h(strip_h)
.child(strip)
.into_any_element()
}

/// Body of the selected page.
Expand All @@ -170,7 +229,20 @@ impl CoreExpertView {
/// rather than by closing. With a page staged, a PORTED tab draws its rows. A tab that is not
/// ported yet says so, and says separately when the reason is that nothing can ever arrive for
/// it.
fn body(&self, p: MoonPalette, cx: &Context<Self>) -> impl IntoElement {
///
/// Args:
/// p: Active palette used by the page body and its notices.
/// window: Window forwarded to the Hotkeys sub-tab strip when that page is selected.
/// cx: View context used to read state and build the selected page.
///
/// Returns:
/// The scrollable body for the selected expert page.
fn body(
&self,
p: MoonPalette,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let view = cx.entity();
// Read here, from the window's own `&self`: a page is built inside this render, where
// reading the view back would panic.
Expand Down Expand Up @@ -222,6 +294,11 @@ impl CoreExpertView {
// A warning, through the shared component: a page whose values cannot arrive at all is not
// the same news as one merely awaiting its port.
let warn = self.state.can_send() && self.tab.source() != TabSource::Projected;
// Moonbot's Hotkeys page carries a strip of its own, above its body. Built HERE rather
// than inside the `.children(...)` closure below: it renders through a lifted palette, so
// it needs `window` and `&mut cx`, and a closure cannot capture either.
let hotkeys_strip = (self.tab == ExpertTab::Hotkeys && page.is_some())
.then(|| self.hotkeys_sub_strip(window, cx));
v_flex()
.id(self.tab.element_id())
.flex_1()
Expand Down Expand Up @@ -250,32 +327,7 @@ impl CoreExpertView {
.into_any_element()
}
}))
// Moonbot's Hotkeys page carries a strip of its own, above its body.
.children((self.tab == ExpertTab::Hotkeys && page.is_some()).then(|| {
let view = cx.entity();
let selected = self.hotkeys_sub;
let items: Vec<MoonTabItem> = pages::HotkeysSub::ALL
.iter()
.map(|sub| MoonTabItem::new(sub.title()).selected(*sub == selected))
.collect();
div()
.w_full()
.flex_none()
.h(design::fit_h_px(cx, 26.0, 13.0, 7.5))
.child(
MoonTabStrip::new("core-expert-hotkeys-tabs")
.gap(4.0)
.overflow_menu(true)
.items(items)
.on_click(move |ix, _event, _window, app| {
let Some(next) = pages::HotkeysSub::at(ix) else {
return;
};
view.update(app, |this, cx| this.set_hotkeys_sub(next, cx));
})
.render(),
)
}))
.children(hotkeys_strip)
.children(page)
}

Expand Down
129 changes: 128 additions & 1 deletion crates/moon-ui-gpui/src/design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use gpui::*;
use moon_core::util::fmt::DeltaSign;
use moon_ui::{MoonMetrics, MoonPalette, MoonTheme, MoonTone, rgba_from};
use moon_ui::{MoonMetrics, MoonPalette, MoonTableStyle, MoonTheme, MoonTone, rgba_from};
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};

Expand Down Expand Up @@ -160,6 +160,133 @@ pub fn chrome_toggle_label_color(p: MoonPalette, on: bool, caution: bool) -> u32
if on && caution { p.amber } else { p.text_soft }
}

/// Foreground for a SECONDARY chrome label that still has to be read at a glance: an inactive tab
/// title, a table column header, the pinned-scope chip.
///
/// One step above `p.text_muted`, which measures 3.5-3.8:1 against every chrome surface in both
/// stock themes and therefore sits under the 4.5:1 body floor; `p.text_soft` measures 6.7-7.0:1 on
/// light and 5.1-5.5:1 on dark. `p.text_dim` is NOT the step up: the dark palette defines it as the
/// same value as `p.text`, so using it here would erase the active/inactive distinction in dark
/// mode while looking correct in light mode.
///
/// This is the label tone only. The ACTIVE member of a pair keeps `p.text`, so raising the
/// inactive one narrows the gap rather than closing it.
///
/// Args:
/// p: Active palette whose secondary label tone is being resolved.
///
/// Returns:
/// The accessible secondary-label colour.
pub fn chrome_label_color(p: MoonPalette) -> u32 {
p.text_soft
}

/// Height of a chrome tab strip, in pixels at the current UI and font scale.
///
/// One source for the triple `(28, 13, 7.5)`, which is `MoonTabStrip`'s own tab height: a strip
/// whose row is a different height from the tabs inside it puts the active-tab underline off the
/// row's bottom edge. Every strip in the app resolves it here — the two chart strips through
/// [`chart_tab_strip_h`](crate::chart_tabs::chart_tab_strip_h), which delegates to
/// [`tab_strip_h_value`], and the four window strips directly.
///
/// Args:
/// cx: Application context supplying the current UI and font scales.
///
/// Returns:
/// The scaled tab-strip height.
pub fn tab_strip_h(cx: &App) -> Pixels {
px(tab_strip_h_value(cx))
}

/// [`tab_strip_h`] as a bare number, for a caller that needs the value rather than a length.
///
/// Args:
/// cx: Application context supplying the current UI and font scales.
///
/// Returns:
/// The scaled tab-strip height as a raw number.
pub fn tab_strip_h_value(cx: &App) -> f32 {
fit_h_value(cx, 28.0, 13.0, 7.5)
}

/// Renders one chrome tab strip through the lifted label palette and the LIVE theme tokens.
///
/// The one place the three-line incantation lives, because getting it wrong is silent:
/// `render_with_palette` is one argument shorter, compiles, and substitutes default tokens, which
/// drops the user's font delta and UI scale — the labels shrink and the strip stops matching
/// [`tab_strip_h`], putting the active-tab underline out of line. The palette and tokens are read
/// into locals first because they cannot be read from `cx` in the same argument list that hands
/// `cx` over mutably, and the result is boxed because it would otherwise hold that `&mut cx`
/// borrow for as long as the element lives, which every caller still needs.
///
/// Returns the strip BARE, with no sizing wrapper: a caller inside an already-sized row wants it
/// that way, and one placing it as a top-level child adds its own [`tab_strip_h`] box.
///
/// Args:
/// strip: Configured tab-strip builder to render.
/// p: Active palette whose muted label tone will be lifted.
/// window: Window that owns the strip's persistent overflow state.
/// cx: Application context supplying the current theme tokens.
///
/// Returns:
/// The themed, lifted tab strip without a sizing wrapper.
pub fn chrome_tab_strip(
strip: moon_ui::MoonTabStrip,
p: MoonPalette,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let palette = chrome_label_palette(p);
let tokens = MoonTheme::active_tokens(cx);
strip
.render_with_theme(window, cx, palette, tokens)
.into_any_element()
}

/// `p` with its muted text lifted to [`chrome_label_color`].
///
/// For MoonUI chrome that keys an inactive label off `text_muted` and exposes no per-item colour
/// prop — `MoonTabStrip` is the case that needs it. Exactly one field moves, so every other colour
/// the widget draws still comes from the same palette as the row around it.
///
/// Args:
/// p: Active palette to copy.
///
/// Returns:
/// A copy with only `text_muted` raised to the chrome label colour.
pub fn chrome_label_palette(p: MoonPalette) -> MoonPalette {
MoonPalette {
text_muted: chrome_label_color(p),
..p
}
}

/// The one table style every `MoonDataTable` in this crate attaches: the palette's own fills and
/// selection, with the column-header text lifted to [`chrome_label_color`].
///
/// `MoonDataTable::render` re-themes any style it is handed, but that pass only replaces a field
/// still holding the stock dark default, and the lifted `header_text` never equals it — so the
/// lift survives. Do not call `themed` here; it would be a no-op today and a silent reset the day
/// the defaults move.
///
/// One consequence was weighed and accepted rather than overlooked: MoonUI already draws the
/// SORTED column's header at this same tone, bypassing `header_text` entirely, so raising the
/// unsorted ones makes both read alike and leaves the sort arrow as the only sorted cue. The arrow
/// is part of the header's own text run and unambiguous; the alternative was leaving nine tables'
/// headings under the contrast floor to preserve a second, weaker signal.
///
/// Args:
/// p: Active palette supplying the table's non-header colours.
///
/// Returns:
/// The table style with an accessible column-header colour.
pub fn table_style(p: MoonPalette) -> MoonTableStyle {
MoonTableStyle {
header_text: chrome_label_color(p),
..MoonTableStyle::for_palette(p)
}
}

/// Icon standing for "which columns does this table show", on every column selector in the app.
///
/// All six pickers — Orders, Figures, Assets, the Screener, the Analytics tuner list and the
Expand Down
1 change: 1 addition & 0 deletions crates/moon-ui-gpui/src/panels/alerts/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl AlertsPanel {
.state(&table_state)
.header_height(design::TABLE_HEAD_H)
.row_height(design::TABLE_ROW_H)
.style(design::table_style(p))
// Row selection carries no meaning here — the marked row is the one whose settings are
// open — so a click clears the fork's three coupled selection fields immediately.
// Only when one of them is actually set: an unconditional `notify` here would wake the
Expand Down
1 change: 1 addition & 0 deletions crates/moon-ui-gpui/src/panels/assets/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,7 @@ pub(super) fn assets_table(
.state(state)
.header_height(design::TABLE_HEAD_H)
.row_height(design::TABLE_ROW_H)
.style(design::table_style(p))
// A header click re-sorts the cached rows; the action column is not sortable.
.on_sort(move |key, ascending, _window, app| {
let key = key.to_string();
Expand Down
Loading