diff --git a/examples/animations/src/main.rs b/examples/animations/src/main.rs index 3a4a7083..044fa73c 100644 --- a/examples/animations/src/main.rs +++ b/examples/animations/src/main.rs @@ -15,17 +15,14 @@ fn app_view() -> impl View { stack({ (label(|| "Hover or click me!") - .on_click(move |_| { + .on_click_stop(move |_| { set_counter.update(|value| *value += 1.0); - true }) - .on_event(EventListener::PointerEnter, move |_| { + .on_event_stop(EventListener::PointerEnter, move |_| { set_is_hovered.update(|val| *val = true); - true }) - .on_event(EventListener::PointerLeave, move |_| { + .on_event_stop(EventListener::PointerLeave, move |_| { set_is_hovered.update(|val| *val = false); - true }) .style(|s| { s.border(1.0) diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index f3046cbf..f5ee8aca 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -23,18 +23,16 @@ fn app_view() -> impl View { .hover(|s| s.background(Color::LIGHT_GREEN)) .active(|s| s.color(Color::WHITE).background(Color::DARK_GREEN)) }) - .on_click({ + .on_click_stop({ move |_| { set_counter.update(|value| *value += 1); - true } }) .keyboard_navigatable(), text("Decrement") - .on_click({ + .on_click_stop({ move |_| { set_counter.update(|value| *value -= 1); - true } }) .style(|s| { @@ -49,10 +47,9 @@ fn app_view() -> impl View { }) .keyboard_navigatable(), text("Reset to 0") - .on_click(move |_| { + .on_click_stop(move |_| { println!("Reset counter pressed"); // will not fire if button is disabled set_counter.update(|value| *value = 0); - true }) .disabled(move || counter.get() == 0) .style(|s| { @@ -77,13 +74,12 @@ fn app_view() -> impl View { }); let id = view.id(); - view.on_event(EventListener::KeyUp, move |e| { + view.on_event_stop(EventListener::KeyUp, move |e| { if let Event::KeyUp(e) = e { if e.key.logical_key == Key::Named(NamedKey::F11) { id.inspect(); } } - true }) } diff --git a/examples/themes/src/main.rs b/examples/themes/src/main.rs index 2d3d008b..12df7b0a 100644 --- a/examples/themes/src/main.rs +++ b/examples/themes/src/main.rs @@ -94,10 +94,9 @@ fn app_view() -> impl View { let view = stack((stack(( text("Toggle Theme") .class(Button) - .on_click({ + .on_click_stop({ move |_| { set_theme.update(|theme| *theme = !*theme); - true } }) .keyboard_navigatable(), @@ -105,28 +104,25 @@ fn app_view() -> impl View { label(move || format!("Value: {}", counter.get())).class(Label), text("Increment") .class(Button) - .on_click({ + .on_click_stop({ move |_| { set_counter.update(|value| *value += 1); - true } }) .keyboard_navigatable(), text("Decrement") .class(Button) - .on_click({ + .on_click_stop({ move |_| { set_counter.update(|value| *value -= 1); - true } }) .keyboard_navigatable(), text("Reset to 0") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { println!("Reset counter pressed"); // will not fire if button is disabled set_counter.update(|value| *value = 0); - true }) .disabled(move || counter.get() == 0) .keyboard_navigatable(), @@ -150,13 +146,12 @@ fn app_view() -> impl View { .window_title(|| "Themes Example".to_string()); let id = view.id(); - view.on_event(EventListener::KeyUp, move |e| { + view.on_event_stop(EventListener::KeyUp, move |e| { if let Event::KeyUp(e) = e { if e.key.logical_key == Key::Named(NamedKey::F11) { id.inspect(); } } - true }) } diff --git a/examples/widget-gallery/src/buttons.rs b/examples/widget-gallery/src/buttons.rs index e1ad281a..97fb37fb 100644 --- a/examples/widget-gallery/src/buttons.rs +++ b/examples/widget-gallery/src/buttons.rs @@ -12,16 +12,14 @@ pub fn button_view() -> impl View { form({ ( form_item("Basic Button:".to_string(), 120.0, || { - button(|| "Click me").on_click(|_| { + button(|| "Click me").on_click_stop(|_| { println!("Button clicked"); - true }) }), form_item("Styled Button:".to_string(), 120.0, || { button(|| "Click me") - .on_click(|_| { + .on_click_stop(|_| { println!("Button clicked"); - true }) .style(|s| { s.border(1.0) @@ -37,15 +35,13 @@ pub fn button_view() -> impl View { }) }), form_item("Disabled Button:".to_string(), 120.0, || { - button(|| "Click me").disabled(|| true).on_click(|_| { + button(|| "Click me").disabled(|| true).on_click_stop(|_| { println!("Button clicked"); - true }) }), form_item("Secondary click button:".to_string(), 120.0, || { - button(|| "Right click me").on_secondary_click(|_| { + button(|| "Right click me").on_secondary_click_stop(|_| { println!("Secondary mouse button click."); - true }) }), form_item("Toggle button - Switch:".to_string(), 120.0, || { diff --git a/examples/widget-gallery/src/checkbox.rs b/examples/widget-gallery/src/checkbox.rs index 132be5d9..b85920dc 100644 --- a/examples/widget-gallery/src/checkbox.rs +++ b/examples/widget-gallery/src/checkbox.rs @@ -15,24 +15,21 @@ pub fn checkbox_view() -> impl View { form_item("Checkbox:".to_string(), width, move || { checkbox(is_checked) .style(|s| s.margin(5.0)) - .on_click(move |_| { + .on_click_stop(move |_| { set_is_checked.update(|checked| *checked = !*checked); - true }) }), form_item("Disabled Checkbox:".to_string(), width, move || { checkbox(is_checked) .style(|s| s.margin(5.0)) - .on_click(move |_| { + .on_click_stop(move |_| { set_is_checked.update(|checked| *checked = !*checked); - true }) .disabled(|| true) }), form_item("Labelled Checkbox:".to_string(), width, move || { - labeled_checkbox(is_checked, || "Check me!").on_click(move |_| { + labeled_checkbox(is_checked, || "Check me!").on_click_stop(move |_| { set_is_checked.update(|checked| *checked = !*checked); - true }) }), form_item( @@ -40,9 +37,8 @@ pub fn checkbox_view() -> impl View { width, move || { labeled_checkbox(is_checked, || "Check me!") - .on_click(move |_| { + .on_click_stop(move |_| { set_is_checked.update(|checked| *checked = !*checked); - true }) .disabled(|| true) }, diff --git a/examples/widget-gallery/src/lists.rs b/examples/widget-gallery/src/lists.rs index 3ea910e5..94621322 100644 --- a/examples/widget-gallery/src/lists.rs +++ b/examples/widget-gallery/src/lists.rs @@ -12,6 +12,7 @@ use floem::{ VirtualListItemSize, }, widgets::checkbox, + EventPropagation, }; use crate::form::{form, form_item}; @@ -64,20 +65,18 @@ fn enhanced_list() -> impl View { container({ stack({ ( - checkbox(is_checked).on_click(move |_| { + checkbox(is_checked).on_click_stop(move |_| { set_is_checked.update(|checked: &mut bool| *checked = !*checked); - true }), label(move || item.to_string()) .style(|s| s.height(32.0).font_size(32.0)), container({ label(move || " X ") - .on_click(move |_| { + .on_click_stop(move |_| { print!("Item Removed"); set_long_list.update(|x| { x.remove(index); }); - true }) .style(|s| { s.height(18.0) @@ -99,11 +98,10 @@ fn enhanced_list() -> impl View { }) .style(move |s| s.height(item_height).width(list_width).items_center()) }) - .on_click(move |_| { + .on_click_stop(move |_| { set_selected.update(|v: &mut usize| { *v = long_list.get().iter().position(|it| *it == item).unwrap(); }); - true }) .on_event(EventListener::KeyDown, move |e| { if let Event::KeyDown(key_event) = e { @@ -113,18 +111,18 @@ fn enhanced_list() -> impl View { if sel > 0 { set_selected.update(|v| *v -= 1); } - true + EventPropagation::Stop } Key::Named(NamedKey::ArrowDown) => { if sel < long_list.get().len() - 1 { set_selected.update(|v| *v += 1); } - true + EventPropagation::Stop } - _ => false, + _ => EventPropagation::Continue, } } else { - false + EventPropagation::Continue } }) .keyboard_navigatable() diff --git a/examples/widget-gallery/src/main.rs b/examples/widget-gallery/src/main.rs index 0617ef3d..eb0888bc 100644 --- a/examples/widget-gallery/src/main.rs +++ b/examples/widget-gallery/src/main.rs @@ -21,6 +21,7 @@ use floem::{ Decorators, VirtualListDirection, VirtualListItemSize, }, widgets::button, + EventPropagation, }; fn app_view() -> impl View { @@ -46,7 +47,7 @@ fn app_view() -> impl View { .position(|it| *it == item) .unwrap(); stack((label(move || item).style(|s| s.font_size(18.0)),)) - .on_click(move |_| { + .on_click_stop(move |_| { set_active_tab.update(|v: &mut usize| { *v = tabs .get_untracked() @@ -54,7 +55,6 @@ fn app_view() -> impl View { .position(|it| *it == item) .unwrap(); }); - true }) .on_event(EventListener::KeyDown, move |e| { if let Event::KeyDown(key_event) = e { @@ -65,21 +65,21 @@ fn app_view() -> impl View { if active > 0 { set_active_tab.update(|v| *v -= 1) } - true + EventPropagation::Stop } Key::Named(NamedKey::ArrowDown) => { if active < tabs.get().len() - 1 { set_active_tab.update(|v| *v += 1) } - true + EventPropagation::Stop } - _ => false, + _ => EventPropagation::Continue, } } else { - false + EventPropagation::Continue } } else { - false + EventPropagation::Continue } }) .keyboard_navigatable() @@ -126,9 +126,8 @@ fn app_view() -> impl View { let id = list.id(); let inspector = button(|| "Open Inspector") - .on_click(move |_| { + .on_click_stop(move |_| { id.inspect(); - true }) .style(|s| s); @@ -159,13 +158,12 @@ fn app_view() -> impl View { .window_title(|| "Widget Gallery".to_owned()); let id = view.id(); - view.on_event(EventListener::KeyUp, move |e| { + view.on_event_stop(EventListener::KeyUp, move |e| { if let Event::KeyUp(e) = e { if e.key.logical_key == Key::Named(NamedKey::F11) { id.inspect(); } } - true }) } diff --git a/examples/window-scale/src/main.rs b/examples/window-scale/src/main.rs index b23da48c..933c0a36 100644 --- a/examples/window-scale/src/main.rs +++ b/examples/window-scale/src/main.rs @@ -20,16 +20,14 @@ fn app_view() -> impl View { ( label(|| "Increment") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { set_counter.update(|value| *value += 1); - true }) .keyboard_navigatable(), label(|| "Decrement") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { set_counter.update(|value| *value -= 1); - true }) .style(|s| { s.margin_left(10.0) @@ -39,10 +37,9 @@ fn app_view() -> impl View { .keyboard_navigatable(), label(|| "Reset to 0") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { println!("Reset counter pressed"); // will not fire if button is disabled set_counter.update(|value| *value = 0); - true }) .disabled(move || counter.get() == 0) .style(|s| { @@ -58,24 +55,21 @@ fn app_view() -> impl View { ( label(|| "Zoom In") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { window_scale.update(|scale| *scale *= 1.2); - true }) .style(|s| s.margin_top(10.0).margin_right(10.0)), label(|| "Zoom Out") .class(Button) - .on_click(move |_| { + .on_click_stop(move |_| { window_scale.update(|scale| *scale /= 1.2); - true }) .style(|s| s.margin_top(10.0).margin_right(10.0)), label(|| "Zoom Reset") .class(Button) .disabled(move || window_scale.get() == 1.0) - .on_click(move |_| { + .on_click_stop(move |_| { window_scale.set(1.0); - true }) .style(|s| s.margin_top(10.0).margin_right(10.0)), ) @@ -105,13 +99,12 @@ fn app_view() -> impl View { }); let id = view.id(); - view.on_event(EventListener::KeyUp, move |e| { + view.on_event_stop(EventListener::KeyUp, move |e| { if let Event::KeyUp(e) = e { if e.key.logical_key == Key::Named(NamedKey::F11) { id.inspect(); } } - true }) } diff --git a/src/context.rs b/src/context.rs index 2ff37b7b..4a454bfd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -34,7 +34,28 @@ use crate::{ view::{paint_bg, paint_border, paint_outline, View}, }; -pub type EventCallback = dyn Fn(&Event) -> bool; +/// Control whether an event will continue propagating or whether it should stop. +pub enum EventPropagation { + /// Stop event propagation and mark the event as processed + Stop, + /// Let event propagation continue + Continue, +} +impl EventPropagation { + pub fn is_continue(&self) -> bool { + matches!(self, EventPropagation::Continue) + } + + pub fn is_stop(&self) -> bool { + matches!(self, EventPropagation::Stop) + } + + pub fn is_processed(&self) -> bool { + matches!(self, EventPropagation::Stop) + } +} + +pub type EventCallback = dyn Fn(&Event) -> EventPropagation; pub type ResizeCallback = dyn Fn(Rect); pub type MenuCallback = dyn Fn() -> Menu; @@ -636,7 +657,7 @@ impl AppState { &self, id: Id, listener: &EventListener, - ) -> Option<&impl Fn(&Event) -> bool> { + ) -> Option<&impl Fn(&Event) -> EventPropagation> { self.view_states .get(&id) .and_then(|s| s.event_listeners.get(listener)) @@ -737,11 +758,11 @@ impl<'a> EventCx<'a> { view: &mut dyn View, id_path: Option<&[Id]>, event: Event, - ) -> bool { + ) -> EventPropagation { if self.should_send(view.id(), &event) { self.unconditional_view_event(view, id_path, event) } else { - false + EventPropagation::Continue } } @@ -751,16 +772,16 @@ impl<'a> EventCx<'a> { view: &mut dyn View, id_path: Option<&[Id]>, event: Event, - ) -> bool { + ) -> EventPropagation { let id = view.id(); if self.app_state.is_hidden(id) { // we don't process events for hidden view - return false; + return EventPropagation::Continue; } if self.app_state.is_disabled(&id) && !event.allow_disabled() { // if the view is disabled and the event is not processed // for disabled views - return false; + return EventPropagation::Continue; } // offset the event positions if the event has positions @@ -776,7 +797,7 @@ impl<'a> EventCx<'a> { // but the parent just passed the event on, // so it's not really for this view and we stop // the event propagation. - return false; + return EventPropagation::Continue; } let id = id_path[0]; @@ -784,7 +805,7 @@ impl<'a> EventCx<'a> { if id != view.id() { // This shouldn't happen - return false; + return EventPropagation::Continue; } // we're the parent of the event destination, so pass it on to the child @@ -793,7 +814,7 @@ impl<'a> EventCx<'a> { return self.unconditional_view_event(child, Some(id_path), event.clone()); } else { // we don't have the child, stop the event propagation - return false; + return EventPropagation::Continue; } } } @@ -801,12 +822,15 @@ impl<'a> EventCx<'a> { // if the event was dispatched to an id_path, the event is supposed to be only // handled by this view only, so we pass an empty id_path // and the event propagation would be stopped at this view - if view.event( - self, - if id_path.is_some() { Some(&[]) } else { None }, - event.clone(), - ) { - return true; + if view + .event( + self, + if id_path.is_some() { Some(&[]) } else { None }, + event.clone(), + ) + .is_processed() + { + return EventPropagation::Stop; } match &event { @@ -917,8 +941,8 @@ impl<'a> EventCx<'a> { } } if let Some(action) = self.get_event_listener(id, &EventListener::PointerMove) { - if (*action)(&event) { - return true; + if (*action)(&event).is_processed() { + return EventPropagation::Stop; } } } @@ -934,7 +958,7 @@ impl<'a> EventCx<'a> { if let Some(action) = self.get_event_listener(id, &EventListener::Drop) { - if (*action)(&event) { + if (*action)(&event).is_processed() { // if the drop is processed, we set dragging to none so that the animation // for the dragged view back to its original position isn't played. self.app_state.dragging = None; @@ -970,24 +994,24 @@ impl<'a> EventCx<'a> { .as_ref() .map(|e| e.count == 2) .unwrap_or(false) - && (*action)(&event) + && (*action)(&event).is_processed() { - return true; + return EventPropagation::Stop; } } if let Some(action) = self.get_event_listener(id, &EventListener::Click) { if on_view && self.app_state.is_clicking(&id) && last_pointer_down.is_some() - && (*action)(&event) + && (*action)(&event).is_processed() { - return true; + return EventPropagation::Stop; } } if let Some(action) = self.get_event_listener(id, &EventListener::PointerUp) { - if (*action)(&event) { - return true; + if (*action)(&event).is_processed() { + return EventPropagation::Stop; } } } else if pointer_event.button.is_secondary() { @@ -998,8 +1022,11 @@ impl<'a> EventCx<'a> { if let Some(action) = self.get_event_listener(id, &EventListener::SecondaryClick) { - if on_view && last_pointer_down.is_some() && (*action)(&event) { - return true; + if on_view + && last_pointer_down.is_some() + && (*action)(&event).is_processed() + { + return EventPropagation::Stop; } } @@ -1040,13 +1067,13 @@ impl<'a> EventCx<'a> { } else { true }; - if should_run && (*action)(&event) { - return true; + if should_run && (*action)(&event).is_processed() { + return EventPropagation::Stop; } } } - false + EventPropagation::Continue } pub(crate) fn get_size(&self, id: Id) -> Option { @@ -1067,7 +1094,7 @@ impl<'a> EventCx<'a> { &self, id: Id, listener: &EventListener, - ) -> Option<&impl Fn(&Event) -> bool> { + ) -> Option<&impl Fn(&Event) -> EventPropagation> { self.app_state.get_event_listener(id, listener) } diff --git a/src/inspector.rs b/src/inspector.rs index e222f923..82812c8e 100644 --- a/src/inspector.rs +++ b/src/inspector.rs @@ -1,941 +1,921 @@ -use crate::app::{add_app_update_event, AppUpdateEvent}; -use crate::context::{AppState, ChangeFlags, StyleCx}; -use crate::event::{Event, EventListener}; -use crate::id::Id; -use crate::profiler::profiler; -use crate::style::{Style, StyleMapValue}; -use crate::view::{view_children, View}; -use crate::views::{ - container, dyn_container, empty, h_stack, img_dynamic, scroll, stack, static_label, - static_list, tab, text, v_stack, Decorators, Label, -}; -use crate::widgets::button; -use crate::window::WindowConfig; -use crate::{new_window, style}; -use floem_reactive::{create_effect, create_rw_signal, create_signal, RwSignal, Scope}; -use image::DynamicImage; -use kurbo::{Point, Rect, Size}; -use peniko::Color; -use std::cell::Cell; -use std::collections::{HashMap, HashSet}; -use std::fmt::Display; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use taffy::prelude::Layout; -use taffy::style::{AlignItems, FlexDirection}; -use winit::keyboard::{Key, NamedKey}; -use winit::window::WindowId; - -#[derive(Clone, Debug)] -pub struct CapturedView { - id: Id, - name: String, - layout: Rect, - taffy: Layout, - clipped: Rect, - children: Vec>, - direct_style: Style, - requested_changes: ChangeFlags, - keyboard_navigable: bool, - focused: bool, -} - -impl CapturedView { - pub fn capture(view: &dyn View, app_state: &mut AppState, clip: Rect) -> Self { - let id = view.id(); - let layout = app_state.get_layout_rect(id); - let taffy = app_state.get_layout(id).unwrap(); - let computed_style = app_state.get_computed_style(id).clone(); - let keyboard_navigable = app_state.keyboard_navigable.contains(&id); - let focused = app_state.focus == Some(id); - let state = app_state.view_state(id); - let clipped = layout.intersect(clip); - Self { - id, - name: view.debug_name().to_string(), - layout, - taffy, - clipped, - direct_style: computed_style, - requested_changes: state.requested_changes, - keyboard_navigable, - focused, - children: view_children(view) - .into_iter() - .map(|view| Rc::new(CapturedView::capture(view, app_state, clipped))) - .collect(), - } - } - - fn find(&self, id: Id) -> Option<&CapturedView> { - if self.id == id { - return Some(self); - } - self.children - .iter() - .filter_map(|child| child.find(id)) - .next() - } - - fn find_by_pos(&self, pos: Point) -> Option<&CapturedView> { - self.children - .iter() - .filter_map(|child| child.find_by_pos(pos)) - .next() - .or_else(|| self.clipped.contains(pos).then_some(self)) - } - - fn warnings(&self) -> bool { - !self.requested_changes.is_empty() || self.children.iter().any(|child| child.warnings()) - } -} - -pub struct Capture { - pub root: Rc, - pub start: Instant, - pub post_style: Instant, - pub post_layout: Instant, - pub end: Instant, - pub taffy_duration: Duration, - pub taffy_node_count: usize, - pub taffy_depth: usize, - pub window: Option>, - pub window_size: Size, - pub scale: f64, - pub state: CaptureState, -} - -#[derive(Default)] -pub struct CaptureState { - styles: HashMap, -} - -impl CaptureState { - pub(crate) fn capture_style(id: Id, cx: &mut StyleCx) { - if cx.app_state_mut().capture.is_some() { - let direct = cx.direct.clone(); - let mut current = (*cx.current).clone(); - current.apply_mut(direct); - cx.app_state_mut() - .capture - .as_mut() - .unwrap() - .styles - .insert(id, current); - } - } -} - -fn captured_view_name(view: &CapturedView) -> impl View { - let name = static_label(view.name.clone()); - let id = text(view.id.to_raw()).style(|s| { - s.margin_right(5.0) - .background(Color::BLACK.with_alpha_factor(0.02)) - .border(1.0) - .border_radius(5.0) - .border_color(Color::BLACK.with_alpha_factor(0.07)) - .padding(3.0) - .padding_top(0.0) - .padding_bottom(0.0) - .font_size(12.0) - .color(Color::BLACK.with_alpha_factor(0.6)) - }); - let tab: Box = if view.focused { - Box::new(text("Focus").style(|s| { - s.margin_right(5.0) - .background(Color::rgb8(63, 81, 101).with_alpha_factor(0.6)) - .border_radius(5.0) - .padding(1.0) - .font_size(10.0) - .color(Color::WHITE.with_alpha_factor(0.8)) - })) - } else if view.keyboard_navigable { - Box::new(text("Tab").style(|s| { - s.margin_right(5.0) - .background(Color::rgb8(204, 217, 221).with_alpha_factor(0.4)) - .border(1.0) - .border_radius(5.0) - .border_color(Color::BLACK.with_alpha_factor(0.07)) - .padding(1.0) - .font_size(10.0) - .color(Color::BLACK.with_alpha_factor(0.4)) - })) - } else { - Box::new(empty()) - }; - h_stack((id, tab, name)).style(|s| s.items_center()) -} - -// Outlined to reduce stack usage. -#[inline(never)] -fn captured_view_no_children( - view: &CapturedView, - depth: usize, - capture_view: &CaptureView, -) -> Box { - let offset = depth as f64 * 14.0; - let name = captured_view_name(view); - let name_id = name.id(); - let height = 20.0; - let id = view.id; - let selected = capture_view.selected; - let highlighted = capture_view.highlighted; - - let row = Box::new( - container(name) - .style(move |s| { - s.padding_left(20.0 + offset) - .hover(move |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - .apply_if(selected.get() == Some(id), |s| { - s.background(Color::rgb8(186, 180, 216)) - }) - }) - .height(height) - .apply_if(highlighted.get() == Some(id), |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - }) - .apply_if(selected.get() == Some(id), |s| { - if highlighted.get() == Some(id) { - s.background(Color::rgb8(186, 180, 216)) - } else { - s.background(Color::rgb8(213, 208, 216)) - } - }) - }) - .on_click(move |_| { - selected.set(Some(id)); - true - }) - .on_event(EventListener::PointerEnter, move |_| { - highlighted.set(Some(id)); - false - }), - ); - - let row_id = row.id(); - let scroll_to = capture_view.scroll_to; - let expanding_selection = capture_view.expanding_selection; - create_effect(move |_| { - if let Some(selection) = expanding_selection.get() { - if selection == id { - // Scroll to the row, then to the name part of the row. - scroll_to.set(Some(row_id)); - scroll_to.set(Some(name_id)); - } - } - }); - - row -} - -// Outlined to reduce stack usage. -#[inline(never)] -fn captured_view_with_children( - view: &Rc, - depth: usize, - capture_view: &CaptureView, - children: Vec>, -) -> Box { - let offset = depth as f64 * 14.0; - let name = captured_view_name(view); - let height = 20.0; - let id = view.id; - let selected = capture_view.selected; - let highlighted = capture_view.highlighted; - let expanding_selection = capture_view.expanding_selection; - let view_ = view.clone(); - - let expanded = create_rw_signal(true); - - let name_id = name.id(); - let row = stack(( - empty() - .style(move |s| { - s.background(if expanded.get() { - Color::WHITE.with_alpha_factor(0.3) - } else { - Color::BLACK.with_alpha_factor(0.3) - }) - .border(1.0) - .width(12.0) - .height(12.0) - .margin_left(offset) - .margin_right(4.0) - .border_color(Color::BLACK.with_alpha_factor(0.4)) - .border_radius(4.0) - .hover(move |s| { - s.border_color(Color::BLACK.with_alpha_factor(0.6)) - .background(if expanded.get() { - Color::WHITE.with_alpha_factor(0.5) - } else { - Color::BLACK.with_alpha_factor(0.5) - }) - }) - }) - .on_click(move |_| { - expanded.set(!expanded.get()); - true - }), - name, - )) - .style(move |s| { - s.padding_left(3.0) - .align_items(AlignItems::Center) - .hover(move |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - .apply_if(selected.get() == Some(id), |s| { - s.background(Color::rgb8(186, 180, 216)) - }) - }) - .height(height) - .apply_if(highlighted.get() == Some(id), |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - }) - .apply_if(selected.get() == Some(id), |s| { - if highlighted.get() == Some(id) { - s.background(Color::rgb8(186, 180, 216)) - } else { - s.background(Color::rgb8(213, 208, 216)) - } - }) - }) - .on_click(move |_| { - selected.set(Some(id)); - true - }) - .on_event(EventListener::PointerEnter, move |_| { - highlighted.set(Some(id)); - false - }); - - let row_id = row.id(); - let scroll_to = capture_view.scroll_to; - create_effect(move |_| { - if let Some(selection) = expanding_selection.get() { - if selection != id && view_.find(selection).is_some() { - expanded.set(true); - } - if selection == id { - // Scroll to the row, then to the name part of the row. - scroll_to.set(Some(row_id)); - scroll_to.set(Some(name_id)); - } - } - }); - - let line = empty().style(move |s| { - s.absolute() - .height_full() - .width(1.0) - .margin_left(9.0 + offset) - .background(Color::BLACK.with_alpha_factor(0.1)) - }); - - let list = static_list(children).style(move |s| { - s.display(if expanded.get() { - style::Display::Flex - } else { - style::Display::None - }) - }); - - let list = v_stack((line, list)); - - Box::new(v_stack((row, list)).style(|s| s.min_width_full())) -} - -fn captured_view( - view: &Rc, - depth: usize, - capture_view: &CaptureView, -) -> Box { - if view.children.is_empty() { - captured_view_no_children(view, depth, capture_view) - } else { - let children: Vec<_> = view - .children - .iter() - .map(|view| captured_view(view, depth + 1, capture_view)) - .collect(); - captured_view_with_children(view, depth, capture_view, children) - } -} - -pub(crate) fn header(label: impl Display) -> Label { - text(label).style(|s| { - s.padding(5.0) - .background(Color::WHITE_SMOKE) - .width_full() - .height(27.0) - .border_bottom(1.0) - .border_color(Color::LIGHT_GRAY) - }) -} - -fn info(name: impl Display, value: String) -> impl View { - info_row(name.to_string(), static_label(value)) -} - -fn info_row(name: String, view: impl View + 'static) -> impl View { - stack(( - stack((static_label(name).style(|s| { - s.margin_right(5.0) - .color(Color::BLACK.with_alpha_factor(0.6)) - }),)) - .style(|s| s.min_width(150.0).flex_direction(FlexDirection::RowReverse)), - view, - )) - .style(|s| { - s.padding(5.0) - .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) - }) -} - -fn stats(capture: &Capture) -> impl View { - let style_time = capture.post_style.saturating_duration_since(capture.start); - let layout_time = capture - .post_layout - .saturating_duration_since(capture.post_style); - let paint_time = capture.end.saturating_duration_since(capture.post_layout); - let style_time = info( - "Style Time", - format!("{:.4} ms", style_time.as_secs_f64() * 1000.0), - ); - let layout_time = info( - "Layout Time", - format!("{:.4} ms", layout_time.as_secs_f64() * 1000.0), - ); - let taffy_time = info( - "Taffy Time", - format!("{:.4} ms", capture.taffy_duration.as_secs_f64() * 1000.0), - ); - let taffy_node_count = info("Taffy Node Count", capture.taffy_node_count.to_string()); - let taffy_depth = info("Taffy Depth", capture.taffy_depth.to_string()); - let paint_time = info( - "Paint Time", - format!("{:.4} ms", paint_time.as_secs_f64() * 1000.0), - ); - let w = info("Window Width", format!("{}", capture.window_size.width)); - let h = info("Window Height", format!("{}", capture.window_size.height)); - v_stack(( - style_time, - layout_time, - taffy_time, - taffy_node_count, - taffy_depth, - paint_time, - w, - h, - )) -} - -fn selected_view(capture: &Rc, selected: RwSignal>) -> impl View { - let capture = capture.clone(); - dyn_container( - move || selected.get(), - move |current| { - if let Some(view) = current.and_then(|id| capture.root.find(id)) { - let name = info("Type", view.name.clone()); - let id = info("Id", view.id.to_raw().to_string()); - let count = info("Child Count", format!("{}", view.children.len())); - let beyond = |view: f64, window| { - if view > window { - format!(" ({} after window edge)", view - window) - } else if view < 0.0 { - format!(" ({} before window edge)", -view) - } else { - String::new() - } - }; - let x = info( - "X", - format!( - "{}{}", - view.layout.x0, - beyond(view.layout.x0, capture.window_size.width) - ), - ); - let y = info( - "Y", - format!( - "{}{}", - view.layout.y0, - beyond(view.layout.y0, capture.window_size.height) - ), - ); - let w = info( - "Width", - format!( - "{}{}", - view.layout.width(), - beyond(view.layout.x1, capture.window_size.width) - ), - ); - let h = info( - "Height", - format!( - "{}{}", - view.layout.height(), - beyond(view.layout.y1, capture.window_size.height) - ), - ); - let tx = info( - "Taffy X", - format!( - "{}{}", - view.taffy.location.x, - beyond( - view.taffy.location.x as f64 + view.taffy.size.width as f64, - capture.window_size.width - ) - ), - ); - let ty = info( - "Taffy Y", - format!( - "{}{}", - view.taffy.location.y, - beyond( - view.taffy.location.x as f64 + view.taffy.size.width as f64, - capture.window_size.width - ) - ), - ); - let tw = info( - "Taffy Width", - format!( - "{}{}", - view.taffy.size.width, - beyond( - view.taffy.location.x as f64 + view.taffy.size.width as f64, - capture.window_size.width - ) - ), - ); - let th = info( - "Taffy Height", - format!( - "{}{}", - view.taffy.size.height, - beyond( - view.taffy.location.y as f64 + view.taffy.size.height as f64, - capture.window_size.height - ) - ), - ); - let clear = button(|| "Clear selection") - .style(|s| s.margin(5.0)) - .on_click(move |_| { - selected.set(None); - true - }); - let clear = stack((clear,)); - - let style_header = header("View Style"); - - let direct: HashSet<_> = view.direct_style.map.keys().copied().collect(); - - let style = capture - .state - .styles - .get(&view.id) - .cloned() - .unwrap_or_default(); - - let mut style_list = style - .map - .clone() - .into_iter() - .map(|(p, v)| ((p, format!("{p:?}")), v)) - .collect::>(); - - style_list.sort_unstable_by(|a, b| a.0 .1.cmp(&b.0 .1)); - - let style_list = - static_list(style_list.into_iter().map(|((prop, name), value)| { - let name = name.strip_prefix("floem::style::").unwrap_or(&name); - let name: Box = if direct.contains(&prop) { - Box::new(text(name)) - } else { - Box::new(stack(( - text("Inherited").style(|s| { - s.margin_right(5.0) - .background(Color::WHITE_SMOKE.with_alpha_factor(0.6)) - .border(1.0) - .border_radius(5.0) - .border_color(Color::WHITE_SMOKE) - .padding(1.0) - .font_size(10.0) - .color(Color::BLACK.with_alpha_factor(0.4)) - }), - text(name), - ))) - }; - let mut v: Box = match value { - StyleMapValue::Val(v) => { - let v = &*v; - (prop.info.debug_view)(v).unwrap_or_else(|| { - Box::new(static_label((prop.info.debug_any)(v))) - }) - } - StyleMapValue::Unset => Box::new(text("Unset")), - }; - if let Some(transition) = style.transitions.get(&prop).cloned() { - let transition = stack(( - text("Transition").style(|s| { - s.margin_top(5.0) - .margin_right(5.0) - .background(Color::WHITE_SMOKE.with_alpha_factor(0.6)) - .border(1.0) - .border_radius(5.0) - .border_color(Color::WHITE_SMOKE) - .padding(1.0) - .font_size(10.0) - .color(Color::BLACK.with_alpha_factor(0.4)) - }), - static_label(format!("{transition:?}")), - )) - .style(|s| s.items_center()); - v = Box::new(v_stack((v, transition))); - } - stack(( - stack((name.style(|s| { - s.margin_right(5.0) - .color(Color::BLACK.with_alpha_factor(0.6)) - }),)) - .style(|s| { - s.min_width(150.0).flex_direction(FlexDirection::RowReverse) - }), - v, - )) - .style(|s| { - s.padding(5.0) - .items_center() - .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) - }) - })) - .style(|s| s.width_full()); - - Box::new( - v_stack(( - name, - id, - count, - x, - y, - w, - h, - tx, - ty, - tw, - th, - clear, - style_header, - style_list, - )) - .style(|s| s.width_full()), - ) - } else { - Box::new(text("No selection").style(|s| s.padding(5.0))) - } - }, - ) -} - -#[derive(Clone, Copy)] -struct CaptureView { - expanding_selection: RwSignal>, - scroll_to: RwSignal>, - selected: RwSignal>, - highlighted: RwSignal>, -} - -fn capture_view(capture: &Rc) -> impl View { - let capture_view = CaptureView { - expanding_selection: create_rw_signal(None), - scroll_to: create_rw_signal(None), - selected: create_rw_signal(None), - highlighted: create_rw_signal(None), - }; - - let window = capture.window.clone(); - let capture_ = capture.clone(); - let capture__ = capture.clone(); - let (image_width, image_height) = capture - .window - .as_ref() - .map(|img| { - ( - img.width() as f64 / capture.scale, - img.height() as f64 / capture.scale, - ) - }) - .unwrap_or_default(); - let image = img_dynamic(move || window.clone()) - .style(move |s| { - s.margin(5.0) - .border(1.0) - .border_color(Color::BLACK.with_alpha_factor(0.5)) - .width(image_width + 2.0) - .height(image_height + 2.0) - .margin_bottom(21.0) - .margin_right(21.0) - }) - .on_event(EventListener::PointerMove, move |e| { - if let Event::PointerMove(e) = e { - if let Some(view) = capture_.root.find_by_pos(e.pos) { - if capture_view.highlighted.get() != Some(view.id) { - capture_view.highlighted.set(Some(view.id)); - } - return false; - } - } - if capture_view.highlighted.get().is_some() { - capture_view.highlighted.set(None); - } - false - }) - .on_click(move |e| { - if let Event::PointerUp(e) = e { - if let Some(view) = capture__.root.find_by_pos(e.pos) { - capture_view.selected.set(Some(view.id)); - capture_view.expanding_selection.set(Some(view.id)); - return true; - } - } - if capture_view.selected.get().is_some() { - capture_view.selected.set(None); - } - true - }) - .on_event(EventListener::PointerLeave, move |_| { - capture_view.highlighted.set(None); - false - }); - - let capture_ = capture.clone(); - let selected_overlay = empty().style(move |s| { - if let Some(view) = capture_view - .selected - .get() - .and_then(|id| capture_.root.find(id)) - { - s.absolute() - .margin_left(5.0 + view.layout.x0) - .margin_top(5.0 + view.layout.y0) - .width(view.layout.width()) - .height(view.layout.height()) - .background(Color::rgb8(186, 180, 216).with_alpha_factor(0.5)) - .border_color(Color::rgb8(186, 180, 216).with_alpha_factor(0.7)) - .border(1.0) - } else { - s - } - }); - - let capture_ = capture.clone(); - let highlighted_overlay = empty().style(move |s| { - if let Some(view) = capture_view - .highlighted - .get() - .and_then(|id| capture_.root.find(id)) - { - s.absolute() - .margin_left(5.0 + view.layout.x0) - .margin_top(5.0 + view.layout.y0) - .width(view.layout.width()) - .height(view.layout.height()) - .background(Color::rgba8(228, 237, 216, 120)) - .border_color(Color::rgba8(75, 87, 53, 120)) - .border(1.0) - } else { - s - } - }); - - let image = stack((image, selected_overlay, highlighted_overlay)); - - let left_scroll = scroll( - v_stack(( - header("Selected View"), - selected_view(capture, capture_view.selected), - header("Stats"), - stats(capture), - )) - .style(|s| s.width_full()), - ) - .style(|s| s.width_full().flex_basis(0).min_height(0).flex_grow(1.0)); - - let seperator = empty().style(move |s| { - s.width_full() - .min_height(1.0) - .background(Color::BLACK.with_alpha_factor(0.2)) - }); - - let left = v_stack(( - header("Captured Window"), - scroll(image).style(|s| s.max_height_pct(60.0)), - seperator, - left_scroll, - )) - .style(|s| s.max_width_pct(60.0)); - - let tree = scroll(captured_view(&capture.root, 0, &capture_view)) - .style(|s| s.width_full().min_height(0).flex_basis(0).flex_grow(1.0)) - .on_event(EventListener::PointerLeave, move |_| { - capture_view.highlighted.set(None); - false - }) - .on_click(move |_| { - capture_view.selected.set(None); - true - }) - .scroll_to_view(move || capture_view.scroll_to.get()); - - let tree: Box = if capture.root.warnings() { - Box::new(v_stack((header("Warnings"), header("View Tree"), tree))) - } else { - Box::new(v_stack((header("View Tree"), tree))) - }; - - let tree = tree.style(|s| s.height_full().min_width(0).flex_basis(0).flex_grow(1.0)); - - let seperator = empty().style(move |s| { - s.height_full() - .min_width(1.0) - .background(Color::BLACK.with_alpha_factor(0.2)) - }); - - h_stack((left, seperator, tree)).style(|s| s.height_full().width_full().max_width_full()) -} - -fn inspector_view(capture: &Option>) -> impl View { - let view: Box = if let Some(capture) = capture { - Box::new(capture_view(capture)) - } else { - Box::new(text("No capture")) - }; - - stack((view,)) - .window_title(|| "Floem Inspector".to_owned()) - .style(|s| { - s.width_full() - .height_full() - .background(Color::WHITE) - .class(scroll::Handle, |s| { - s.border_radius(4.0) - .background(Color::rgba8(166, 166, 166, 140)) - .set(scroll::Thickness, 16.0) - .set(scroll::Rounded, false) - .active(|s| s.background(Color::rgb8(166, 166, 166))) - .hover(|s| s.background(Color::rgb8(184, 184, 184))) - }) - .class(scroll::Track, |s| { - s.hover(|s| s.background(Color::rgba8(166, 166, 166, 30))) - }) - }) -} - -thread_local! { - pub(crate) static RUNNING: Cell = Cell::new(false); - pub(crate) static CAPTURE: RwSignal>> = { - Scope::new().create_rw_signal(None) - }; -} - -pub fn capture(window_id: WindowId) { - let capture = CAPTURE.with(|c| *c); - - if !RUNNING.get() { - RUNNING.set(true); - new_window( - move |_| { - let (selected, set_selected) = create_signal(0); - - let tab_item = |name, index| { - text(name) - .on_click(move |_| { - set_selected.set(index); - true - }) - .style(move |s| { - s.padding(5.0) - .border_right(1) - .border_color(Color::BLACK.with_alpha_factor(0.2)) - .hover(move |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - .apply_if(selected.get() == index, |s| { - s.background(Color::rgb8(186, 180, 216)) - }) - }) - .apply_if(selected.get() == index, |s| { - s.background(Color::rgb8(213, 208, 216)) - }) - }) - }; - - let tabs = h_stack((tab_item("Views", 0), tab_item("Profiler", 1))) - .style(|s| s.background(Color::WHITE)); - - let tab = tab( - move || selected.get(), - move || [0, 1].into_iter(), - |it| *it, - move |it| -> Box { - match it { - 0 => Box::new( - dyn_container( - move || capture.get(), - |capture| Box::new(inspector_view(&capture)), - ) - .style(|s| s.width_full().height_full()), - ), - 1 => Box::new(profiler(window_id)), - _ => panic!(), - } - }, - ) - .style(|s| s.flex_basis(0.0).min_height(0.0).flex_grow(1.0)); - - let seperator = empty().style(move |s| { - s.width_full() - .min_height(1.0) - .background(Color::BLACK.with_alpha_factor(0.2)) - }); - - let stack = v_stack((tabs, seperator, tab)); - let id = stack.id(); - stack - .style(|s| s.width_full().height_full()) - .on_event(EventListener::KeyUp, move |e| { - if let Event::KeyUp(e) = e { - if e.key.logical_key == Key::Named(NamedKey::F11) - && e.modifiers.shift_key() - { - id.inspect(); - return true; - } - } - false - }) - .on_event(EventListener::WindowClosed, |_| { - RUNNING.set(false); - false - }) - }, - Some(WindowConfig { - size: Some(Size { - width: 1200.0, - height: 800.0, - }), - ..Default::default() - }), - ); - } - - add_app_update_event(AppUpdateEvent::CaptureWindow { - window_id, - capture: capture.write_only(), - }) -} +use crate::app::{add_app_update_event, AppUpdateEvent}; +use crate::context::{AppState, ChangeFlags, StyleCx}; +use crate::event::{Event, EventListener}; +use crate::id::Id; +use crate::profiler::profiler; +use crate::style::{Style, StyleMapValue}; +use crate::view::{view_children, View}; +use crate::views::{ + container, dyn_container, empty, h_stack, img_dynamic, scroll, stack, static_label, + static_list, tab, text, v_stack, Decorators, Label, +}; +use crate::widgets::button; +use crate::window::WindowConfig; +use crate::{new_window, style, EventPropagation}; +use floem_reactive::{create_effect, create_rw_signal, create_signal, RwSignal, Scope}; +use image::DynamicImage; +use kurbo::{Point, Rect, Size}; +use peniko::Color; +use std::cell::Cell; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::rc::Rc; +use std::time::{Duration, Instant}; +use taffy::prelude::Layout; +use taffy::style::{AlignItems, FlexDirection}; +use winit::keyboard::{Key, NamedKey}; +use winit::window::WindowId; + +#[derive(Clone, Debug)] +pub struct CapturedView { + id: Id, + name: String, + layout: Rect, + taffy: Layout, + clipped: Rect, + children: Vec>, + direct_style: Style, + requested_changes: ChangeFlags, + keyboard_navigable: bool, + focused: bool, +} + +impl CapturedView { + pub fn capture(view: &dyn View, app_state: &mut AppState, clip: Rect) -> Self { + let id = view.id(); + let layout = app_state.get_layout_rect(id); + let taffy = app_state.get_layout(id).unwrap(); + let computed_style = app_state.get_computed_style(id).clone(); + let keyboard_navigable = app_state.keyboard_navigable.contains(&id); + let focused = app_state.focus == Some(id); + let state = app_state.view_state(id); + let clipped = layout.intersect(clip); + Self { + id, + name: view.debug_name().to_string(), + layout, + taffy, + clipped, + direct_style: computed_style, + requested_changes: state.requested_changes, + keyboard_navigable, + focused, + children: view_children(view) + .into_iter() + .map(|view| Rc::new(CapturedView::capture(view, app_state, clipped))) + .collect(), + } + } + + fn find(&self, id: Id) -> Option<&CapturedView> { + if self.id == id { + return Some(self); + } + self.children + .iter() + .filter_map(|child| child.find(id)) + .next() + } + + fn find_by_pos(&self, pos: Point) -> Option<&CapturedView> { + self.children + .iter() + .filter_map(|child| child.find_by_pos(pos)) + .next() + .or_else(|| self.clipped.contains(pos).then_some(self)) + } + + fn warnings(&self) -> bool { + !self.requested_changes.is_empty() || self.children.iter().any(|child| child.warnings()) + } +} + +pub struct Capture { + pub root: Rc, + pub start: Instant, + pub post_style: Instant, + pub post_layout: Instant, + pub end: Instant, + pub taffy_duration: Duration, + pub taffy_node_count: usize, + pub taffy_depth: usize, + pub window: Option>, + pub window_size: Size, + pub scale: f64, + pub state: CaptureState, +} + +#[derive(Default)] +pub struct CaptureState { + styles: HashMap, +} + +impl CaptureState { + pub(crate) fn capture_style(id: Id, cx: &mut StyleCx) { + if cx.app_state_mut().capture.is_some() { + let direct = cx.direct.clone(); + let mut current = (*cx.current).clone(); + current.apply_mut(direct); + cx.app_state_mut() + .capture + .as_mut() + .unwrap() + .styles + .insert(id, current); + } + } +} + +fn captured_view_name(view: &CapturedView) -> impl View { + let name = static_label(view.name.clone()); + let id = text(view.id.to_raw()).style(|s| { + s.margin_right(5.0) + .background(Color::BLACK.with_alpha_factor(0.02)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::BLACK.with_alpha_factor(0.07)) + .padding(3.0) + .padding_top(0.0) + .padding_bottom(0.0) + .font_size(12.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }); + let tab: Box = if view.focused { + Box::new(text("Focus").style(|s| { + s.margin_right(5.0) + .background(Color::rgb8(63, 81, 101).with_alpha_factor(0.6)) + .border_radius(5.0) + .padding(1.0) + .font_size(10.0) + .color(Color::WHITE.with_alpha_factor(0.8)) + })) + } else if view.keyboard_navigable { + Box::new(text("Tab").style(|s| { + s.margin_right(5.0) + .background(Color::rgb8(204, 217, 221).with_alpha_factor(0.4)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::BLACK.with_alpha_factor(0.07)) + .padding(1.0) + .font_size(10.0) + .color(Color::BLACK.with_alpha_factor(0.4)) + })) + } else { + Box::new(empty()) + }; + h_stack((id, tab, name)).style(|s| s.items_center()) +} + +// Outlined to reduce stack usage. +#[inline(never)] +fn captured_view_no_children( + view: &CapturedView, + depth: usize, + capture_view: &CaptureView, +) -> Box { + let offset = depth as f64 * 14.0; + let name = captured_view_name(view); + let name_id = name.id(); + let height = 20.0; + let id = view.id; + let selected = capture_view.selected; + let highlighted = capture_view.highlighted; + + let row = Box::new( + container(name) + .style(move |s| { + s.padding_left(20.0 + offset) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == Some(id), |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .height(height) + .apply_if(highlighted.get() == Some(id), |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + }) + .apply_if(selected.get() == Some(id), |s| { + if highlighted.get() == Some(id) { + s.background(Color::rgb8(186, 180, 216)) + } else { + s.background(Color::rgb8(213, 208, 216)) + } + }) + }) + .on_click_stop(move |_| selected.set(Some(id))) + .on_event_cont(EventListener::PointerEnter, move |_| { + highlighted.set(Some(id)) + }), + ); + + let row_id = row.id(); + let scroll_to = capture_view.scroll_to; + let expanding_selection = capture_view.expanding_selection; + create_effect(move |_| { + if let Some(selection) = expanding_selection.get() { + if selection == id { + // Scroll to the row, then to the name part of the row. + scroll_to.set(Some(row_id)); + scroll_to.set(Some(name_id)); + } + } + }); + + row +} + +// Outlined to reduce stack usage. +#[inline(never)] +fn captured_view_with_children( + view: &Rc, + depth: usize, + capture_view: &CaptureView, + children: Vec>, +) -> Box { + let offset = depth as f64 * 14.0; + let name = captured_view_name(view); + let height = 20.0; + let id = view.id; + let selected = capture_view.selected; + let highlighted = capture_view.highlighted; + let expanding_selection = capture_view.expanding_selection; + let view_ = view.clone(); + + let expanded = create_rw_signal(true); + + let name_id = name.id(); + let row = stack(( + empty() + .style(move |s| { + s.background(if expanded.get() { + Color::WHITE.with_alpha_factor(0.3) + } else { + Color::BLACK.with_alpha_factor(0.3) + }) + .border(1.0) + .width(12.0) + .height(12.0) + .margin_left(offset) + .margin_right(4.0) + .border_color(Color::BLACK.with_alpha_factor(0.4)) + .border_radius(4.0) + .hover(move |s| { + s.border_color(Color::BLACK.with_alpha_factor(0.6)) + .background(if expanded.get() { + Color::WHITE.with_alpha_factor(0.5) + } else { + Color::BLACK.with_alpha_factor(0.5) + }) + }) + }) + .on_click_stop(move |_| { + expanded.set(!expanded.get()); + }), + name, + )) + .style(move |s| { + s.padding_left(3.0) + .align_items(AlignItems::Center) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == Some(id), |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .height(height) + .apply_if(highlighted.get() == Some(id), |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + }) + .apply_if(selected.get() == Some(id), |s| { + if highlighted.get() == Some(id) { + s.background(Color::rgb8(186, 180, 216)) + } else { + s.background(Color::rgb8(213, 208, 216)) + } + }) + }) + .on_click_stop(move |_| selected.set(Some(id))) + .on_event_cont(EventListener::PointerEnter, move |_| { + highlighted.set(Some(id)) + }); + + let row_id = row.id(); + let scroll_to = capture_view.scroll_to; + create_effect(move |_| { + if let Some(selection) = expanding_selection.get() { + if selection != id && view_.find(selection).is_some() { + expanded.set(true); + } + if selection == id { + // Scroll to the row, then to the name part of the row. + scroll_to.set(Some(row_id)); + scroll_to.set(Some(name_id)); + } + } + }); + + let line = empty().style(move |s| { + s.absolute() + .height_full() + .width(1.0) + .margin_left(9.0 + offset) + .background(Color::BLACK.with_alpha_factor(0.1)) + }); + + let list = static_list(children).style(move |s| { + s.display(if expanded.get() { + style::Display::Flex + } else { + style::Display::None + }) + }); + + let list = v_stack((line, list)); + + Box::new(v_stack((row, list)).style(|s| s.min_width_full())) +} + +fn captured_view( + view: &Rc, + depth: usize, + capture_view: &CaptureView, +) -> Box { + if view.children.is_empty() { + captured_view_no_children(view, depth, capture_view) + } else { + let children: Vec<_> = view + .children + .iter() + .map(|view| captured_view(view, depth + 1, capture_view)) + .collect(); + captured_view_with_children(view, depth, capture_view, children) + } +} + +pub(crate) fn header(label: impl Display) -> Label { + text(label).style(|s| { + s.padding(5.0) + .background(Color::WHITE_SMOKE) + .width_full() + .height(27.0) + .border_bottom(1.0) + .border_color(Color::LIGHT_GRAY) + }) +} + +fn info(name: impl Display, value: String) -> impl View { + info_row(name.to_string(), static_label(value)) +} + +fn info_row(name: String, view: impl View + 'static) -> impl View { + stack(( + stack((static_label(name).style(|s| { + s.margin_right(5.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }),)) + .style(|s| s.min_width(150.0).flex_direction(FlexDirection::RowReverse)), + view, + )) + .style(|s| { + s.padding(5.0) + .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) + }) +} + +fn stats(capture: &Capture) -> impl View { + let style_time = capture.post_style.saturating_duration_since(capture.start); + let layout_time = capture + .post_layout + .saturating_duration_since(capture.post_style); + let paint_time = capture.end.saturating_duration_since(capture.post_layout); + let style_time = info( + "Style Time", + format!("{:.4} ms", style_time.as_secs_f64() * 1000.0), + ); + let layout_time = info( + "Layout Time", + format!("{:.4} ms", layout_time.as_secs_f64() * 1000.0), + ); + let taffy_time = info( + "Taffy Time", + format!("{:.4} ms", capture.taffy_duration.as_secs_f64() * 1000.0), + ); + let taffy_node_count = info("Taffy Node Count", capture.taffy_node_count.to_string()); + let taffy_depth = info("Taffy Depth", capture.taffy_depth.to_string()); + let paint_time = info( + "Paint Time", + format!("{:.4} ms", paint_time.as_secs_f64() * 1000.0), + ); + let w = info("Window Width", format!("{}", capture.window_size.width)); + let h = info("Window Height", format!("{}", capture.window_size.height)); + v_stack(( + style_time, + layout_time, + taffy_time, + taffy_node_count, + taffy_depth, + paint_time, + w, + h, + )) +} + +fn selected_view(capture: &Rc, selected: RwSignal>) -> impl View { + let capture = capture.clone(); + dyn_container( + move || selected.get(), + move |current| { + if let Some(view) = current.and_then(|id| capture.root.find(id)) { + let name = info("Type", view.name.clone()); + let id = info("Id", view.id.to_raw().to_string()); + let count = info("Child Count", format!("{}", view.children.len())); + let beyond = |view: f64, window| { + if view > window { + format!(" ({} after window edge)", view - window) + } else if view < 0.0 { + format!(" ({} before window edge)", -view) + } else { + String::new() + } + }; + let x = info( + "X", + format!( + "{}{}", + view.layout.x0, + beyond(view.layout.x0, capture.window_size.width) + ), + ); + let y = info( + "Y", + format!( + "{}{}", + view.layout.y0, + beyond(view.layout.y0, capture.window_size.height) + ), + ); + let w = info( + "Width", + format!( + "{}{}", + view.layout.width(), + beyond(view.layout.x1, capture.window_size.width) + ), + ); + let h = info( + "Height", + format!( + "{}{}", + view.layout.height(), + beyond(view.layout.y1, capture.window_size.height) + ), + ); + let tx = info( + "Taffy X", + format!( + "{}{}", + view.taffy.location.x, + beyond( + view.taffy.location.x as f64 + view.taffy.size.width as f64, + capture.window_size.width + ) + ), + ); + let ty = info( + "Taffy Y", + format!( + "{}{}", + view.taffy.location.y, + beyond( + view.taffy.location.x as f64 + view.taffy.size.width as f64, + capture.window_size.width + ) + ), + ); + let tw = info( + "Taffy Width", + format!( + "{}{}", + view.taffy.size.width, + beyond( + view.taffy.location.x as f64 + view.taffy.size.width as f64, + capture.window_size.width + ) + ), + ); + let th = info( + "Taffy Height", + format!( + "{}{}", + view.taffy.size.height, + beyond( + view.taffy.location.y as f64 + view.taffy.size.height as f64, + capture.window_size.height + ) + ), + ); + let clear = button(|| "Clear selection") + .style(|s| s.margin(5.0)) + .on_click_stop(move |_| selected.set(None)); + let clear = stack((clear,)); + + let style_header = header("View Style"); + + let direct: HashSet<_> = view.direct_style.map.keys().copied().collect(); + + let style = capture + .state + .styles + .get(&view.id) + .cloned() + .unwrap_or_default(); + + let mut style_list = style + .map + .clone() + .into_iter() + .map(|(p, v)| ((p, format!("{p:?}")), v)) + .collect::>(); + + style_list.sort_unstable_by(|a, b| a.0 .1.cmp(&b.0 .1)); + + let style_list = + static_list(style_list.into_iter().map(|((prop, name), value)| { + let name = name.strip_prefix("floem::style::").unwrap_or(&name); + let name: Box = if direct.contains(&prop) { + Box::new(text(name)) + } else { + Box::new(stack(( + text("Inherited").style(|s| { + s.margin_right(5.0) + .background(Color::WHITE_SMOKE.with_alpha_factor(0.6)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::WHITE_SMOKE) + .padding(1.0) + .font_size(10.0) + .color(Color::BLACK.with_alpha_factor(0.4)) + }), + text(name), + ))) + }; + let mut v: Box = match value { + StyleMapValue::Val(v) => { + let v = &*v; + (prop.info.debug_view)(v).unwrap_or_else(|| { + Box::new(static_label((prop.info.debug_any)(v))) + }) + } + StyleMapValue::Unset => Box::new(text("Unset")), + }; + if let Some(transition) = style.transitions.get(&prop).cloned() { + let transition = stack(( + text("Transition").style(|s| { + s.margin_top(5.0) + .margin_right(5.0) + .background(Color::WHITE_SMOKE.with_alpha_factor(0.6)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::WHITE_SMOKE) + .padding(1.0) + .font_size(10.0) + .color(Color::BLACK.with_alpha_factor(0.4)) + }), + static_label(format!("{transition:?}")), + )) + .style(|s| s.items_center()); + v = Box::new(v_stack((v, transition))); + } + stack(( + stack((name.style(|s| { + s.margin_right(5.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }),)) + .style(|s| { + s.min_width(150.0).flex_direction(FlexDirection::RowReverse) + }), + v, + )) + .style(|s| { + s.padding(5.0) + .items_center() + .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) + }) + })) + .style(|s| s.width_full()); + + Box::new( + v_stack(( + name, + id, + count, + x, + y, + w, + h, + tx, + ty, + tw, + th, + clear, + style_header, + style_list, + )) + .style(|s| s.width_full()), + ) + } else { + Box::new(text("No selection").style(|s| s.padding(5.0))) + } + }, + ) +} + +#[derive(Clone, Copy)] +struct CaptureView { + expanding_selection: RwSignal>, + scroll_to: RwSignal>, + selected: RwSignal>, + highlighted: RwSignal>, +} + +fn capture_view(capture: &Rc) -> impl View { + let capture_view = CaptureView { + expanding_selection: create_rw_signal(None), + scroll_to: create_rw_signal(None), + selected: create_rw_signal(None), + highlighted: create_rw_signal(None), + }; + + let window = capture.window.clone(); + let capture_ = capture.clone(); + let capture__ = capture.clone(); + let (image_width, image_height) = capture + .window + .as_ref() + .map(|img| { + ( + img.width() as f64 / capture.scale, + img.height() as f64 / capture.scale, + ) + }) + .unwrap_or_default(); + let image = img_dynamic(move || window.clone()) + .style(move |s| { + s.margin(5.0) + .border(1.0) + .border_color(Color::BLACK.with_alpha_factor(0.5)) + .width(image_width + 2.0) + .height(image_height + 2.0) + .margin_bottom(21.0) + .margin_right(21.0) + }) + .on_event(EventListener::PointerMove, move |e| { + if let Event::PointerMove(e) = e { + if let Some(view) = capture_.root.find_by_pos(e.pos) { + if capture_view.highlighted.get() != Some(view.id) { + capture_view.highlighted.set(Some(view.id)); + } + return EventPropagation::Continue; + } + } + if capture_view.highlighted.get().is_some() { + capture_view.highlighted.set(None); + } + EventPropagation::Continue + }) + .on_click(move |e| { + if let Event::PointerUp(e) = e { + if let Some(view) = capture__.root.find_by_pos(e.pos) { + capture_view.selected.set(Some(view.id)); + capture_view.expanding_selection.set(Some(view.id)); + return EventPropagation::Stop; + } + } + if capture_view.selected.get().is_some() { + capture_view.selected.set(None); + } + EventPropagation::Stop + }) + .on_event_cont(EventListener::PointerLeave, move |_| { + capture_view.highlighted.set(None) + }); + + let capture_ = capture.clone(); + let selected_overlay = empty().style(move |s| { + if let Some(view) = capture_view + .selected + .get() + .and_then(|id| capture_.root.find(id)) + { + s.absolute() + .margin_left(5.0 + view.layout.x0) + .margin_top(5.0 + view.layout.y0) + .width(view.layout.width()) + .height(view.layout.height()) + .background(Color::rgb8(186, 180, 216).with_alpha_factor(0.5)) + .border_color(Color::rgb8(186, 180, 216).with_alpha_factor(0.7)) + .border(1.0) + } else { + s + } + }); + + let capture_ = capture.clone(); + let highlighted_overlay = empty().style(move |s| { + if let Some(view) = capture_view + .highlighted + .get() + .and_then(|id| capture_.root.find(id)) + { + s.absolute() + .margin_left(5.0 + view.layout.x0) + .margin_top(5.0 + view.layout.y0) + .width(view.layout.width()) + .height(view.layout.height()) + .background(Color::rgba8(228, 237, 216, 120)) + .border_color(Color::rgba8(75, 87, 53, 120)) + .border(1.0) + } else { + s + } + }); + + let image = stack((image, selected_overlay, highlighted_overlay)); + + let left_scroll = scroll( + v_stack(( + header("Selected View"), + selected_view(capture, capture_view.selected), + header("Stats"), + stats(capture), + )) + .style(|s| s.width_full()), + ) + .style(|s| s.width_full().flex_basis(0).min_height(0).flex_grow(1.0)); + + let seperator = empty().style(move |s| { + s.width_full() + .min_height(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let left = v_stack(( + header("Captured Window"), + scroll(image).style(|s| s.max_height_pct(60.0)), + seperator, + left_scroll, + )) + .style(|s| s.max_width_pct(60.0)); + + let tree = scroll(captured_view(&capture.root, 0, &capture_view)) + .style(|s| s.width_full().min_height(0).flex_basis(0).flex_grow(1.0)) + .on_event_cont(EventListener::PointerLeave, move |_| { + capture_view.highlighted.set(None) + }) + .on_click_stop(move |_| capture_view.selected.set(None)) + .scroll_to_view(move || capture_view.scroll_to.get()); + + let tree: Box = if capture.root.warnings() { + Box::new(v_stack((header("Warnings"), header("View Tree"), tree))) + } else { + Box::new(v_stack((header("View Tree"), tree))) + }; + + let tree = tree.style(|s| s.height_full().min_width(0).flex_basis(0).flex_grow(1.0)); + + let seperator = empty().style(move |s| { + s.height_full() + .min_width(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + h_stack((left, seperator, tree)).style(|s| s.height_full().width_full().max_width_full()) +} + +fn inspector_view(capture: &Option>) -> impl View { + let view: Box = if let Some(capture) = capture { + Box::new(capture_view(capture)) + } else { + Box::new(text("No capture")) + }; + + stack((view,)) + .window_title(|| "Floem Inspector".to_owned()) + .style(|s| { + s.width_full() + .height_full() + .background(Color::WHITE) + .class(scroll::Handle, |s| { + s.border_radius(4.0) + .background(Color::rgba8(166, 166, 166, 140)) + .set(scroll::Thickness, 16.0) + .set(scroll::Rounded, false) + .active(|s| s.background(Color::rgb8(166, 166, 166))) + .hover(|s| s.background(Color::rgb8(184, 184, 184))) + }) + .class(scroll::Track, |s| { + s.hover(|s| s.background(Color::rgba8(166, 166, 166, 30))) + }) + }) +} + +thread_local! { + pub(crate) static RUNNING: Cell = Cell::new(false); + pub(crate) static CAPTURE: RwSignal>> = { + Scope::new().create_rw_signal(None) + }; +} + +pub fn capture(window_id: WindowId) { + let capture = CAPTURE.with(|c| *c); + + if !RUNNING.get() { + RUNNING.set(true); + new_window( + move |_| { + let (selected, set_selected) = create_signal(0); + + let tab_item = |name, index| { + text(name) + .on_click_stop(move |_| set_selected.set(index)) + .style(move |s| { + s.padding(5.0) + .border_right(1) + .border_color(Color::BLACK.with_alpha_factor(0.2)) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == index, |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .apply_if(selected.get() == index, |s| { + s.background(Color::rgb8(213, 208, 216)) + }) + }) + }; + + let tabs = h_stack((tab_item("Views", 0), tab_item("Profiler", 1))) + .style(|s| s.background(Color::WHITE)); + + let tab = tab( + move || selected.get(), + move || [0, 1].into_iter(), + |it| *it, + move |it| -> Box { + match it { + 0 => Box::new( + dyn_container( + move || capture.get(), + |capture| Box::new(inspector_view(&capture)), + ) + .style(|s| s.width_full().height_full()), + ), + 1 => Box::new(profiler(window_id)), + _ => panic!(), + } + }, + ) + .style(|s| s.flex_basis(0.0).min_height(0.0).flex_grow(1.0)); + + let seperator = empty().style(move |s| { + s.width_full() + .min_height(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let stack = v_stack((tabs, seperator, tab)); + let id = stack.id(); + stack + .style(|s| s.width_full().height_full()) + .on_event(EventListener::KeyUp, move |e| { + if let Event::KeyUp(e) = e { + if e.key.logical_key == Key::Named(NamedKey::F11) + && e.modifiers.shift_key() + { + id.inspect(); + return EventPropagation::Stop; + } + } + EventPropagation::Continue + }) + .on_event(EventListener::WindowClosed, |_| { + RUNNING.set(false); + EventPropagation::Continue + }) + }, + Some(WindowConfig { + size: Some(Size { + width: 1200.0, + height: 800.0, + }), + ..Default::default() + }), + ); + } + + add_app_update_event(AppUpdateEvent::CaptureWindow { + window_id, + capture: capture.write_only(), + }) +} diff --git a/src/lib.rs b/src/lib.rs index 9621fe87..7c01a63d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,7 @@ pub mod window; mod window_handle; pub use app::{launch, quit_app, AppEvent, Application}; +pub use context::EventPropagation; pub use floem_reactive as reactive; pub use floem_renderer::cosmic_text; pub use floem_renderer::Renderer; diff --git a/src/profiler.rs b/src/profiler.rs index 5ae14ab5..5dac16d7 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -1,307 +1,304 @@ -use crate::app::{add_app_update_event, AppUpdateEvent}; -use crate::event::{Event, EventListener}; -use crate::inspector::header; -use crate::view::View; -use crate::views::{ - clip, container, dyn_container, empty, h_stack, label, scroll, stack, static_label, - static_list, text, v_stack, Decorators, -}; -use crate::widgets::button; -use floem_reactive::{create_rw_signal, RwSignal, Scope}; -use peniko::Color; -use std::fmt::Display; -use std::mem; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use taffy::style::FlexDirection; -use winit::window::WindowId; - -#[derive(Clone)] -pub struct ProfileEvent { - pub start: Instant, - pub end: Instant, - pub name: &'static str, -} - -#[derive(Default)] -pub struct ProfileFrame { - pub events: Vec, -} - -#[derive(Default)] -pub struct Profile { - pub current: ProfileFrame, - frames: Vec, -} - -impl Profile { - pub fn next_frame(&mut self) { - self.frames.push(mem::take(&mut self.current)); - } -} - -struct ProfileFrameData { - start: Option, - duration: Duration, - sum: Duration, - events: Vec, -} - -fn info(name: impl Display, value: String) -> impl View { - info_row(name.to_string(), static_label(value)) -} - -fn info_row(name: String, view: impl View + 'static) -> impl View { - stack(( - stack((static_label(name).style(|s| { - s.margin_right(5.0) - .color(Color::BLACK.with_alpha_factor(0.6)) - }),)) - .style(|s| s.min_width(80.0).flex_direction(FlexDirection::RowReverse)), - view, - )) - .style(|s| { - s.padding(5.0) - .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) - }) -} - -fn profile_view(profile: &Rc) -> impl View { - let mut frames: Vec<_> = profile - .frames - .iter() - .map(|frame| { - let start = frame.events.first().map(|event| event.start); - let end = frame.events.last().map(|event| event.end); - let sum = frame - .events - .iter() - .map(|event| event.end.saturating_duration_since(event.start)) - .sum(); - let duration = start - .and_then(|start| end.map(|end| end.saturating_duration_since(start))) - .unwrap_or_default(); - Rc::new(ProfileFrameData { - start, - duration, - sum, - events: frame.events.clone(), - }) - }) - .collect(); - frames.sort_by(|a, b| b.sum.cmp(&a.sum)); - - let selected_frame = create_rw_signal(None); - - let zoom = create_rw_signal(1.0); - - let frames: Vec<_> = frames - .iter() - .enumerate() - .map(|(i, frame)| { - let frame = frame.clone(); - let frame_ = frame.clone(); - h_stack(( - static_label(format!("Frame #{i}")).style(|s| s.flex_grow(1.0)), - static_label(format!("{:.4} ms", frame.sum.as_secs_f64() * 1000.0)) - .style(|s| s.margin_right(16)), - )) - .on_click(move |_| { - selected_frame.set(Some(frame.clone())); - zoom.set(1.0); - true - }) - .style(move |s| { - let selected = selected_frame - .get() - .map(|selected| Rc::ptr_eq(&selected, &frame_)) - .unwrap_or(false); - s.padding(5.0) - .apply_if(selected, |s| s.background(Color::rgb8(213, 208, 216))) - .hover(move |s| { - s.background(Color::rgba8(228, 237, 216, 160)) - .apply_if(selected, |s| s.background(Color::rgb8(186, 180, 216))) - }) - }) - }) - .collect(); - - let hovered_event = create_rw_signal(None); - - let event_tooltip = dyn_container( - move || hovered_event.get(), - move |event: Option| -> Box { - if let Some(event) = event { - let len = event - .end - .saturating_duration_since(event.start) - .as_secs_f64(); - Box::new(v_stack(( - info("Name", event.name.to_string()), - info("Time", format!("{:.4} ms", len * 1000.0)), - ))) - } else { - Box::new(text("No hovered event").style(|s| s.padding(5.0))) - } - }, - ) - .style(|s| s.min_height(50)); - - let frames = v_stack(( - header("Frames"), - scroll(static_list(frames).style(|s| s.width_full())).style(|s| { - s.background(Color::WHITE) - .flex_basis(0) - .min_height(0) - .flex_grow(1.0) - }), - header("Event"), - event_tooltip, - )) - .style(|s| s.max_width_pct(60.0).min_width(200.0)); - - let seperator = empty().style(move |s| { - s.height_full() - .min_width(1.0) - .background(Color::BLACK.with_alpha_factor(0.2)) - }); - - let timeline = dyn_container( - move || selected_frame.get(), - move |frame| { - if let Some(frame) = frame { - let list = frame.events.iter().map(|event| { - let len = event - .end - .saturating_duration_since(event.start) - .as_secs_f64(); - let left = event - .start - .saturating_duration_since(frame.start.unwrap()) - .as_secs_f64() - / frame.duration.as_secs_f64(); - let width = len / frame.duration.as_secs_f64(); - let event_ = event.clone(); - clip( - static_label(format!("{} ({:.4} ms)", event.name, len * 1000.0)) - .style(|s| s.padding(5.0)), - ) - .style(move |s| { - s.min_width(0) - .width_pct(width * 100.0) - .absolute() - .inset_left_pct(left * 100.0) - .border(0.3) - .border_color(Color::rgb8(129, 164, 192)) - .background(Color::rgb8(209, 222, 233).with_alpha_factor(0.6)) - .text_clip() - .hover(|s| { - s.color(Color::WHITE) - .background(Color::BLACK.with_alpha_factor(0.6)) - }) - }) - .on_event(EventListener::PointerEnter, move |_| { - hovered_event.set(Some(event_.clone())); - false - }) - }); - Box::new( - scroll( - static_list(list) - .style(move |s| s.min_width_pct(zoom.get() * 100.0).height_full()), - ) - .style(|s| s.height_full().min_width(0).flex_basis(0).flex_grow(1.0)) - .on_event(EventListener::PointerWheel, move |e| { - if let Event::PointerWheel(e) = e { - zoom.set(zoom.get() * (1.0 - e.delta.y / 400.0)); - true - } else { - false - } - }), - ) - } else { - Box::new(text("No selected frame").style(|s| s.padding(5.0))) - } - }, - ) - .style(|s| { - s.width_full() - .min_height(0) - .flex_basis(0) - .flex_grow(1.0) - .background(Color::WHITE) - }); - - let timeline = v_stack((header("Timeline"), timeline)) - .style(|s| s.min_width(0).flex_basis(0).flex_grow(1.0)); - - h_stack((frames, seperator, timeline)).style(|s| s.height_full().width_full().max_width_full()) -} - -thread_local! { - pub(crate) static PROFILE: RwSignal>> = { - Scope::new().create_rw_signal(None) - }; -} - -pub fn profiler(window_id: WindowId) -> impl View { - let profiling = create_rw_signal(false); - let profile = PROFILE.with(|c| *c); - - let button = h_stack(( - button(move || { - if profiling.get() { - "Stop Profiling" - } else { - "Start Profiling" - } - }) - .on_click(move |_| { - add_app_update_event(AppUpdateEvent::ProfileWindow { - window_id, - end_profile: if profiling.get() { - Some(profile.write_only()) - } else { - None - }, - }); - profiling.set(!profiling.get()); - true - }) - .style(|s| s.margin(5.0)), - label(move || if profiling.get() { "Profiling..." } else { "" }), - )) - .style(|s| s.items_center()); - - let seperator = empty().style(move |s| { - s.width_full() - .min_height(1.0) - .background(Color::BLACK.with_alpha_factor(0.2)) - }); - - let lower = dyn_container( - move || profile.get(), - |profile| { - if let Some(profile) = profile { - Box::new(profile_view(&profile)) - } else { - Box::new(text("No profile").style(|s| s.padding(5.0))) - } - }, - ) - .style(|s| s.width_full().min_height(0).flex_basis(0).flex_grow(1.0)); - - // FIXME: This needs an extra `container` or the `v_stack` ends up horizontal. - container(v_stack((button, seperator, lower)).style(|s| s.width_full().height_full())) - .style(|s| s.width_full().height_full()) - .on_event(EventListener::WindowClosed, move |_| { - if profiling.get() { - add_app_update_event(AppUpdateEvent::ProfileWindow { - window_id, - end_profile: Some(profile.write_only()), - }); - } - false - }) -} +use crate::app::{add_app_update_event, AppUpdateEvent}; +use crate::event::{Event, EventListener}; +use crate::inspector::header; +use crate::view::View; +use crate::views::{ + clip, container, dyn_container, empty, h_stack, label, scroll, stack, static_label, + static_list, text, v_stack, Decorators, +}; +use crate::widgets::button; +use crate::EventPropagation; +use floem_reactive::{create_rw_signal, RwSignal, Scope}; +use peniko::Color; +use std::fmt::Display; +use std::mem; +use std::rc::Rc; +use std::time::{Duration, Instant}; +use taffy::style::FlexDirection; +use winit::window::WindowId; + +#[derive(Clone)] +pub struct ProfileEvent { + pub start: Instant, + pub end: Instant, + pub name: &'static str, +} + +#[derive(Default)] +pub struct ProfileFrame { + pub events: Vec, +} + +#[derive(Default)] +pub struct Profile { + pub current: ProfileFrame, + frames: Vec, +} + +impl Profile { + pub fn next_frame(&mut self) { + self.frames.push(mem::take(&mut self.current)); + } +} + +struct ProfileFrameData { + start: Option, + duration: Duration, + sum: Duration, + events: Vec, +} + +fn info(name: impl Display, value: String) -> impl View { + info_row(name.to_string(), static_label(value)) +} + +fn info_row(name: String, view: impl View + 'static) -> impl View { + stack(( + stack((static_label(name).style(|s| { + s.margin_right(5.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }),)) + .style(|s| s.min_width(80.0).flex_direction(FlexDirection::RowReverse)), + view, + )) + .style(|s| { + s.padding(5.0) + .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) + }) +} + +fn profile_view(profile: &Rc) -> impl View { + let mut frames: Vec<_> = profile + .frames + .iter() + .map(|frame| { + let start = frame.events.first().map(|event| event.start); + let end = frame.events.last().map(|event| event.end); + let sum = frame + .events + .iter() + .map(|event| event.end.saturating_duration_since(event.start)) + .sum(); + let duration = start + .and_then(|start| end.map(|end| end.saturating_duration_since(start))) + .unwrap_or_default(); + Rc::new(ProfileFrameData { + start, + duration, + sum, + events: frame.events.clone(), + }) + }) + .collect(); + frames.sort_by(|a, b| b.sum.cmp(&a.sum)); + + let selected_frame = create_rw_signal(None); + + let zoom = create_rw_signal(1.0); + + let frames: Vec<_> = frames + .iter() + .enumerate() + .map(|(i, frame)| { + let frame = frame.clone(); + let frame_ = frame.clone(); + h_stack(( + static_label(format!("Frame #{i}")).style(|s| s.flex_grow(1.0)), + static_label(format!("{:.4} ms", frame.sum.as_secs_f64() * 1000.0)) + .style(|s| s.margin_right(16)), + )) + .on_click_stop(move |_| { + selected_frame.set(Some(frame.clone())); + zoom.set(1.0); + }) + .style(move |s| { + let selected = selected_frame + .get() + .map(|selected| Rc::ptr_eq(&selected, &frame_)) + .unwrap_or(false); + s.padding(5.0) + .apply_if(selected, |s| s.background(Color::rgb8(213, 208, 216))) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected, |s| s.background(Color::rgb8(186, 180, 216))) + }) + }) + }) + .collect(); + + let hovered_event = create_rw_signal(None); + + let event_tooltip = dyn_container( + move || hovered_event.get(), + move |event: Option| -> Box { + if let Some(event) = event { + let len = event + .end + .saturating_duration_since(event.start) + .as_secs_f64(); + Box::new(v_stack(( + info("Name", event.name.to_string()), + info("Time", format!("{:.4} ms", len * 1000.0)), + ))) + } else { + Box::new(text("No hovered event").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| s.min_height(50)); + + let frames = v_stack(( + header("Frames"), + scroll(static_list(frames).style(|s| s.width_full())).style(|s| { + s.background(Color::WHITE) + .flex_basis(0) + .min_height(0) + .flex_grow(1.0) + }), + header("Event"), + event_tooltip, + )) + .style(|s| s.max_width_pct(60.0).min_width(200.0)); + + let seperator = empty().style(move |s| { + s.height_full() + .min_width(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let timeline = dyn_container( + move || selected_frame.get(), + move |frame| { + if let Some(frame) = frame { + let list = frame.events.iter().map(|event| { + let len = event + .end + .saturating_duration_since(event.start) + .as_secs_f64(); + let left = event + .start + .saturating_duration_since(frame.start.unwrap()) + .as_secs_f64() + / frame.duration.as_secs_f64(); + let width = len / frame.duration.as_secs_f64(); + let event_ = event.clone(); + clip( + static_label(format!("{} ({:.4} ms)", event.name, len * 1000.0)) + .style(|s| s.padding(5.0)), + ) + .style(move |s| { + s.min_width(0) + .width_pct(width * 100.0) + .absolute() + .inset_left_pct(left * 100.0) + .border(0.3) + .border_color(Color::rgb8(129, 164, 192)) + .background(Color::rgb8(209, 222, 233).with_alpha_factor(0.6)) + .text_clip() + .hover(|s| { + s.color(Color::WHITE) + .background(Color::BLACK.with_alpha_factor(0.6)) + }) + }) + .on_event_cont(EventListener::PointerEnter, move |_| { + hovered_event.set(Some(event_.clone())) + }) + }); + Box::new( + scroll( + static_list(list) + .style(move |s| s.min_width_pct(zoom.get() * 100.0).height_full()), + ) + .style(|s| s.height_full().min_width(0).flex_basis(0).flex_grow(1.0)) + .on_event(EventListener::PointerWheel, move |e| { + if let Event::PointerWheel(e) = e { + zoom.set(zoom.get() * (1.0 - e.delta.y / 400.0)); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }), + ) + } else { + Box::new(text("No selected frame").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| { + s.width_full() + .min_height(0) + .flex_basis(0) + .flex_grow(1.0) + .background(Color::WHITE) + }); + + let timeline = v_stack((header("Timeline"), timeline)) + .style(|s| s.min_width(0).flex_basis(0).flex_grow(1.0)); + + h_stack((frames, seperator, timeline)).style(|s| s.height_full().width_full().max_width_full()) +} + +thread_local! { + pub(crate) static PROFILE: RwSignal>> = { + Scope::new().create_rw_signal(None) + }; +} + +pub fn profiler(window_id: WindowId) -> impl View { + let profiling = create_rw_signal(false); + let profile = PROFILE.with(|c| *c); + + let button = h_stack(( + button(move || { + if profiling.get() { + "Stop Profiling" + } else { + "Start Profiling" + } + }) + .on_click_stop(move |_| { + add_app_update_event(AppUpdateEvent::ProfileWindow { + window_id, + end_profile: if profiling.get() { + Some(profile.write_only()) + } else { + None + }, + }); + profiling.set(!profiling.get()); + }) + .style(|s| s.margin(5.0)), + label(move || if profiling.get() { "Profiling..." } else { "" }), + )) + .style(|s| s.items_center()); + + let seperator = empty().style(move |s| { + s.width_full() + .min_height(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let lower = dyn_container( + move || profile.get(), + |profile| { + if let Some(profile) = profile { + Box::new(profile_view(&profile)) + } else { + Box::new(text("No profile").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| s.width_full().min_height(0).flex_basis(0).flex_grow(1.0)); + + // FIXME: This needs an extra `container` or the `v_stack` ends up horizontal. + container(v_stack((button, seperator, lower)).style(|s| s.width_full().height_full())) + .style(|s| s.width_full().height_full()) + .on_event_cont(EventListener::WindowClosed, move |_| { + if profiling.get() { + add_app_update_event(AppUpdateEvent::ProfileWindow { + window_id, + end_profile: Some(profile.write_only()), + }); + } + }) +} diff --git a/src/view.rs b/src/view.rs index d519cca5..674b052c 100644 --- a/src/view.rs +++ b/src/view.rs @@ -93,6 +93,7 @@ use crate::{ event::Event, id::Id, style::{BoxShadowProp, Style, StyleClassRef}, + EventPropagation, }; pub trait View { @@ -206,13 +207,22 @@ pub trait View { /// /// If the event needs other passes to run you're expected to call /// `cx.app_state_mut().request_changes`. - fn event(&mut self, cx: &mut EventCx, id_path: Option<&[Id]>, event: Event) -> bool { + fn event( + &mut self, + cx: &mut EventCx, + id_path: Option<&[Id]>, + event: Event, + ) -> EventPropagation { let mut handled = false; self.for_each_child_rev_mut(&mut |child| { - handled |= cx.view_event(child, id_path, event.clone()); + handled |= cx.view_event(child, id_path, event.clone()).is_processed(); handled }); - handled + if handled { + EventPropagation::Stop + } else { + EventPropagation::Continue + } } /// `View`-specific implementation. Will be called in the [`View::paint_main`] entry point method. @@ -630,7 +640,12 @@ impl View for Box { (**self).compute_layout(cx) } - fn event(&mut self, cx: &mut EventCx, id_path: Option<&[Id]>, event: Event) -> bool { + fn event( + &mut self, + cx: &mut EventCx, + id_path: Option<&[Id]>, + event: Event, + ) -> EventPropagation { (**self).event(cx, id_path, event) } diff --git a/src/views/decorator.rs b/src/views/decorator.rs index d3fb04b1..9a21e11a 100644 --- a/src/views/decorator.rs +++ b/src/views/decorator.rs @@ -8,6 +8,7 @@ use crate::{ menu::Menu, style::{Style, StyleClass, StyleSelector}, view::View, + EventPropagation, }; pub trait Decorators: View + Sized { @@ -107,30 +108,118 @@ pub trait Decorators: View + Sized { self } - fn on_event(self, listener: EventListener, action: impl Fn(&Event) -> bool + 'static) -> Self { + /// Add an event hanlder for the given EventListener + fn on_event( + self, + listener: EventListener, + action: impl Fn(&Event) -> EventPropagation + 'static, + ) -> Self { let id = self.id(); id.update_event_listener(listener, Box::new(action)); self } - fn on_click(self, action: impl Fn(&Event) -> bool + 'static) -> Self { + /// Add an event hanlder for the given EventListener + /// + /// This event will be handled with the given handler and the event will continue propagating + fn on_event_cont(self, listener: EventListener, action: impl Fn(&Event) + 'static) -> Self { + self.on_event(listener, move |e| { + action(e); + EventPropagation::Continue + }) + } + + /// Add an event hanlder for the given EventListener + /// + /// This event will be handled with the given handler and the event will stop propagating + fn on_event_stop(self, listener: EventListener, action: impl Fn(&Event) + 'static) -> Self { + self.on_event(listener, move |e| { + action(e); + EventPropagation::Stop + }) + } + + /// Add an event hanlder for the [Click] Event Listener + fn on_click(self, action: impl Fn(&Event) -> EventPropagation + 'static) -> Self { let id = self.id(); id.update_event_listener(EventListener::Click, Box::new(action)); self } - fn on_double_click(self, action: impl Fn(&Event) -> bool + 'static) -> Self { + /// Add an event hanlder for the [Click] Event Listener + /// + /// This event will be handled with the given handler and the event will continue propagating + fn on_click_cont(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_click(move |e| { + action(e); + EventPropagation::Continue + }) + } + + /// Add an event hanlder for the [Click] Event Listener + /// + /// This event will be handled with the given handler and the event will stop propagating + fn on_click_stop(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_click(move |e| { + action(e); + EventPropagation::Stop + }) + } + + /// Add an event hanlder for the [DoubleClick] Event Listener + fn on_double_click(self, action: impl Fn(&Event) -> EventPropagation + 'static) -> Self { let id = self.id(); id.update_event_listener(EventListener::DoubleClick, Box::new(action)); self } - fn on_secondary_click(self, action: impl Fn(&Event) -> bool + 'static) -> Self { + /// Add an event hanlder for the [DoubleClick] Event Listener + /// + /// This event will be handled with the given handler and the event will continue propagating + fn on_double_click_cont(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_double_click(move |e| { + action(e); + EventPropagation::Continue + }) + } + + /// Add an event hanlder for the [DoubleClick] Event Listener + /// + /// This event will be handled with the given handler and the event will stop propagating + fn on_double_click_stop(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_double_click(move |e| { + action(e); + EventPropagation::Stop + }) + } + + /// Add an event hanlder for the [SecondaryClick] Event Listener. This is most often the "Right" click. + fn on_secondary_click(self, action: impl Fn(&Event) -> EventPropagation + 'static) -> Self { let id = self.id(); id.update_event_listener(EventListener::SecondaryClick, Box::new(action)); self } + /// Add an event hanlder for the [SecondaryClick] Event Listener. This is most often the "Right" click. + /// + /// This event will be handled with the given handler and the event will continue propagating + fn on_secondary_click_cont(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_secondary_click(move |e| { + action(e); + EventPropagation::Continue + }) + } + + /// Add an event hanlder for the [SecondaryClick] Event Listener. This is most often the "Right" click. + /// + /// This event will be handled with the given handler and the event will stop propagating + fn on_secondary_click_stop(self, action: impl Fn(&Event) + 'static) -> Self { + self.on_secondary_click(move |e| { + action(e); + EventPropagation::Stop + }) + } + fn on_resize(self, action: impl Fn(Rect) + 'static) -> Self { let id = self.id(); id.update_resize_listener(Box::new(action)); diff --git a/src/views/drag_resize_window_area.rs b/src/views/drag_resize_window_area.rs index 55d8ccd6..a62b1f5e 100644 --- a/src/views/drag_resize_window_area.rs +++ b/src/views/drag_resize_window_area.rs @@ -20,9 +20,8 @@ pub fn drag_resize_window_area( id, child: Box::new(child), } - .on_event(EventListener::PointerDown, move |_| { - drag_resize_window(direction); - true + .on_event_stop(EventListener::PointerDown, move |_| { + drag_resize_window(direction) }) .base_style(move |s| { let cursor = match direction { diff --git a/src/views/drag_window_area.rs b/src/views/drag_window_area.rs index 329a7576..0788bda8 100644 --- a/src/views/drag_window_area.rs +++ b/src/views/drag_window_area.rs @@ -18,14 +18,8 @@ pub fn drag_window_area(child: V) -> DragWindowArea { id, child: Box::new(child), } - .on_event(EventListener::PointerDown, |_| { - drag_window(); - true - }) - .on_double_click(|_| { - toggle_window_maximized(); - true - }) + .on_event_stop(EventListener::PointerDown, |_| drag_window()) + .on_double_click_stop(|_| toggle_window_maximized()) } impl View for DragWindowArea { diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 74e183c8..2ad4234a 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -13,6 +13,7 @@ use crate::{ style_class, unit::Px, view::View, + EventPropagation, }; enum ScrollState { @@ -722,7 +723,7 @@ impl View for Scroll { cx: &mut crate::context::EventCx, id_path: Option<&[Id]>, event: crate::event::Event, - ) -> bool { + ) -> EventPropagation { let viewport_size = self.child_viewport.size(); let scroll_offset = self.child_viewport.origin().to_vec2(); let content_size = self.child_size; @@ -744,7 +745,7 @@ impl View for Scroll { cx.update_active(self.id); // Force a repaint. cx.request_paint(self.id); - return true; + return EventPropagation::Stop; } self.click_vertical_bar_area(cx.app_state, event.pos); let scroll_offset = self.child_viewport.origin().to_vec2(); @@ -754,7 +755,7 @@ impl View for Scroll { scroll_offset, ); cx.update_active(self.id); - return true; + return EventPropagation::Stop; } else if self.point_within_horizontal_bar(cx.app_state, pos) { if self.point_hits_horizontal_bar(cx.app_state, pos) { self.held = BarHeldState::Horizontal( @@ -765,7 +766,7 @@ impl View for Scroll { cx.update_active(self.id); // Force a repaint. cx.request_paint(self.id); - return true; + return EventPropagation::Stop; } self.click_horizontal_bar_area(cx.app_state, event.pos); let scroll_offset = self.child_viewport.origin().to_vec2(); @@ -775,7 +776,7 @@ impl View for Scroll { scroll_offset, ); cx.update_active(self.id); - return true; + return EventPropagation::Stop; } } } @@ -816,7 +817,7 @@ impl View for Scroll { } else if self.point_within_vertical_bar(cx.app_state, pos) || self.point_within_horizontal_bar(cx.app_state, pos) { - return false; + return EventPropagation::Continue; } } } @@ -830,15 +831,18 @@ impl View for Scroll { _ => {} } - if cx.view_event(&mut self.child, id_path, event.clone()) { - return true; + if cx + .view_event(&mut self.child, id_path, event.clone()) + .is_processed() + { + return EventPropagation::Stop; } if let Event::PointerWheel(pointer_event) = &event { if let Some(listener) = event.listener() { if let Some(action) = cx.get_event_listener(self.id, &listener) { - if (*action)(&event) { - return true; + if (*action)(&event).is_processed() { + return EventPropagation::Stop; } } } @@ -853,10 +857,14 @@ impl View for Scroll { // Check if the scroll bars now hover self.update_hover_states(cx.app_state, pointer_event.pos); - return !self.propagate_pointer_wheel; + return if !self.propagate_pointer_wheel { + EventPropagation::Stop + } else { + EventPropagation::Continue + }; } - false + EventPropagation::Continue } fn paint(&mut self, cx: &mut crate::context::PaintCx) { diff --git a/src/views/text_input.rs b/src/views/text_input.rs index 976127d5..6e4e309f 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1,10 +1,10 @@ use crate::action::exec_after; use crate::keyboard::{self, KeyEvent}; -use crate::prop_extracter; use crate::reactive::{create_effect, RwSignal}; use crate::style::{CursorStyle, TextColor}; use crate::style::{FontProps, PaddingLeft}; use crate::unit::PxPct; +use crate::{prop_extracter, EventPropagation}; use clipboard::{ClipboardContext, ClipboardProvider}; use taffy::prelude::{Layout, Node}; @@ -730,7 +730,12 @@ impl View for TextInput { } } - fn event(&mut self, cx: &mut EventCx, _id_path: Option<&[Id]>, event: Event) -> bool { + fn event( + &mut self, + cx: &mut EventCx, + _id_path: Option<&[Id]>, + event: Event, + ) -> EventPropagation { let is_handled = match &event { Event::PointerDown(event) => { if !self.is_focused { @@ -767,7 +772,7 @@ impl View for TextInput { Event::PointerMove(_) => { if !matches!(cx.app_state.cursor, Some(CursorStyle::Text)) { cx.app_state.cursor = Some(CursorStyle::Text); - return false; + return EventPropagation::Continue; } false } @@ -779,7 +784,7 @@ impl View for TextInput { self.last_cursor_action_on = Instant::now(); } - false + EventPropagation::Continue } fn style(&mut self, cx: &mut crate::context::StyleCx<'_>) { diff --git a/src/widgets/toggle_button.rs b/src/widgets/toggle_button.rs index ebb2a9a5..a51d1800 100644 --- a/src/widgets/toggle_button.rs +++ b/src/widgets/toggle_button.rs @@ -12,6 +12,7 @@ use crate::{ unit::PxPct, view::View, views::Decorators, + EventPropagation, }; /// Controls the switching behavior of the switch. The cooresponding style prop is [ToggleButtonBehavior] @@ -117,7 +118,7 @@ impl View for ToggleButton { cx: &mut crate::context::EventCx, _id_path: Option<&[crate::id::Id]>, event: crate::event::Event, - ) -> bool { + ) -> EventPropagation { match event { crate::event::Event::PointerDown(_event) => { cx.update_active(self.id); @@ -202,7 +203,7 @@ impl View for ToggleButton { } _ => {} }; - false + EventPropagation::Continue } fn compute_layout(&mut self, cx: &mut crate::context::LayoutCx) -> Option { @@ -251,6 +252,11 @@ impl ToggleButton { .max(self.radius + inset) .min(self.width - self.radius - inset); } + + /// Add an event handler to be run when the button is toggled. + /// + ///This does not run if the state is changed because of an outside signal. + /// This handler is only called if this button is clicked or switched pub fn on_toggle(mut self, ontoggle: impl Fn(bool) + 'static) -> Self { self.ontoggle = Some(Box::new(ontoggle)); self diff --git a/src/window_handle.rs b/src/window_handle.rs index 8d072c3f..e186cce5 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -177,11 +177,13 @@ impl WindowHandle { if let Some(id) = cx.app_state.focus { let id_path = ID_PATHS.with(|paths| paths.borrow().get(&id).cloned()); if let Some(id_path) = id_path { - processed |= cx.unconditional_view_event( - &mut self.view, - Some(id_path.dispatch()), - event.clone(), - ); + processed |= cx + .unconditional_view_event( + &mut self.view, + Some(id_path.dispatch()), + event.clone(), + ) + .is_processed(); } else { cx.app_state.focus = None; } @@ -190,7 +192,7 @@ impl WindowHandle { if !processed { if let Some(listener) = event.listener() { if let Some(action) = cx.get_event_listener(self.view.id(), &listener) { - processed |= (*action)(&event); + processed |= (*action)(&event).is_processed(); } } } @@ -1275,17 +1277,15 @@ fn context_menu_view( .apply_if(!has_submenu, |s| s.hide()) }), )) - .on_event(EventListener::PointerEnter, move |_| { + .on_event_stop(EventListener::PointerEnter, move |_| { if has_submenu { show_submenu.set(true); } - true }) - .on_event(EventListener::PointerLeave, move |_| { + .on_event_stop(EventListener::PointerLeave, move |_| { if has_submenu { show_submenu.set(false); } - true }) .on_resize(move |rect| { let width = rect.width(); @@ -1293,7 +1293,7 @@ fn context_menu_view( menu_width.set(width); } }) - .on_click(move |_| { + .on_click_stop(move |_| { context_menu.set(None); focus_count.set(0); if let Some(id) = menu.id { @@ -1302,9 +1302,8 @@ fn context_menu_view( action_id: id as usize, }); } - true }) - .on_secondary_click(move |_| { + .on_secondary_click_stop(move |_| { context_menu.set(None); focus_count.set(0); if let Some(id) = menu.id { @@ -1313,7 +1312,6 @@ fn context_menu_view( action_id: id as usize, }); } - true }) .disabled(move || !menu.enabled) .style(|s| { @@ -1334,13 +1332,12 @@ fn context_menu_view( }, ) .keyboard_navigatable() - .on_event(EventListener::FocusGained, move |_| { + .on_event_stop(EventListener::FocusGained, move |_| { focus_count.update(|count| { *count += 1; }); - true }) - .on_event(EventListener::FocusLost, move |_| { + .on_event_stop(EventListener::FocusLost, move |_| { let count = focus_count .try_update(|count| { *count -= 1; @@ -1350,30 +1347,26 @@ fn context_menu_view( if count < 1 { context_menu.set(None); } - true }) - .on_event(EventListener::KeyDown, move |event| { + .on_event_stop(EventListener::KeyDown, move |event| { if let Event::KeyDown(event) = event { if event.key.logical_key == Key::Named(NamedKey::Escape) { context_menu.set(None); } } - true }) - .on_event(EventListener::PointerDown, move |_| true) - .on_event(EventListener::PointerEnter, move |_| { + .on_event_stop(EventListener::PointerDown, move |_| {}) + .on_event_stop(EventListener::PointerEnter, move |_| { if has_submenu { on_submenu.set(true); on_child_submenu_for_parent.set(true); } - true }) - .on_event(EventListener::PointerLeave, move |_| { + .on_event_stop(EventListener::PointerLeave, move |_| { if has_submenu { on_submenu.set(false); on_child_submenu_for_parent.set(false); } - true }) .style(move |s| { s.absolute() @@ -1416,23 +1409,21 @@ fn context_menu_view( .on_resize(move |rect| { context_menu_size.set(rect.size()); }) - .on_event(EventListener::PointerDown, move |_| true) + .on_event_stop(EventListener::PointerDown, move |_| {}) .keyboard_navigatable() - .on_event(EventListener::KeyDown, move |event| { + .on_event_stop(EventListener::KeyDown, move |event| { if let Event::KeyDown(event) = event { if event.key.logical_key == Key::Named(NamedKey::Escape) { context_menu.set(None); } } - true }) - .on_event(EventListener::FocusGained, move |_| { + .on_event_stop(EventListener::FocusGained, move |_| { focus_count.update(|count| { *count += 1; }); - true }) - .on_event(EventListener::FocusLost, move |_| { + .on_event_stop(EventListener::FocusLost, move |_| { let count = focus_count .try_update(|count| { *count -= 1; @@ -1442,7 +1433,6 @@ fn context_menu_view( if count < 1 { context_menu.set(None); } - true }) .style(move |s| { let window_size = window_size.get();