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
111 changes: 100 additions & 11 deletions crates/moon-ui-gpui/src/chart_tabs/add_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! [`super::stack`]. Used by both the tab strip and detached windows ([`super::windows`]).

use std::cell::Cell;
use std::collections::HashSet;
use std::ops::Range;
use std::rc::Rc;
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -137,6 +138,19 @@ pub(crate) struct AddChartStack {
compact_timer_armed: bool,
/// Scroll handle for the vertical `MoonVirtualList` used by the stack's Scroll mode.
scroll: MoonVirtualListScrollHandle,
/// Whether the host presenting this stack is on screen.
///
/// The strip writes it through [`Self::set_scene_visible`]. A direct render — the detached
/// window, which never receives the dock hook, or this stack painted as the active tab —
/// stores `true` because paint proves the host is present. Child visibility is this flag
/// AND [`Self::viewport_visible`], so a range update cannot show a chart under a hidden host.
host_visible: bool,
/// Entity ids of children last known to lie in the painted viewport.
///
/// Identities, not indexes: pin reorder, a retained vacated slot, and close/add all renumber
/// the stack. Geometry writes this set before host visibility is applied, and a hide keeps
/// it so a later reveal can restore that subset when a cached ancestor skips the range callback.
viewport_visible: HashSet<EntityId>,
}

impl AddChartStack {
Expand Down Expand Up @@ -204,6 +218,8 @@ impl AddChartStack {
last_count_change: Instant::now(),
compact_timer_armed: false,
scroll: MoonVirtualListScrollHandle::new(),
host_visible: true,
viewport_visible: HashSet::new(),
}
}

Expand Down Expand Up @@ -1101,26 +1117,86 @@ impl AddChartStack {
}
}

/// Record whether the host is showing this stack, and propagate a real change.
///
/// An unchanged value returns immediately. A hide forces every child off and keeps
/// [`Self::viewport_visible`]. A reveal turns on only the nonvacated children in that set.
/// Children the viewport has not seen yet stay off until render or the next prepaint.
///
/// Args:
/// visible: Whether the host currently presents this stack.
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing; an unchanged host flag leaves child visibility and viewport membership
/// untouched.
pub(super) fn set_scene_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
for entry in &self.charts {
entry
.panel
.update(cx, |panel, _| panel.set_scene_visible(visible));
if self.host_visible == visible {
return;
}
self.host_visible = visible;
self.apply_viewport_visibility(cx);
}

