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
51 changes: 7 additions & 44 deletions crates/moon-ui-gpui/src/chartdx/data_state/market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,53 +248,16 @@ impl ChartDataState {
pr.gpu_prepare_dirty = true;
pixels_changed = true;
}
let (axis_pos, price_axis_w, glass_w, chart_w) = horizontal_chart_layout(
rect.w,
let areas = pane_layout(
*rect,
self.orderbook_only,
self.orderbook_enabled,
self.time_axis_visible,
self.price_axis_pos,
self.last_ppp,
);
// A hidden time axis reserves no label gutter, allowing the plot to use the full height.
let time_axis_h = if self.time_axis_visible {
moon_chart::TIME_AXIS_H * self.last_ppp
} else {
0.0
};
let plot_h = (rect.h - time_axis_h).max(1.0);
// Left places the axis gutter on the left, shifts the plot right, and keeps the book at
// the right edge. Right starts the plot at the left edge, then places the book and the
// axis gutter to its right. Hide removes the axis, starts the plot at the left edge,
// and keeps the book at the right edge.
let axis_on_left = matches!(
axis_pos,
crate::persistence::chart_persist::PriceAxisPos::Left
);
let chart_x = if axis_on_left {
rect.x + price_axis_w
} else {
rect.x
};
let glass_x = if matches!(
axis_pos,
crate::persistence::chart_persist::PriceAxisPos::Right
) {
chart_x + chart_w
} else {
rect.x + (rect.w - glass_w).max(1.0)
};
let chart_area = Rect {
x: chart_x,
y: rect.y,
w: chart_w,
h: plot_h,
};
let glass_area = Rect {
x: glass_x,
y: rect.y,
w: glass_w,
h: plot_h,
};
let (chart_area, glass_area) = (areas.plot, areas.glass);
let plot_h = chart_area.h;
pane.view
.ensure_default_window(chart_area.w, self.present_rate_hz, self.default_x_ppm);
// A framing request asked for outside a prepared frame lands HERE, at the first width
Expand Down Expand Up @@ -907,7 +870,7 @@ impl ChartDataState {
let mut next_view =
view::view_gpu(&pane.view, area_win, res, self.last_ppp, view_style);
next_view.pad = view_time0
+ (chart_area.w + glass_w)
+ (chart_area.w + glass_area.w)
/ pane.view.px_per_ms.max(moon_chart::view::MIN_PX_PER_MS);
if pr.view != next_view {
pr.view = next_view;
Expand Down Expand Up @@ -1062,7 +1025,7 @@ impl ChartDataState {
// Order-book-only mode forces the book on even when the Order Book toggle is cleared.
pr.orderbook_only = self.orderbook_only;
// Store the effective axis position, including forced hiding in book-only mode, for labels.
pr.price_axis_pos = axis_pos;
pr.price_axis_pos = areas.axis_pos;
pr.time_axis_visible = self.time_axis_visible;
pr.prospective_usd = self.prospective_usd;
let orderbook_on = self.orderbook_enabled || self.orderbook_only;
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-ui-gpui/src/chartdx/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,8 +1108,8 @@ impl ChartEngine {
///
/// The interval is REQUESTED rather than applied: the plot width is knowable only inside a
/// prepared frame, and this method is reached from application code that commonly runs before
/// the first present. It used to measure the width itself, through `pane_rects` and
/// `horizontal_chart_layout`, both of which floor an unpresented slot at ONE PIXEL rather than
/// the first present. It used to measure the width itself, through `pane_rects` and the shared
/// pane layout, both of which floor an unpresented slot at ONE PIXEL rather than
/// reporting that they do not know - so the interval was framed for a one-pixel plot and the
/// visible span at the real width came out wider by the ratio of the two, drawing correct axes
/// over an empty plot. The view now holds the request until a real width exists, and re-applies
Expand Down
59 changes: 36 additions & 23 deletions crates/moon-ui-gpui/src/chartdx/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use crate::chartdx::pane::Container;
use moon_chart::paint::now_unix_ms;
use moon_chart::view::{ChartView, Rect};
use moon_chart::{GLASS_ZONE_PX, PRICE_AXIS_W};
use moon_core::session::CoreId;

/// Mouse button subset used by chart navigation instead of `winit::MouseButton`.
Expand Down Expand Up @@ -37,11 +36,22 @@ pub struct ChartInput {
pub hovered_pane: Option<usize>,
/// Pane layout from the previous render, in device pixels, used for input hit testing.
pub pane_rects: Vec<(usize, Rect)>,
/// Price-axis position published during render.
/// Price-axis position published during render, as CONFIGURED — book-only broom mode hides it,
/// and that is resolved by the shared layout, not here.
///
/// This controls plot width and the left offset used to calculate `cursor_x`; its default is
/// `Left`, matching the panel default.
pub price_axis_pos: crate::persistence::chart_persist::PriceAxisPos,
/// Whether this panel draws only its order book, published during render. Broom mode gives the
/// book the whole pane and floors the plot at one pixel, which navigation has to know: the
/// width it pans and zooms against is the drawn one or none of it agrees.
pub orderbook_only: bool,
/// Whether this panel's order book is drawn at all, published during render. With it off the
/// book's width goes back to the plot, so navigation must not keep subtracting it.
pub orderbook_enabled: bool,
/// Whether the time axis reserves its gutter, published during render. Navigation reads only
/// horizontal extents, but the layout it shares with the engine answers for both.
pub time_axis_visible: bool,
/// Market queued by an eligible chart double-click for the caller to take and open on Main.
pub pending_to_main: Option<(CoreId, String)>,

Expand All @@ -63,6 +73,20 @@ pub struct ChartInput {
}

impl ChartInput {
/// This pane's areas as the ENGINE lays them out. Navigation asks the same function `prepare`
/// does, or it pans and zooms against a width nothing drew — a cramped pane's narrowed book, a
/// disabled book's width handed back to the plot, a broom pane's plot floored at one pixel.
fn areas_of(&self, rect: &Rect, ppp: f32) -> crate::chartdx::PaneAreas {
crate::chartdx::pane_layout(
*rect,
self.orderbook_only,
self.orderbook_enabled,
self.time_axis_visible,
self.price_axis_pos,
ppp,
)
}

fn plot_metrics_for(&self, pane: Option<usize>, fallback_w: f32, ppp: f32) -> (f32, f32) {
let Some(idx) = pane else {
return (
Expand All @@ -76,22 +100,9 @@ impl ChartInput {
self.last_ptr.0.clamp(0.0, fallback_w.max(1.0)),
);
};
use crate::persistence::chart_persist::PriceAxisPos;
let price_axis_w = if matches!(self.price_axis_pos, PriceAxisPos::Hide) {
0.0
} else {
PRICE_AXIS_W * ppp
};
let glass_w = GLASS_ZONE_PX.min(r.w * 0.5);
let plot_w = (r.w - price_axis_w - glass_w).max(1.0);
// Reserve a left offset only for a left-side axis; right-side or hidden axes start at the slot edge.
let left_off = if matches!(self.price_axis_pos, PriceAxisPos::Left) {
price_axis_w
} else {
0.0
};
let cursor_x = (self.last_ptr.0 - r.x - left_off).clamp(0.0, plot_w);
(plot_w, cursor_x)
let plot = self.areas_of(r, ppp).plot;
let cursor_x = (self.last_ptr.0 - plot.x).clamp(0.0, plot.w);
(plot.w, cursor_x)
}

/// Return the hovered pane's mutable view for pan or zoom operations.
Expand All @@ -111,14 +122,16 @@ impl ChartInput {
}

/// Queue the hovered pane's market after a chart-area double-click.
fn try_dblclick_to_main(&mut self, container: &Container) {
fn try_dblclick_to_main(&mut self, container: &Container, ppp: f32) {
let Some(idx) = self.hovered_pane else { return };
let Some((_, r)) = self.pane_rects.iter().find(|(i, _)| *i == idx) else {
return;
};
// Ignore double-clicks in the right-side order-book/glass zone.
let glass_w = GLASS_ZONE_PX.min(r.w * 0.5);
if self.last_ptr.0 >= r.x + r.w - glass_w {
// Ignore double-clicks in the order book's own RECTANGLE, wherever the engine put it: left
// of an outboard right-side axis gutter, narrowed on a cramped pane, the whole pane in
// broom mode — where this gesture therefore has nowhere left to fire, which is the intent.
let glass = self.areas_of(r, ppp).glass;
if glass.w > 0.0 && self.last_ptr.0 >= glass.x {
return;
}
self.pending_to_main = container.target(idx);
Expand Down Expand Up @@ -209,7 +222,7 @@ impl ChartInput {
self.last_lmb_ms = now;
self.last_lmb_pos = (px, py);
if dbl && allow_dbl_to_main {
self.try_dblclick_to_main(container);
self.try_dblclick_to_main(container, ppp);
}
self.lmb_down = true;
self.lmb_x_active = false;
Expand Down
95 changes: 72 additions & 23 deletions crates/moon-ui-gpui/src/chartdx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ mod text;
/// The caption editor formats its sample line with the chart's OWN formatter, never a second
/// spelling of it.
pub(crate) use text::preview_row;
#[cfg(test)]
mod tests;
pub mod types;
#[cfg(windows)]
pub mod userdata;
Expand Down Expand Up @@ -1090,56 +1092,103 @@ impl ChartDataHandle {
}
}

/// Resolve the horizontal plot and order-book widths shared by data preparation and navigation.
/// Where a pane's plot and order book sit, in device pixels.
///
/// Both rectangles together, because they answer one question: a caller handed only widths has to
/// place them itself, which is how three copies of the placement came to exist. The engine draws
/// these rectangles and the panel hit-tests them, so what is DRAWN and what is CLICKABLE are the
/// same arithmetic or they drift — book-only broom mode, where the book takes the whole pane, is
/// the case that punished the drift hardest.
#[derive(Clone, Copy)]
pub(crate) struct PaneAreas {
/// Effective axis position: broom mode hides the price axis whatever the tab configured.
pub axis_pos: crate::persistence::chart_persist::PriceAxisPos,
/// The plot area. Its width is floored at one pixel, so an unpresented slot and a broom pane
/// both report a plot that exists but holds nothing.
pub plot: Rect,
/// The order book's area, `w == 0.0` when no book is drawn.
pub glass: Rect,
}

/// Lay one pane out into its plot and order-book areas.
///
/// Args:
/// rect_w: Full pane width in device pixels.
/// orderbook_only: Whether the plot collapses behind the order book.
/// orderbook_enabled: Whether the normal order-book zone is visible.
/// rect: The pane's full rectangle in device pixels.
/// orderbook_only: Whether the plot collapses behind the order book (broom mode).
/// orderbook_enabled: Whether the ordinary order-book zone is drawn.
/// time_axis_visible: Whether the time axis reserves its gutter under both areas.
/// price_axis_pos: Configured per-tab price-axis position.
/// pixel_scale: Device pixels per logical pixel.
///
/// Returns:
/// Effective axis position, axis width, order-book width, and plot width.
fn horizontal_chart_layout(
rect_w: f32,
/// The pane's [`PaneAreas`].
pub(crate) fn pane_layout(
rect: Rect,
orderbook_only: bool,
orderbook_enabled: bool,
time_axis_visible: bool,
price_axis_pos: crate::persistence::chart_persist::PriceAxisPos,
pixel_scale: f32,
) -> (
crate::persistence::chart_persist::PriceAxisPos,
f32,
f32,
f32,
) {
) -> PaneAreas {
use crate::persistence::chart_persist::PriceAxisPos;
let axis_pos = if orderbook_only {
crate::persistence::chart_persist::PriceAxisPos::Hide
PriceAxisPos::Hide
} else {
price_axis_pos
};
let price_axis_w = if matches!(
axis_pos,
crate::persistence::chart_persist::PriceAxisPos::Hide
) {
let price_axis_w = if matches!(axis_pos, PriceAxisPos::Hide) {
0.0
} else {
moon_chart::PRICE_AXIS_W * pixel_scale
};
let glass_cap = rect_w * 0.5;
let glass_cap = rect.w * 0.5;
let glass_base = moon_chart::GLASS_ZONE_PX.min(glass_cap);
let chart_w_base = rect_w - price_axis_w - glass_base;
let chart_w_base = rect.w - price_axis_w - glass_base;
let glass_w = if orderbook_only {
(rect_w - price_axis_w).max(1.0)
(rect.w - price_axis_w).max(1.0)
} else if !orderbook_enabled {
0.0
} else if chart_w_base < glass_base * 2.0 {
(moon_chart::GLASS_ZONE_PX * 0.8).min(glass_cap)
} else {
glass_base
};
let chart_w = (rect_w - price_axis_w - glass_w).max(1.0);
(axis_pos, price_axis_w, glass_w, chart_w)
let chart_w = (rect.w - price_axis_w - glass_w).max(1.0);
// Left puts the axis gutter on the left and shifts the plot right; Right and Hide start the
// plot at the pane's edge. The book follows the plot only for a right-side axis, which leaves
// that gutter outboard of it; otherwise it sits against the pane's right edge.
let chart_x = if matches!(axis_pos, PriceAxisPos::Left) {
rect.x + price_axis_w
} else {
rect.x
};
let glass_x = if matches!(axis_pos, PriceAxisPos::Right) {
chart_x + chart_w
} else {
rect.x + (rect.w - glass_w).max(0.0)
};
// A hidden time axis reserves no label gutter, letting both areas use the full height.
let time_axis_h = if time_axis_visible {
moon_chart::TIME_AXIS_H * pixel_scale
} else {
0.0
};
let h = (rect.h - time_axis_h).max(1.0);
PaneAreas {
axis_pos,
plot: Rect {
x: chart_x,
y: rect.y,
w: chart_w,
h,
},
glass: Rect {
x: glass_x,
y: rect.y,
w: glass_w,
h,
},
}
}

struct ChartDataState {
Expand Down
Loading