feat(widgets): right-click context-menu events for timeline, node graph, explorer

TimelineView emits ContextMenuRequested with hit target (clip/empty/
track head/ruler marker), NodeGraphView emits it for nodes and the
background, and ProjectExplorer emits it for entries and blank areas,
so the app can open context menus aligned with the C++ version.
This commit is contained in:
2026-08-18 17:14:37 +08:00
parent 6f75d3c92a
commit 050d5ba22d
3 changed files with 220 additions and 10 deletions
+50 -2
View File
@@ -9,8 +9,8 @@
use gpui::{
App, ClickEvent, Context, ElementId, Entity, EventEmitter, ExternalPaths, FocusHandle,
Focusable, Hsla, Point, Pixels, Render, SharedString, Window, colors::DefaultColors, div,
hsla, img, prelude::*, px,
Focusable, Hsla, MouseButton, MouseDownEvent, Point, Pixels, Render, SharedString, Window,
colors::DefaultColors, div, hsla, img, prelude::*, px,
};
use std::collections::HashSet;
use std::path::PathBuf;
@@ -130,6 +130,18 @@ pub enum ProjectExplorerEvent {
/// The new view mode.
view: ExplorerView,
},
/// An entry (or the empty area) was right-clicked. The widget selects
/// the entry but opens no menu itself; the host assembles and shows the
/// context menu at `position`.
ContextMenuRequested {
/// The explorer's stable id.
control: usize,
/// The right-clicked entry, or `None` when the empty area was
/// clicked.
id: Option<u64>,
/// Mouse position in window coordinates.
position: Point<Pixels>,
},
}
/// Flatten a tree into visible rows, honoring the `expanded` set.
@@ -236,6 +248,20 @@ impl<D: ProjectDataSource> ProjectExplorer<D> {
cx.notify();
}
}
/// Reports a right-click: selects the entry (if any) and emits
/// [`ProjectExplorerEvent::ContextMenuRequested`] for the host's menu.
fn context_menu(&mut self, id: Option<u64>, position: Point<Pixels>, cx: &mut Context<Self>) {
if let Some(id) = id {
self.selected = Some(id);
}
cx.emit(ProjectExplorerEvent::ContextMenuRequested {
control: self.control,
id,
position,
});
cx.notify();
}
}
impl<D: ProjectDataSource> EventEmitter<ProjectExplorerEvent> for ProjectExplorer<D> {}
@@ -338,6 +364,13 @@ impl<D: ProjectDataSource> Render for ProjectExplorer<D> {
}
cx.notify();
}))
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
this.context_menu(Some(entry_id), event.position, cx);
cx.stop_propagation();
}),
)
.child(div().w(px(14.0)).child(if entry.is_dir {
if this_expanded(&expanded, entry.id) {
"▾"
@@ -425,6 +458,13 @@ impl<D: ProjectDataSource> Render for ProjectExplorer<D> {
}
cx.notify();
}))
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
this.context_menu(Some(entry_id), event.position, cx);
cx.stop_propagation();
}),
)
.child(if let Some(thumbnail) = entry.thumbnail.clone() {
// A filesystem path: load through the path resource so
// generated thumbnails resolve without an asset source.
@@ -475,6 +515,14 @@ impl<D: ProjectDataSource> Render for ProjectExplorer<D> {
cx.notify();
}),
)
// A right-click that bubbles up to the container hit no entry
// (entry handlers stop propagation): the empty-area menu.
.on_mouse_down(
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, _window, cx| {
this.context_menu(None, event.position, cx);
}),
)
.child(toolbar)
.child(content.flex_1())
}