/// Replace viewport membership from one reported display range, then push host-gated visibility.
///
/// Membership is derived before `host_visible` is consulted and is stored even when the host
/// is hidden. An empty range clears the set. A hidden host therefore cannot be switched back
/// on by this callback.
///
/// Args:
/// range: Display indexes the virtual list currently paints. Empty means the list has no rows.
/// render_order: Real chart indexes in the order the list paints them.
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing. A child is on only when the host is showing, the slot is not vacated, and its
/// entity id is in the range.
fn sync_stack_visible_ordered(
&mut self,
range: Range<usize>,
render_order: &[usize],
cx: &mut Context<Self>,
) {
for (ix, entry) in self.charts.iter().enumerate() {
let visible = !entry.vacated
&& render_order
.iter()
.enumerate()
.any(|(display_ix, real_ix)| *real_ix == ix && range.contains(&display_ix));
let mut viewport = HashSet::with_capacity(range.len());
for (display_ix, &real_ix) in render_order.iter().enumerate() {
if !range.contains(&display_ix) {
continue;
}
if let Some(entry) = self.charts.get(real_ix).filter(|entry| !entry.vacated) {
viewport.insert(entry.panel.entity_id());
}
}
self.viewport_visible = viewport;
self.apply_viewport_visibility(cx);
}

/// Remember every nonvacated child as viewport membership, without pushing visibility.
///
/// FIT, COMPRESS, and horizontal scroll mount each of those children and do not report a
/// range. Vertical scroll must not call this: its painted subset arrives from
/// [`Self::sync_stack_visible_ordered`].
fn remember_mounted_children(&mut self) {
self.viewport_visible.clear();
for entry in &self.charts {
if !entry.vacated {
self.viewport_visible.insert(entry.panel.entity_id());
}
}
}

/// Push `host_visible` AND remembered viewport membership to every child.
///
/// Args:
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing. The remembered set is not changed.
fn apply_viewport_visibility(&self, cx: &mut Context<Self>) {
for entry in &self.charts {
let visible = self.host_visible
&& !entry.vacated
&& self.viewport_visible.contains(&entry.panel.entity_id());
entry
.panel
.update(cx, |panel, _| panel.set_scene_visible(visible));
Expand All @@ -1142,12 +1218,19 @@ impl Render for AddChartStack {
/// Renders every Add/Custom chart as a labelled, guttered stack tile.
///
/// These stacks have no fullscreen mode, so their shared card helper always retains the
/// separator and never needs the Main stack's position note.
/// separator and never needs the Main stack's position note. Paint stores `host_visible`
/// because a rendered stack is on screen, including a detached window that never receives
/// the dock hook. It does not call `set_scene_visible`: vertical scroll keeps the last
/// reported range, and every mounted route records that mounted set for a later reveal.
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Paint means this host is on screen, detached window included. Store the flag only:
// `set_scene_visible` would mark every remembered child visible from inside render.
self.host_visible = true;
crate::diag::bump(&crate::diag::ADD_STACK_RENDER);
let _render_us = crate::diag::scope(&crate::diag::ADD_STACK_RENDER_US);
let palette = moon_ui::MoonPalette::active(cx);
if self.charts.is_empty() {
self.viewport_visible.clear();
// The cover is not optional here: a detached window has `Root=NoFill` and no own
// pass, so without it the white window backing shows through. Only the mark on it
// follows the switch, and both come from one builder so that cannot be got wrong.
Expand Down Expand Up @@ -1193,6 +1276,12 @@ impl Render for AddChartStack {
.layout_orientation
.unwrap_or(StackOrientation::Vertical)
.is_horizontal();
// `render_chart_stack` builds a `MoonVirtualList` only for vertical scroll outside
// COMPRESS. Horizontal scroll and FIT/COMPRESS mount every nonvacated child and never
// report a range, so those routes are the geometry the next reveal is allowed to restore.
if !(scroll && !compress && !horizontal) {
self.remember_mounted_children();
}
let entity = cx.entity();
let p = palette;
let title_size = crate::design::t_body(cx);
Expand Down
53 changes: 50 additions & 3 deletions crates/moon-ui-gpui/src/chart_tabs/main_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ pub(crate) struct MainChartStack {
layout_min_slot: Option<u16>,
/// Size the stack was last painted at, written by the render probe. See `AddChartStack`.
measured: Rc<Cell<Size<Pixels>>>,
/// Whether the host presenting this stack is on screen.
///
/// The dock writes it through [`Self::set_scene_visible`]. Paint stores `true` because a
/// rendered stack is present. Prune and virtual-list callbacks AND this flag, so a hidden
/// host cannot be switched back on by a local visibility update.
host_visible: bool,
scroll: MoonVirtualListScrollHandle,
}

Expand Down Expand Up @@ -246,6 +252,7 @@ impl MainChartStack {
layout_columns_exact: None,
layout_min_slot: None,
measured: Rc::new(Cell::new(Size::default())),
host_visible: true,
scroll: MoonVirtualListScrollHandle::new(),
};
if let Some((core, market)) = focus_open {
Expand Down Expand Up @@ -1202,7 +1209,24 @@ impl MainChartStack {
.update(cx, |panel, pcx| panel.debug_fill_history_to_capacity(pcx))
}

/// Record whether the host is showing this stack, and propagate a real change.
///
/// An unchanged value returns immediately. Child prune and layout already update local
/// visibility, and repeating the hide or reveal on every backend observation would reset
/// virtual-list children.
///
/// Args:
/// visible: Whether the host currently presents this stack.
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing; an unchanged host flag leaves child visibility untouched. A reveal reuses
/// [`Self::sync_visibility`]. A hide forces every child off and clears stack-scroll mode.
pub(super) fn set_scene_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
if self.host_visible == visible {
return;
}
self.host_visible = visible;
if visible {
self.sync_visibility(cx);
} else {
Expand All @@ -1215,12 +1239,20 @@ impl MainChartStack {
}
}

/// Push fullscreen or stack visibility down to every child, gated by the host.
///
/// Args:
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing. A hidden host forces every child off.
fn sync_visibility(&mut self, cx: &mut Context<Self>) {
for (ix, entry) in self.charts.iter().enumerate() {
// In fullscreen, only the active chart is visible. In stack mode, visible tiles set
// themselves visible in `ChartPanel::render`; offscreen virtual-list entries remain
// hidden and do not run preparation work.
let visible = !self.show_stack && Some(ix) == self.active;
// hidden and do not run preparation work. `host_visible` keeps a hidden dock or an
// inactive Main tab from being turned back on by prune, open, or close.
let visible = self.host_visible && !self.show_stack && Some(ix) == self.active;
let stack_scroll = self.show_stack;
entry.panel.update(cx, |panel, _| {
panel.set_main_stack_scroll(stack_scroll);
Expand All @@ -1229,12 +1261,21 @@ impl MainChartStack {
}
}

/// Apply one virtual-list window to stack-mode children, gated by the host.
///
/// Args:
/// range: Display indexes the virtual list currently paints.
/// cx: Stack context used to update child panels.
///
/// Returns:
/// Nothing. Fullscreen ignores the window. A hidden host forces every child in the
/// window off.
fn sync_stack_visible_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
if !self.show_stack {
return;
}
for (ix, entry) in self.charts.iter().enumerate() {
let visible = range.contains(&ix);
let visible = self.host_visible && range.contains(&ix);
entry.panel.update(cx, |panel, _| {
panel.set_main_stack_scroll(true);
panel.set_scene_visible(visible);
Expand Down Expand Up @@ -1619,7 +1660,13 @@ mod tests;
impl Render for MainChartStack {
/// Renders the per-chart tab row above either the active full-bleed chart or the virtualized
/// whole-stack layout.
///
/// Paint stores `host_visible` because a rendered stack is on screen. Child wake stays on the
/// visible-range path; `set_scene_visible` is not called from here.
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Paint means this host is on screen. Store the flag only: `set_scene_visible` would mark
// every offscreen child visible.
self.host_visible = true;
crate::diag::bump(&crate::diag::MAIN_STACK_RENDER);
let _render_us = crate::diag::scope(&crate::diag::MAIN_STACK_RENDER_US);
let palette = moon_ui::MoonPalette::active(cx);
Expand Down
62 changes: 48 additions & 14 deletions crates/moon-ui-gpui/src/chart_tabs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ pub struct ChartTabs {
detached: Vec<(u32, ChartBucket, Entity<AddChartStack>)>,
/// Active tab.
active: Tab,
/// Whether this Charts panel is the front dock tab.
///
/// Separate from the inner-tab flag above. `Panel::set_active` is the dock host's visibility
/// hook; keyboard focus is not occlusion. Starts shown, so the frame before the dock announces
/// a change still prepares the active chart. Detached windows are not gated by this flag.
host_visible: bool,
/// Number of markets already seen while each `(number, bucket)` tab was active.
/// The badge is `pane_count - seen`; active tabs catch up and hide it, while inactive tabs
/// retain their seen count so new detects increase the badge.
Expand Down Expand Up @@ -737,6 +743,7 @@ impl ChartTabs {
custom_gate_gen: HashMap::new(),
detached: Vec::new(),
active: Tab::Main,
host_visible: true,
seen: HashMap::new(),
add_seq: HashMap::new(),
last_sig: initial_sig,
Expand Down Expand Up @@ -1147,25 +1154,30 @@ impl ChartTabs {
chart_pane_label(&self.backend, &self.group, n, bucket, cx)
}

/// Mark inactive tabs invisible because they are absent from the current GPUI scene and must
/// not run CPU preparation from chart-data observation. Active or detached panels mark
/// themselves visible in their own render path.
/// Mark attached stacks that are absent from the current GPUI scene so they do not run CPU
/// preparation from chart-data observation.
///
/// A hidden Charts dock passes `false` to every attached Main, Add, and Custom stack. While
/// the dock is showing, each attached stack follows the active inner tab, including the active
/// Add stack. Detached windows are not in these lists and keep their own visibility.
///
/// Args:
/// cx: Tab context used to update attached stacks.
///
/// Returns:
/// Nothing; `detached` is not visited.
fn sync_inactive_chart_visibility(&self, cx: &mut Context<Self>) {
let active = self.active.clone();
if matches!(active, Tab::Main) {
self.main
.update(cx, |panel, pcx| panel.set_scene_visible(true, pcx));
} else {
self.main
.update(cx, |panel, pcx| panel.set_scene_visible(false, pcx));
}
let host_visible = self.host_visible;
self.main.update(cx, |panel, pcx| {
panel.set_scene_visible(host_visible && matches!(active, Tab::Main), pcx);
});
for (n, c, panel) in &self.add {
if Tab::Add(*n, c.clone()) != active {
panel.update(cx, |panel, pcx| panel.set_scene_visible(false, pcx));
}
let visible = host_visible && Tab::Add(*n, c.clone()) == active;
panel.update(cx, |panel, pcx| panel.set_scene_visible(visible, pcx));
}
for (n, c, panel) in &self.custom {
let visible = Tab::Custom(*n, c.clone()) == active;
let visible = host_visible && Tab::Custom(*n, c.clone()) == active;
panel.update(cx, |panel, pcx| panel.set_scene_visible(visible, pcx));
}
}
Expand Down Expand Up @@ -1347,6 +1359,28 @@ impl Panel for ChartTabs {
fn background_policy(&self, _cx: &App) -> MoonBackgroundPolicy {
MoonBackgroundPolicy::NoFill
}

/// Propagate dock-tab visibility into the attached chart stacks.
///
/// `active` is whether this Charts panel is the front dock tab. An unchanged value returns
/// without notifying, persisting, switching the inner tab, or touching detached windows.
/// Keyboard focus is not consulted: a window can stay focused while this dock tab sits behind
/// another.
///
/// Args:
/// active: Whether the dock host is showing this panel.
/// _window: Unused. The dock passes the window that rendered the tab group.
/// cx: Panel context used to update attached stacks on a real change.
///
/// Returns:
/// Nothing.
fn set_active(&mut self, active: bool, _window: &mut Window, cx: &mut Context<Self>) {
if self.host_visible == active {
return;
}
self.host_visible = active;
self.sync_inactive_chart_visibility(cx);
}
}

/// Build an AddToChart label for both the tab strip and detached-window title.
Expand Down
22 changes: 22 additions & 0 deletions crates/moon-ui-gpui/src/chartdx/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,28 @@ impl PlatformLayers {
}
}

/// Whether appending these rows can change a cached combo bitmap.
///
/// DX11 asks the combo layer, which knows both bake spans. Every other
/// backend reports damage, so it keeps the previous unconditional repaint.
///
/// Args:
/// data: Rows about to be appended.
///
/// Returns:
/// `false` only when DX11 can prove neither cached span is touched.
pub fn combo_append_touches_cached_span(&self, data: &[ChartCross]) -> bool {
#[cfg(windows)]
{
self.combo.append_touches_cached_span(data)
}
#[cfg(not(windows))]
{
let _ = data;
true
}
}

/// Fully replaces the layer's candle set from the whole composed list.
pub fn set_candles(&mut self, data: &[CandleGpu]) {
#[cfg(windows)]
Expand Down
Loading
Loading