180 lines
6.9 KiB
Rust
180 lines
6.9 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
App, Bounds, Div, Entity, InteractiveElement, MouseButton, MouseDownEvent, MouseMoveEvent,
|
|
MouseUpEvent, Pixels, Point, ScrollWheelEvent, Stateful, StatefulInteractiveElement, Styled,
|
|
Subscription, Window,
|
|
};
|
|
|
|
use super::{
|
|
market::ChartSync,
|
|
stage::Visible,
|
|
tools::{Drawing, Tool},
|
|
};
|
|
use crate::charts::{Linear, Rect};
|
|
|
|
/// A drawing in progress: its tool, and where the drag began and is now, each a candle and a price.
|
|
pub(crate) type Sketch = (Tool, (f64, f64), (f64, f64));
|
|
|
|
/// A market chart's own state: its box, what it shows and where the pointer is when no sync holds them, how many candles it last saw, and a drag in progress.
|
|
#[derive(Default)]
|
|
pub(crate) struct Stage {
|
|
pub bounds: Bounds<Pixels>,
|
|
pub visible: Option<Visible>,
|
|
pub hover: Option<i64>,
|
|
pub pointer: Option<(f32, f32)>,
|
|
pub seen: usize,
|
|
pub drag: Option<(f32, Visible)>,
|
|
pub sketch: Option<Sketch>,
|
|
pub _synced: Option<Subscription>,
|
|
}
|
|
|
|
pub(crate) type OnDraw = Rc<dyn Fn(Drawing, &mut Window, &mut App)>;
|
|
|
|
/// What the pointer moves on a market chart: where it keeps what shows, the candles in view out of `total`, the price pane and its scale, and the drawing tool.
|
|
pub(crate) struct Steering {
|
|
pub stage: Entity<Stage>,
|
|
pub sync: Option<Entity<ChartSync>>,
|
|
pub visible: Visible,
|
|
pub total: usize,
|
|
pub main: Rect,
|
|
pub price: Linear,
|
|
pub tool: Option<Tool>,
|
|
pub on_draw: Option<OnDraw>,
|
|
}
|
|
|
|
/// Keeps what shows and the candle under the pointer in the sync when one is shared, or in the chart's own stage.
|
|
fn hold(
|
|
stage: &Entity<Stage>,
|
|
sync: &Option<Entity<ChartSync>>,
|
|
cx: &mut App,
|
|
visible: Option<Visible>,
|
|
hover: Option<i64>,
|
|
) {
|
|
match sync {
|
|
Some(sync) => sync.update(cx, |sync, cx| {
|
|
(sync.visible, sync.hover) = (visible, hover);
|
|
cx.notify();
|
|
}),
|
|
None => stage.update(cx, |stage, cx| {
|
|
(stage.visible, stage.hover) = (visible, hover);
|
|
cx.notify();
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// A window position in the chart box's own pixels.
|
|
fn local(position: Point<Pixels>, bounds: Bounds<Pixels>) -> (f32, f32) {
|
|
let offset = position - bounds.origin;
|
|
(f32::from(offset.x), f32::from(offset.y))
|
|
}
|
|
|
|
impl Steering {
|
|
/// Drag or scroll sideways to pan, hold Cmd and scroll to zoom, double-press for the newest, and drag with a tool to draw.
|
|
pub(crate) fn attach(self, root: Stateful<Div>) -> Stateful<Div> {
|
|
let Steering {
|
|
stage,
|
|
sync,
|
|
visible,
|
|
total,
|
|
main,
|
|
price,
|
|
tool,
|
|
on_draw,
|
|
} = self;
|
|
let (moved, pressed, released, wheeled, left) = (
|
|
stage.clone(),
|
|
stage.clone(),
|
|
stage.clone(),
|
|
stage.clone(),
|
|
stage,
|
|
);
|
|
let (sync_move, sync_press, sync_wheel, sync_leave) =
|
|
(sync.clone(), sync.clone(), sync.clone(), sync);
|
|
let across = (main.x, main.w);
|
|
let pinned = move |(x, y): (f32, f32)| {
|
|
(
|
|
visible.start + f64::from((x - across.0) / across.1) * visible.count - 0.5,
|
|
price.value(y),
|
|
)
|
|
};
|
|
root.cursor_crosshair()
|
|
.on_mouse_move(move |event: &MouseMoveEvent, _, cx| {
|
|
let place = local(event.position, moved.read(cx).bounds);
|
|
let pressed_left = event.pressed_button == Some(MouseButton::Left);
|
|
let drag = moved.read(cx).drag.filter(|_| pressed_left);
|
|
moved.update(cx, |stage, _| {
|
|
stage.pointer = Some(place);
|
|
if let Some(sketch) = stage.sketch.as_mut().filter(|_| pressed_left) {
|
|
sketch.2 = pinned(place);
|
|
}
|
|
});
|
|
let (visible, hover) = match drag {
|
|
Some((from, start)) => (
|
|
start.pan(f64::from((from - place.0) / start.slot(across.1)), total),
|
|
None,
|
|
),
|
|
None => (
|
|
visible,
|
|
(place.0 >= across.0 && place.0 <= across.0 + across.1)
|
|
.then(|| visible.at(place.0, across)),
|
|
),
|
|
};
|
|
hold(&moved, &sync_move, cx, Some(visible), hover);
|
|
})
|
|
.on_mouse_down(MouseButton::Left, move |event: &MouseDownEvent, _, cx| {
|
|
let place = local(event.position, pressed.read(cx).bounds);
|
|
if event.click_count == 2 {
|
|
log::info!("market chart: back to the newest");
|
|
hold(&pressed, &sync_press, cx, None, None);
|
|
return;
|
|
}
|
|
pressed.update(cx, |stage, cx| match tool {
|
|
Some(tool) if main.contains(place) => {
|
|
stage.sketch = Some((tool, pinned(place), pinned(place)));
|
|
cx.notify();
|
|
}
|
|
Some(_) => {}
|
|
None => stage.drag = Some((place.0, visible)),
|
|
});
|
|
})
|
|
.on_mouse_up(MouseButton::Left, move |_: &MouseUpEvent, window, cx| {
|
|
let sketch = released.update(cx, |stage, cx| {
|
|
stage.drag = None;
|
|
cx.notify();
|
|
stage.sketch.take()
|
|
});
|
|
if let (Some((tool, from, to)), Some(on_draw)) = (sketch, &on_draw) {
|
|
let drawn = Drawing::of(tool, from, to);
|
|
log::info!("market chart: drew {drawn:?}");
|
|
on_draw(drawn, window, cx);
|
|
}
|
|
})
|
|
.on_scroll_wheel(move |event: &ScrollWheelEvent, _, cx| {
|
|
let delta = event
|
|
.delta
|
|
.pixel_delta(Pixels::from(across.1 / visible.count as f32));
|
|
let (dx, dy) = (f32::from(delta.x), f32::from(delta.y));
|
|
let place = local(event.position, wheeled.read(cx).bounds);
|
|
let next = if event.modifiers.platform && dy.abs() > dx.abs() {
|
|
let share = f64::from(((place.0 - across.0) / across.1).clamp(0.0, 1.0));
|
|
visible.zoom((-f64::from(dy) * 0.004).exp(), share, total)
|
|
} else if dx.abs() > dy.abs() {
|
|
visible.pan(-f64::from(dx / visible.slot(across.1)), total)
|
|
} else {
|
|
return;
|
|
};
|
|
cx.stop_propagation();
|
|
hold(&wheeled, &sync_wheel, cx, Some(next), None);
|
|
})
|
|
.on_hover(move |inside, _, cx| {
|
|
if !*inside {
|
|
left.update(cx, |stage, _| {
|
|
(stage.pointer, stage.drag, stage.sketch) = (None, None, None)
|
|
});
|
|
hold(&left, &sync_leave, cx, Some(visible), None);
|
|
}
|
|
})
|
|
}
|
|
}
|