use std::cell::Cell;
use std::rc::Rc;
use gpui_kit::base::dock::{Panel as BasePanel, PanelEvent};
use gpui_kit::component::dock::{Panel, PanelControl};
use gpui_kit::component::menu::{PopupMenu, PopupMenuItem};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::TestSupportExt as _;
use gpui_kit::{
canvas, div, point, px, relative, rgb, size, App, Bounds, Context, Corners, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, InteractiveElement as _,
IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, ScrollEvent, SharedString, StatefulInteractiveElement as _,
Styled as _, Subscription, WeakEntity, Window,
};
use crate::navigation::actions::*;
use crate::ui::plot::canvas::Pixel;
use crate::screens::explore::trace::renderer::TimeViewKind;
use crate::screens::explore::{ExploreVm, TraceVm};
pub const GUTTER: f32 = 64.;
pub const AXIS: f32 = 22.;
pub fn color(p: Pixel) -> gpui_kit::Hsla {
rgb(((p.r as u32) << 16) | ((p.g as u32) << 8) | p.b as u32).into()
}
#[derive(Debug, Clone, Copy)]
struct Drag {
x: f32,
start: f64,
fy: f32,
moved: bool,
}
const CLICK_SLOP: f32 = 3.0;
pub struct TracePanel {
vm: Entity<TraceVm>,
explore: WeakEntity<ExploreVm>,
focus: FocusHandle,
plot: Rc<Cell<Bounds<Pixels>>>,
drag: Option<Drag>,
wheel: f32,
pointer: Option<(f32, f32)>,
_vm: Subscription,
}
impl TracePanel {
pub fn new(vm: Entity<TraceVm>, explore: WeakEntity<ExploreVm>, cx: &mut Context<Self>) -> Self {
let sub = cx.observe(&vm, |_, _, cx| cx.notify());
Self { vm, explore, focus: cx.focus_handle(), plot: Rc::new(Cell::new(Bounds::default())), drag: None, wheel: 0.0, pointer: None, _vm: sub }
}
fn fractions(&self, p: gpui_kit::Point<Pixels>) -> (f32, f32) {
let b = self.plot.get();
let (w, h) = (b.size.width.as_f32().max(1.0), b.size.height.as_f32().max(1.0));
(((p.x - b.origin.x).as_f32() / w).clamp(0.0, 1.0), ((p.y - b.origin.y).as_f32() / h).clamp(0.0, 1.0))
}
fn store_update(&self, cx: &mut Context<Self>, f: impl FnOnce(&mut crate::data::store::Store, &mut Context<crate::data::store::Store>)) {
let store = self.vm.read(cx).store().clone();
store.update(cx, f);
}
fn on_down(&mut self, e: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
window.focus(&self.focus, cx);
self.vm.update(cx, |vm, cx| vm.pressed(cx));
let (fx, fy) = self.fractions(e.position);
if e.click_count == 2 && self.vm.read(cx).view.kind == TimeViewKind::Heatmap {
if let (Some(ch), Some(explore)) = (self.vm.read(cx).channel_at(fy), self.explore.upgrade()) {
let vm = self.vm.clone();
explore.update(cx, |ex, cx| ex.reveal_in_traces(&vm, ch, cx));
}
return;
}
let _ = fx;
let start = self.vm.read(cx).store().read(cx).timeline.window_start_sec;
self.drag = Some(Drag { x: e.position.x.as_f32(), start, fy, moved: false });
}
fn on_move(&mut self, e: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
let (fx, fy) = self.fractions(e.position);
match self.drag {
Some(d) if e.pressed_button == Some(MouseButton::Left) => {
let w = self.plot.get().size.width.as_f32().max(1.0) as f64;
let dx = (e.position.x.as_f32() - d.x) as f64;
if dx.abs() as f32 > CLICK_SLOP {
if let Some(drag) = &mut self.drag {
drag.moved = true;
}
}
self.store_update(cx, |s, cx| {
let span = s.timeline.visible_window_sec;
let start = d.start - dx / w * span;
s.set_window(start, start + span, cx);
});
}
Some(_) => self.drag = None,
None => {}
}
if self.plot.get().contains(&e.position) {
self.pointer = Some((fx, fy));
self.vm.update(cx, |vm, cx| vm.hover_at(fx, fy, cx));
cx.notify();
}
}
fn on_up(&mut self, _: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
if let Some(d) = self.drag.take() {
if !d.moved {
if let Some(ch) = self.vm.read(cx).channel_at(d.fy) {
self.store_update(cx, |s, cx| s.pick_channel(Some(ch), cx));
}
}
}
}
fn on_wheel(&mut self, e: &ScrollEvent, _: &mut Window, cx: &mut Context<Self>) {
let d = e.delta.pixel_delta(px(20.));
let (dx, dy) = (d.x.as_f32(), d.y.as_f32());
let (fx, _) = self.fractions(e.position);
let w = self.plot.get().size.width.as_f32().max(1.0) as f64;
let m = e.modifiers;
if m.control || m.platform {
self.store_update(cx, |s, cx| s.zoom_at((dy as f64 * 0.004).exp(), fx as f64, cx));
} else if m.alt {
self.vm.update(cx, |vm, cx| vm.zoom_gain((-dy * 0.004).exp(), cx));
} else if dx != 0.0 || m.shift {
let moved = if dx != 0.0 { dx } else { dy } as f64;
self.store_update(cx, |s, cx| {
let dt = -moved / w * s.timeline.visible_window_sec;
s.pan_time(dt, cx);
});
} else {
self.wheel += dy;
let steps = (self.wheel / 20.0).trunc();
if steps != 0.0 {
self.wheel -= steps * 20.0;
self.vm.update(cx, |vm, cx| vm.scroll_by(-(steps as i64), cx));
}
}
cx.stop_propagation();
}
fn overlays(&self, cx: &mut Context<Self>) -> Vec<gpui_kit::AnyElement> {
let vm = self.vm.read(cx);
let palette = vm.store().read(cx).palette();
let tl = &vm.store().read(cx).timeline;
let ink = |a: f32| if palette.dark { gpui_kit::hsla(0., 0., 1., a) } else { gpui_kit::hsla(0., 0., 0., a) };
let mut out = Vec::new();
let t = (tl.current_time_sec - tl.window_start_sec) / tl.visible_window_sec.max(1e-12);
if (0.0..=1.0).contains(&t) {
out.push(div().absolute().top_0().bottom_0().left(relative(t as f32)).w(px(1.5)).bg(color(palette.playhead)).into_any_element());
}
if let Some((fx, _)) = self.pointer {
out.push(div().absolute().top_0().bottom_0().left(relative(fx)).w(px(1.)).bg(ink(0.22)).into_any_element());
}
let label = vm.view.scale_bar_label();
if !label.is_empty() {
let y = vm.view.scale_bar_center_frac();
out.push(
div()
.absolute()
.right(px(18.))
.top(relative(y))
.mt(px(-7.))
.text_xs()
.text_color(color(palette.text))
.child(label)
.into_any_element(),
);
}
out
}
}
impl Render for TracePanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let vm = self.vm.read(cx);
let view = &vm.view;
let tl = vm.store().read(cx).timeline.clone();
let heatmap = view.kind == TimeViewKind::Heatmap;
let hovered_lane = self.pointer.map(|(_, fy)| (fy * view.drawn_channels().len() as f32) as usize);
let palette = vm.store().read(cx).palette();
let lanes = view.lane_labels(&palette);
let ticks = view.time_ticks(&tl);
let shown = vm.shown.clone();
let (canvas_w, canvas_h, canvas_scale) = (view.canvas_width, view.canvas_height, view.scale_factor);
let empty = view.selection.is_empty();
let view_id = view.id as usize;
let muted = color(palette.text);
let _ = window;
let gutter = div().w(px(GUTTER)).h_full().relative().overflow_hidden().children(lanes.into_iter().enumerate().map(|(i, l)| {
let strong = !heatmap && hovered_lane == Some(i);
let nudge = if l.y_frac <= 0.04 { 0. } else if l.y_frac >= 0.96 { -16. } else { -8. };
div()
.absolute()
.left(px(10.))
.top(relative(l.y_frac))
.mt(px(nudge))
.text_xs()
.text_color(color(l.color))
.when(strong, |d| d.font_weight(FontWeight::BOLD))
.child(SharedString::from(l.label))
}));
let plot_cell = self.plot.clone();
let vm_handle = self.vm.downgrade();
let raster = canvas(
move |bounds, window, cx| {
plot_cell.set(bounds);
let scale = window.scale_factor();
let (w, h) = ((bounds.size.width.as_f32() * scale).round() as u32, (bounds.size.height.as_f32() * scale).round() as u32);
if (w, h, scale) != (canvas_w, canvas_h, canvas_scale) && w > 0 && h > 0 {
let vm = vm_handle.clone();
cx.defer(move |cx| {
let _ = vm.update(cx, |vm, cx| vm.set_plot_size(w, h, scale, cx));
});
}
},
move |bounds, _, window, _| {
let Some(s) = shown else { return };
let span = tl.visible_window_sec.max(1e-12);
let x = bounds.origin.x + bounds.size.width * (((s.start - tl.window_start_sec) / span) as f32);
let w = bounds.size.width * ((s.window / span) as f32);
let image_bounds = Bounds { origin: point(x, bounds.origin.y), size: size(w, bounds.size.height) };
let _ = window.paint_image(bounds, image_bounds, Corners::default(), s.image, 0, false);
},
)
.absolute()
.size_full();
let plot = div()
.id("plot")
.flex_1()
.h_full()
.relative()
.overflow_hidden()
.cursor(if self.drag.is_some() { CursorStyle::ClosedHand } else { CursorStyle::Crosshair })
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_down))
.on_mouse_move(cx.listener(Self::on_move))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_up))
.on_scroll(cx.listener(Self::on_wheel))
.on_hover(cx.listener(|this, hovered: &bool, _, cx| {
if !hovered {
this.pointer = None;
this.vm.update(cx, |vm, cx| vm.hover_left(cx));
}
}))
.child(raster)
.children(self.overlays(cx))
.when(empty, |d| {
d.child(div().absolute().size_full().flex().items_center().justify_center().text_sm().text_color(muted).child("No channels selected: pick some in Channels"))
});
let readout = (!self.vm.read(cx).hover.is_empty() && self.pointer.is_some()).then(|| {
div().absolute().right_0().top_0().h_full().pl_3().pr_1().flex().items_center().bg(color(palette.background)).text_xs().text_color(color(palette.text)).child(self.vm.read(cx).hover.clone())
});
let axis = div().h(px(AXIS)).flex().child(div().w(px(GUTTER))).child(
div()
.flex_1()
.h_full()
.relative()
.children(ticks.into_iter().map(|t| {
let tick = div().absolute().left(relative(t.frac)).top(px(3.)).w(px(48.)).flex().text_xs().text_color(muted).child(t.label);
if t.frac < 0.04 {
tick.justify_start()
} else if t.frac > 0.96 {
tick.ml(px(-48.)).justify_end()
} else {
tick.ml(px(-24.)).justify_center()
}
}))
.children(readout),
);
div()
.id(("time-view", view_id))
.test_support()
.key_context(PLOT)
.track_focus(&self.focus)
.size_full()
.flex()
.flex_col()
.bg(color(palette.background))
.on_action(cx.listener(|this, _: &PlayPause, _, cx| this.store_update(cx, |s, cx| s.toggle_play(cx))))
.on_action(cx.listener(|this, _: &PanBack, _, cx| this.store_update(cx, |s, cx| s.pan_fraction(-0.1, cx))))
.on_action(cx.listener(|this, _: &PanForward, _, cx| this.store_update(cx, |s, cx| s.pan_fraction(0.1, cx))))
.on_action(cx.listener(|this, _: &PageBack, _, cx| this.store_update(cx, |s, cx| s.pan_fraction(-1.0, cx))))
.on_action(cx.listener(|this, _: &PageForward, _, cx| this.store_update(cx, |s, cx| s.pan_fraction(1.0, cx))))
.on_action(cx.listener(|this, _: &ZoomIn, _, cx| this.store_update(cx, |s, cx| s.zoom_at(0.8, 0.5, cx))))
.on_action(cx.listener(|this, _: &ZoomOut, _, cx| this.store_update(cx, |s, cx| s.zoom_at(1.25, 0.5, cx))))
.on_action(cx.listener(|this, _: &JumpStart, _, cx| this.store_update(cx, |s, cx| s.jump_to_start(cx))))
.on_action(cx.listener(|this, _: &JumpEnd, _, cx| this.store_update(cx, |s, cx| s.jump_to_end(cx))))
.on_action(cx.listener(|this, _: &ToggleLoop, _, cx| this.store_update(cx, |s, cx| s.toggle_loop(cx))))
.on_action(cx.listener(|this, _: &ChannelUp, _, cx| this.vm.update(cx, |vm, cx| vm.scroll_by(-1, cx))))
.on_action(cx.listener(|this, _: &ChannelDown, _, cx| this.vm.update(cx, |vm, cx| vm.scroll_by(1, cx))))
.on_action(cx.listener(|this, _: &ChannelPageUp, _, cx| this.vm.update(cx, |vm, cx| vm.page(false, cx))))
.on_action(cx.listener(|this, _: &ChannelPageDown, _, cx| this.vm.update(cx, |vm, cx| vm.page(true, cx))))
.on_action(cx.listener(|this, _: &GainUp, _, cx| this.vm.update(cx, |vm, cx| vm.zoom_gain(1.0 / 0.75, cx))))
.on_action(cx.listener(|this, _: &GainDown, _, cx| this.vm.update(cx, |vm, cx| vm.zoom_gain(0.75, cx))))
.child(div().flex_1().min_h_0().flex().child(gutter).child(plot))
.child(axis)
}
}
impl EventEmitter<PanelEvent> for TracePanel {}
impl Focusable for TracePanel {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus.clone()
}
}
impl BasePanel for TracePanel {
fn panel_name(&self) -> &'static str {
"dspk-app.time-view"
}
fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
self.vm.update(cx, |vm, cx| {
let shown_now = active && !vm.is_active();
vm.set_active(active, cx);
if shown_now {
vm.pressed(cx);
}
});
}
fn on_removed(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let id = self.vm.read(cx).id();
if let Some(explore) = self.explore.upgrade() {
explore.update(cx, |ex, cx| ex.removed(id, cx));
}
}
}
impl Panel for TracePanel {
fn title(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = &self.vm.read(cx).view;
let full = SharedString::from(view.title.clone());
div().id(("view-title", view.id as usize)).max_w(px(280.)).truncate().child(full.clone()).tooltip(move |window, cx| gpui_kit::component::tooltip::Tooltip::new(full.clone()).build(window, cx))
}
fn zoom_control(&self, _: &App) -> Option<PanelControl> {
Some(PanelControl::Toolbar)
}
fn dropdown_menu(&mut self, menu: PopupMenu, _: &mut Window, cx: &mut Context<Self>) -> PopupMenu {
let explore = self.explore.clone();
let add = move |kind: TimeViewKind| {
let explore = explore.clone();
move |_: &gpui_kit::ClickEvent, _: &mut Window, cx: &mut App| {
if let Some(ex) = explore.upgrade() {
ex.update(cx, |ex, cx| {
ex.add(kind, cx);
});
}
}
};
let (explore, id) = (self.explore.clone(), self.vm.read(cx).id());
menu.item(PopupMenuItem::new("Add traces view").on_click(add(TimeViewKind::Traces)))
.item(PopupMenuItem::new("Add heatmap view").on_click(add(TimeViewKind::Heatmap)))
.item({
let vm = self.vm.clone();
PopupMenuItem::new("Copy with shown channels").on_click(move |_, _, cx| {
let (store, doc_view, source) = {
let v = vm.read(cx);
(v.store().clone(), v.shown_view(v.store().read(cx)).map(|d| d.id.clone()), v.view.source.clone())
};
let Some(view) = doc_view else { return };
store.update(cx, |s, cx| {
let sel = s.selection(&source).to_vec();
let channels = (!sel.is_empty()).then_some(sel);
let _ = s.execute(crate::domain::command::SessionCommand::CopyView { view, channels, name: None }, cx);
});
if let Some(copy) = store.read(cx).doc.active_view.clone() {
vm.update(cx, |vm, cx| {
vm.view.doc_view = copy;
cx.notify();
});
}
})
})
.separator()
.item(PopupMenuItem::new("Close view").on_click(move |_, _, cx| {
if let Some(ex) = explore.upgrade() {
ex.update(cx, |ex, cx| ex.close(id, cx));
}
}))
}
fn inner_padding(&self, _: &App) -> bool {
false
}
}