P5 follow-ups: double-tap priority, inspector dismiss, breadcrumb path
- Mobile double-tap is now a single tool-independent priority chain: object → enter (movie clip or group); empty inside a clip → exit one level; empty at root → zoom-to-fit. Desktop keeps its own select-tool double-click (gated). - Inspector "tap outside to dismiss" now only HIDES the sheet (with a dismissed flag reset on selection change) instead of clearing the selection — the earlier clear clobbered the selection before menu/omnibutton actions ran, breaking Group/Convert/Cut/Copy. Delete also clears focus so it dismisses the inspector. - Breadcrumb shows the full editing path (Scene 1 › … › current) via a new SharedPaneState.editing_clip_path / EditingContext::clip_path(); each ancestor crumb jumps to that level (pending_exit_to_depth → EditingContext::exit_to_depth). - Remove the "Vello Stage" debug overlay; move the breadcrumb up to the top-left.
This commit is contained in:
parent
01b20165cc
commit
4f58edf436
|
|
@ -942,6 +942,21 @@ impl EditingContext {
|
||||||
self.stack.pop()
|
self.stack.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The clip_id path from outermost to the current level (for breadcrumbs).
|
||||||
|
fn clip_path(&self) -> Vec<Uuid> {
|
||||||
|
self.stack.iter().map(|e| e.clip_id).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop down to `depth` entries, returning the entry at `depth` (whose saved state should be
|
||||||
|
/// restored). Returns None if already at or above that depth.
|
||||||
|
fn exit_to_depth(&mut self, depth: usize) -> Option<EditingContextEntry> {
|
||||||
|
if depth >= self.stack.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let restore = self.stack[depth].clone();
|
||||||
|
self.stack.truncate(depth);
|
||||||
|
Some(restore)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EditorApp {
|
struct EditorApp {
|
||||||
|
|
@ -6926,6 +6941,15 @@ impl eframe::App for EditorApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Breadcrumb jump: exit up to a specific depth (restoring that level's saved context).
|
||||||
|
if let Some(depth) = scratch.pending_exit_to_depth.take() {
|
||||||
|
if let Some(entry) = self.editing_context.exit_to_depth(depth) {
|
||||||
|
self.selection.clear();
|
||||||
|
self.active_layer_id = entry.saved_active_layer_id;
|
||||||
|
self.playback_time = entry.saved_playback_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set cursor based on hover state
|
// Set cursor based on hover state
|
||||||
if let Some((_, is_horizontal)) = self.hovered_divider {
|
if let Some((_, is_horizontal)) = self.hovered_divider {
|
||||||
if is_horizontal {
|
if is_horizontal {
|
||||||
|
|
@ -7157,6 +7181,7 @@ struct FrameScratch {
|
||||||
pending_handlers: Vec<panes::ViewActionHandler>,
|
pending_handlers: Vec<panes::ViewActionHandler>,
|
||||||
pending_enter_clip: Option<(Uuid, Uuid, Uuid)>,
|
pending_enter_clip: Option<(Uuid, Uuid, Uuid)>,
|
||||||
pending_exit_clip: bool,
|
pending_exit_clip: bool,
|
||||||
|
pending_exit_to_depth: Option<usize>,
|
||||||
pending_actions: Vec<Box<dyn lightningbeam_core::action::Action>>,
|
pending_actions: Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||||
pending_menu_actions: Vec<MenuAction>,
|
pending_menu_actions: Vec<MenuAction>,
|
||||||
effect_thumbnail_requests: Vec<Uuid>,
|
effect_thumbnail_requests: Vec<Uuid>,
|
||||||
|
|
@ -7221,8 +7246,10 @@ impl EditorApp {
|
||||||
editing_clip_id: self.editing_context.current_clip_id(),
|
editing_clip_id: self.editing_context.current_clip_id(),
|
||||||
editing_instance_id: self.editing_context.current_instance_id(),
|
editing_instance_id: self.editing_context.current_instance_id(),
|
||||||
editing_parent_layer_id: self.editing_context.current_parent_layer_id(),
|
editing_parent_layer_id: self.editing_context.current_parent_layer_id(),
|
||||||
|
editing_clip_path: self.editing_context.clip_path(),
|
||||||
pending_enter_clip: &mut scratch.pending_enter_clip,
|
pending_enter_clip: &mut scratch.pending_enter_clip,
|
||||||
pending_exit_clip: &mut scratch.pending_exit_clip,
|
pending_exit_clip: &mut scratch.pending_exit_clip,
|
||||||
|
pending_exit_to_depth: &mut scratch.pending_exit_to_depth,
|
||||||
active_layer_id: &mut self.active_layer_id,
|
active_layer_id: &mut self.active_layer_id,
|
||||||
tool_state: &mut self.tool_state,
|
tool_state: &mut self.tool_state,
|
||||||
pending_actions: &mut scratch.pending_actions,
|
pending_actions: &mut scratch.pending_actions,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,22 @@ pub fn is_active(shared: &SharedPaneState) -> bool {
|
||||||
!shared.focus.is_none() || !shared.selection.is_empty()
|
!shared.focus.is_none() || !shared.selection.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cheap content-sensitive signature of the current selection, so the shell can detect when the
|
||||||
|
/// selection *changes* (to re-show a manually-dismissed inspector for the new thing).
|
||||||
|
pub fn selection_sig(shared: &SharedPaneState) -> u64 {
|
||||||
|
let mut h: u64 = 0;
|
||||||
|
for id in shared.selection.clip_instances() {
|
||||||
|
h ^= (id.as_u128() as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||||
|
}
|
||||||
|
for f in shared.selection.selected_fills() {
|
||||||
|
h ^= (f.idx() as u64).wrapping_mul(0xD1B54A32D192ED03);
|
||||||
|
}
|
||||||
|
for e in shared.selection.selected_edges() {
|
||||||
|
h ^= (e.idx() as u64).wrapping_mul(0xA24BAED4963EE407);
|
||||||
|
}
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
/// The stack slot (see `super::STACK`) where the current selection lives, so we can tell if the
|
/// The stack slot (see `super::STACK`) where the current selection lives, so we can tell if the
|
||||||
/// sheet would cover it. Geometry/selection lives on the Stage; clips/layers on the Timeline; etc.
|
/// sheet would cover it. Geometry/selection lives on the Stage; clips/layers on the Timeline; etc.
|
||||||
#[allow(dead_code)] // selection→pane mapping; kept for reflow/jump heuristics
|
#[allow(dead_code)] // selection→pane mapping; kept for reflow/jump heuristics
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,11 @@ pub struct MobileState {
|
||||||
/// Screen-y of the tap that opened the inspector — used to decide whether the tapped thing would
|
/// Screen-y of the tap that opened the inspector — used to decide whether the tapped thing would
|
||||||
/// be hidden by the sheet (only then do we reflow the stack).
|
/// be hidden by the sheet (only then do we reflow the stack).
|
||||||
pub inspector_anchor_y: f32,
|
pub inspector_anchor_y: f32,
|
||||||
|
/// Set when the user taps outside the sheet to dismiss it; suppresses re-showing until the
|
||||||
|
/// selection changes. Reset when the selection signature below changes.
|
||||||
|
pub inspector_dismissed: bool,
|
||||||
|
/// Cheap signature of the current selection, to detect when it changes (re-show the inspector).
|
||||||
|
pub inspector_sel_sig: u64,
|
||||||
/// Whether the omnibutton radial tool menu is open.
|
/// Whether the omnibutton radial tool menu is open.
|
||||||
pub omni_open: bool,
|
pub omni_open: bool,
|
||||||
/// Whether the omnibutton "more" grid (all tools) is open.
|
/// Whether the omnibutton "more" grid (all tools) is open.
|
||||||
|
|
@ -225,6 +230,8 @@ impl Default for MobileState {
|
||||||
inspector_frac: 0.45,
|
inspector_frac: 0.45,
|
||||||
inspector_visible: false,
|
inspector_visible: false,
|
||||||
inspector_anchor_y: 0.0,
|
inspector_anchor_y: 0.0,
|
||||||
|
inspector_dismissed: false,
|
||||||
|
inspector_sel_sig: 0,
|
||||||
omni_open: false,
|
omni_open: false,
|
||||||
omni_grid_open: false,
|
omni_grid_open: false,
|
||||||
omni_create_open: false,
|
omni_create_open: false,
|
||||||
|
|
@ -269,9 +276,16 @@ pub fn render_mobile_shell(
|
||||||
// interacted with (pointer down but still selected).
|
// interacted with (pointer down but still selected).
|
||||||
let active = inspector::is_active(&rc.shared);
|
let active = inspector::is_active(&rc.shared);
|
||||||
let any_down = ui.input(|i| i.pointer.any_down());
|
let any_down = ui.input(|i| i.pointer.any_down());
|
||||||
|
let sel_sig = inspector::selection_sig(&rc.shared);
|
||||||
|
if sel_sig != state.inspector_sel_sig {
|
||||||
|
// Selection changed → a fresh thing to inspect; clear any prior manual dismissal.
|
||||||
|
state.inspector_dismissed = false;
|
||||||
|
state.inspector_sel_sig = sel_sig;
|
||||||
|
}
|
||||||
if !active {
|
if !active {
|
||||||
state.inspector_visible = false;
|
state.inspector_visible = false;
|
||||||
} else if !any_down {
|
state.inspector_dismissed = false;
|
||||||
|
} else if !state.inspector_dismissed && !any_down {
|
||||||
if !state.inspector_visible {
|
if !state.inspector_visible {
|
||||||
// Just opened (tap released): anchor to the release position for the reflow test below.
|
// Just opened (tap released): anchor to the release position for the reflow test below.
|
||||||
state.inspector_anchor_y = ui
|
state.inspector_anchor_y = ui
|
||||||
|
|
@ -289,8 +303,9 @@ pub fn render_mobile_shell(
|
||||||
};
|
};
|
||||||
let mut sheet_top = region.bottom() - sheet_h;
|
let mut sheet_top = region.bottom() - sheet_h;
|
||||||
|
|
||||||
// Tapping outside the sheet (but not on the transport) dismisses the inspector. The press still
|
// Tapping outside the sheet (but not on the transport) dismisses (hides) the inspector. We only
|
||||||
// reaches the panes below via stack::render, so tapping another object reselects and reopens it.
|
// hide it — NOT clear the selection — so actions dispatched from overlays (context menu, "+New"
|
||||||
|
// grid) still see the selection. It stays dismissed until the selection changes (sig above).
|
||||||
if inspector_shown {
|
if inspector_shown {
|
||||||
let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max);
|
let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max);
|
||||||
let dismiss = ui.input(|i| {
|
let dismiss = ui.input(|i| {
|
||||||
|
|
@ -300,9 +315,8 @@ pub fn render_mobile_shell(
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if dismiss {
|
if dismiss {
|
||||||
rc.shared.selection.clear();
|
|
||||||
*rc.shared.focus = lightningbeam_core::selection::FocusSelection::None;
|
|
||||||
state.inspector_visible = false;
|
state.inspector_visible = false;
|
||||||
|
state.inspector_dismissed = true;
|
||||||
inspector_shown = false;
|
inspector_shown = false;
|
||||||
sheet_top = region.bottom();
|
sheet_top = region.bottom();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,10 +212,15 @@ pub struct SharedPaneState<'a> {
|
||||||
pub editing_instance_id: Option<uuid::Uuid>,
|
pub editing_instance_id: Option<uuid::Uuid>,
|
||||||
/// The parent layer ID containing the clip instance being edited
|
/// The parent layer ID containing the clip instance being edited
|
||||||
pub editing_parent_layer_id: Option<uuid::Uuid>,
|
pub editing_parent_layer_id: Option<uuid::Uuid>,
|
||||||
|
/// The full clip_id path being edited, outermost → current (for breadcrumbs). Empty at root.
|
||||||
|
pub editing_clip_path: Vec<uuid::Uuid>,
|
||||||
/// Request to enter a movie clip for editing: (clip_id, instance_id, parent_layer_id)
|
/// Request to enter a movie clip for editing: (clip_id, instance_id, parent_layer_id)
|
||||||
pub pending_enter_clip: &'a mut Option<(uuid::Uuid, uuid::Uuid, uuid::Uuid)>,
|
pub pending_enter_clip: &'a mut Option<(uuid::Uuid, uuid::Uuid, uuid::Uuid)>,
|
||||||
/// Request to exit the current movie clip
|
/// Request to exit the current movie clip
|
||||||
pub pending_exit_clip: &'a mut bool,
|
pub pending_exit_clip: &'a mut bool,
|
||||||
|
/// Request to exit up to a specific editing depth (number of clips to keep); e.g. a breadcrumb
|
||||||
|
/// click. `Some(0)` exits all the way to the document root.
|
||||||
|
pub pending_exit_to_depth: &'a mut Option<usize>,
|
||||||
/// Currently active layer ID
|
/// Currently active layer ID
|
||||||
pub active_layer_id: &'a mut Option<uuid::Uuid>,
|
pub active_layer_id: &'a mut Option<uuid::Uuid>,
|
||||||
/// Current tool interaction state (mutable for tools to modify)
|
/// Current tool interaction state (mutable for tools to modify)
|
||||||
|
|
|
||||||
|
|
@ -4009,8 +4009,9 @@ impl StagePane {
|
||||||
// geometry selection/editing there; clip-instance interaction stays available.
|
// geometry selection/editing there; clip-instance interaction stays available.
|
||||||
let inbetween = vector_layer.is_tween_inbetween(*shared.playback_time);
|
let inbetween = vector_layer.is_tween_inbetween(*shared.playback_time);
|
||||||
|
|
||||||
// Double-click: enter/exit movie clip editing
|
// Double-click: enter/exit movie clip editing (desktop; mobile handles double-tap as a
|
||||||
if response.double_clicked() {
|
// single tool-independent gesture in handle_input).
|
||||||
|
if !shared.is_mobile && response.double_clicked() {
|
||||||
// Hit test clip instances at the click position
|
// Hit test clip instances at the click position
|
||||||
let document = shared.action_executor.document();
|
let document = shared.action_executor.document();
|
||||||
let clip_hit = hit_test::hit_test_clip_instances(
|
let clip_hit = hit_test::hit_test_clip_instances(
|
||||||
|
|
@ -11445,6 +11446,36 @@ impl StagePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P5 gesture (mobile): double-tap semantics, in priority order and regardless of the active
|
||||||
|
// tool. (double_clicked() fires on double-tap and, on desktop, double-click — so testable.)
|
||||||
|
// 1. object under the tap → enter it (movie clip or group — go a level deeper)
|
||||||
|
// 2. empty, inside a clip → exit one level
|
||||||
|
// 3. empty, at the top level → zoom to fit
|
||||||
|
// The desktop equivalent lives in handle_select_tool (gated to !is_mobile below).
|
||||||
|
if shared.is_mobile && response.double_clicked() {
|
||||||
|
use lightningbeam_core::layer::AnyLayer;
|
||||||
|
use vello::kurbo::{Affine, Point};
|
||||||
|
let point = Point::new(world_pos.x as f64, world_pos.y as f64);
|
||||||
|
let hit = (*shared.active_layer_id).and_then(|layer_id| {
|
||||||
|
let document = shared.action_executor.document();
|
||||||
|
if let Some(AnyLayer::Vector(vl)) = document.get_layer(&layer_id) {
|
||||||
|
lightningbeam_core::hit_test::hit_test_clip_instances(
|
||||||
|
&vl.clip_instances, document, point, Affine::IDENTITY, *shared.playback_time,
|
||||||
|
)
|
||||||
|
.and_then(|iid| vl.clip_instances.iter().find(|c| c.id == iid).map(|c| (c.clip_id, iid, layer_id)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some((clip_id, instance_id, layer_id)) = hit {
|
||||||
|
*shared.pending_enter_clip = Some((clip_id, instance_id, layer_id));
|
||||||
|
} else if shared.editing_clip_id.is_some() {
|
||||||
|
*shared.pending_exit_clip = true;
|
||||||
|
} else {
|
||||||
|
self.zoom_to_fit(shared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// P5 gesture (mobile only): long-press on an object opens its actions menu. In the egui fork
|
// P5 gesture (mobile only): long-press on an object opens its actions menu. In the egui fork
|
||||||
// `secondary_clicked()` fires on long-press (and right-click) and `context_menu()` opens on
|
// `secondary_clicked()` fires on long-press (and right-click) and `context_menu()` opens on
|
||||||
// the same — so the menu must be attached UNCONDITIONALLY for egui to register it. Desktop
|
// the same — so the menu must be attached UNCONDITIONALLY for egui to register it. Desktop
|
||||||
|
|
@ -12865,72 +12896,72 @@ impl PaneRenderer for StagePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show camera info overlay
|
// Render breadcrumb navigation showing the full editing path (root → … → current clip).
|
||||||
let info_color = shared.theme.text_color(&["#stage", ".text-secondary"], ui.ctx(), egui::Color32::from_gray(200));
|
if !shared.editing_clip_path.is_empty() {
|
||||||
ui.painter().text(
|
let font = egui::FontId::proportional(13.0);
|
||||||
rect.min + egui::vec2(10.0, 10.0),
|
let sep = " > ";
|
||||||
egui::Align2::LEFT_TOP,
|
// Segments: "Scene 1" (root) + each clip name. `target` = depth to exit to when clicked
|
||||||
format!("Vello Stage (zoom: {:.2}, pan: {:.0},{:.0})",
|
// (root → 0; clip at path index k → k+1); the current (last) level isn't clickable.
|
||||||
self.zoom, self.pan_offset.x, self.pan_offset.y),
|
let segments: Vec<(String, Option<usize>)> = {
|
||||||
egui::FontId::proportional(14.0),
|
|
||||||
info_color,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Render breadcrumb navigation when inside a movie clip
|
|
||||||
if shared.editing_clip_id.is_some() {
|
|
||||||
let document = shared.action_executor.document();
|
let document = shared.action_executor.document();
|
||||||
// Build breadcrumb names from the editing context
|
let n = shared.editing_clip_path.len();
|
||||||
// We only have the current clip_id, so show "Scene 1 > ClipName"
|
let mut segs: Vec<(String, Option<usize>)> = vec![("Scene 1".to_string(), Some(0))];
|
||||||
let clip_name = shared.editing_clip_id
|
for (k, cid) in shared.editing_clip_path.iter().enumerate() {
|
||||||
.and_then(|id| document.get_vector_clip(&id))
|
let name = document
|
||||||
|
.get_vector_clip(cid)
|
||||||
.map(|c| c.name.clone())
|
.map(|c| c.name.clone())
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
segs.push((name, if k + 1 == n { None } else { Some(k + 1) }));
|
||||||
let breadcrumb_y = rect.min.y + 30.0;
|
|
||||||
let breadcrumb_x = rect.min.x + 10.0;
|
|
||||||
|
|
||||||
// Background pill
|
|
||||||
let scene_text = "Scene 1";
|
|
||||||
let separator = " > ";
|
|
||||||
let full_text = format!("{}{}{}", scene_text, separator, clip_name);
|
|
||||||
let font = egui::FontId::proportional(13.0);
|
|
||||||
let galley = ui.painter().layout_no_wrap(full_text.clone(), font.clone(), egui::Color32::WHITE);
|
|
||||||
let text_rect = egui::Rect::from_min_size(
|
|
||||||
egui::pos2(breadcrumb_x, breadcrumb_y),
|
|
||||||
galley.size() + egui::vec2(16.0, 8.0),
|
|
||||||
);
|
|
||||||
ui.painter().rect_filled(
|
|
||||||
text_rect,
|
|
||||||
4.0,
|
|
||||||
egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180),
|
|
||||||
);
|
|
||||||
|
|
||||||
// "Scene 1" as clickable (exit clip)
|
|
||||||
let scene_galley = ui.painter().layout_no_wrap(
|
|
||||||
scene_text.to_string(), font.clone(), egui::Color32::from_rgb(120, 180, 255),
|
|
||||||
);
|
|
||||||
let scene_rect = egui::Rect::from_min_size(
|
|
||||||
egui::pos2(breadcrumb_x + 8.0, breadcrumb_y + 4.0),
|
|
||||||
scene_galley.size(),
|
|
||||||
);
|
|
||||||
let scene_response = ui.allocate_rect(scene_rect, egui::Sense::click());
|
|
||||||
ui.painter().galley(scene_rect.min, scene_galley, egui::Color32::WHITE);
|
|
||||||
if scene_response.clicked() {
|
|
||||||
*shared.pending_exit_clip = true;
|
|
||||||
}
|
}
|
||||||
if scene_response.hovered() {
|
segs
|
||||||
|
};
|
||||||
|
|
||||||
|
let bx = rect.min.x + 10.0;
|
||||||
|
let by = rect.min.y + 10.0;
|
||||||
|
|
||||||
|
// Background pill sized to the whole path.
|
||||||
|
let full: String = segments
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (s, _))| if i == 0 { s.clone() } else { format!("{sep}{s}") })
|
||||||
|
.collect();
|
||||||
|
let full_galley = ui.painter().layout_no_wrap(full, font.clone(), egui::Color32::WHITE);
|
||||||
|
let pill = egui::Rect::from_min_size(egui::pos2(bx, by), full_galley.size() + egui::vec2(16.0, 8.0));
|
||||||
|
ui.painter().rect_filled(pill, 4.0, egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180));
|
||||||
|
|
||||||
|
// Draw segments left → right; clickable ancestors in blue.
|
||||||
|
let mut x = bx + 8.0;
|
||||||
|
let y = by + 4.0;
|
||||||
|
let mut clicked_target: Option<usize> = None;
|
||||||
|
for (i, (name, target)) in segments.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
let sg = ui.painter().layout_no_wrap(sep.to_string(), font.clone(), egui::Color32::from_gray(160));
|
||||||
|
let sw = sg.size().x;
|
||||||
|
ui.painter().galley(egui::pos2(x, y), sg, egui::Color32::from_gray(160));
|
||||||
|
x += sw;
|
||||||
|
}
|
||||||
|
let color = if target.is_some() {
|
||||||
|
egui::Color32::from_rgb(120, 180, 255)
|
||||||
|
} else {
|
||||||
|
egui::Color32::WHITE
|
||||||
|
};
|
||||||
|
let g = ui.painter().layout_no_wrap(name.clone(), font.clone(), color);
|
||||||
|
let seg_rect = egui::Rect::from_min_size(egui::pos2(x, y), g.size());
|
||||||
|
if let Some(depth) = target {
|
||||||
|
let resp = ui.allocate_rect(seg_rect, egui::Sense::click());
|
||||||
|
if resp.clicked() {
|
||||||
|
clicked_target = Some(*depth);
|
||||||
|
}
|
||||||
|
if resp.hovered() {
|
||||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Separator + clip name (not clickable, it's the current level)
|
ui.painter().galley(seg_rect.min, g, color);
|
||||||
let rest_text = format!("{}{}", separator, clip_name);
|
x += seg_rect.width();
|
||||||
ui.painter().text(
|
}
|
||||||
egui::pos2(scene_rect.max.x, breadcrumb_y + 4.0),
|
if let Some(depth) = clicked_target {
|
||||||
egui::Align2::LEFT_TOP,
|
*shared.pending_exit_to_depth = Some(depth);
|
||||||
rest_text,
|
}
|
||||||
font,
|
|
||||||
egui::Color32::WHITE,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render vector editing overlays (vertices, control points, etc.)
|
// Render vector editing overlays (vertices, control points, etc.)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue