diff --git a/crates/op-editor-core/src/editor_ui_state.rs b/crates/op-editor-core/src/editor_ui_state.rs index 907fce124..fa07d5592 100644 --- a/crates/op-editor-core/src/editor_ui_state.rs +++ b/crates/op-editor-core/src/editor_ui_state.rs @@ -896,8 +896,8 @@ pub struct EditorUiState { /// Last-selected shape tool — drives the toolbar shape slot's /// icon. Always one of Rect / Ellipse / Polygon / Line / Pen. pub shape_tool: Tool, - /// Whether the Toolbar's Icon action picker is open. - pub icon_picker_open: bool, + /// Toolbar/property icon picker interaction state. + pub icon_picker: jian_widgets::components::select::SelectState, /// True when the icon picker should replace the selected icon /// instead of inserting a new icon at the canvas centre. pub icon_picker_replace_selection: bool, @@ -909,9 +909,6 @@ pub struct EditorUiState { /// True after Cmd/Ctrl+A in the icon search box. The next edit /// replaces the whole search query. pub icon_picker_select_all: bool, - /// Which icon-picker target the cursor is over (close / row / - /// load-more) — drives the `theme.button_hover` wash. - pub icon_picker_hover: Option, /// Remote Iconify search results appended by the desktop host. pub icon_picker_remote: crate::icon_picker_state::IconPickerRemoteState, /// Queued "load more" request drained asynchronously by desktop. @@ -1227,12 +1224,11 @@ impl Default for EditorUiState { shape_picker: jian_widgets::components::select::SelectState::default(), toolbar_hover: None, shape_tool: Tool::Rect, - icon_picker_open: false, + icon_picker: jian_widgets::components::select::SelectState::default(), icon_picker_replace_selection: false, icon_picker_panel_pos: None, icon_picker_search: String::new(), icon_picker_select_all: false, - icon_picker_hover: None, icon_picker_remote: crate::icon_picker_state::IconPickerRemoteState::default(), icon_picker_load_more_request: None, chat_model_picker: jian_widgets::components::select::SelectState::default(), @@ -1352,6 +1348,30 @@ impl EditorUiState { changed } + pub fn open_icon_picker(&mut self, replace_selection: bool) { + self.close_icon_picker(); + self.icon_picker.open = true; + self.icon_picker_replace_selection = replace_selection; + } + + pub fn close_icon_picker(&mut self) -> bool { + let changed = self.icon_picker.open + || self.icon_picker.hover.is_some() + || self.icon_picker.pressed.is_some() + || self.icon_picker.scroll.offset != 0.0 + || self.icon_picker_replace_selection + || !self.icon_picker_search.is_empty() + || self.icon_picker_select_all; + self.icon_picker.open = false; + self.icon_picker.hover = None; + self.icon_picker.pressed = None; + self.icon_picker.scroll.offset = 0.0; + self.icon_picker_replace_selection = false; + self.icon_picker_search.clear(); + self.icon_picker_select_all = false; + changed + } + pub fn toggle_font_picker(&mut self) { let opening = !self.font_picker.open; self.close_font_picker(); @@ -1703,4 +1723,35 @@ mod tests { assert_eq!(ui.chat_model_picker.pressed, None); assert_eq!(ui.chat_model_picker.scroll.offset, 0.0); } + + #[test] + fn icon_picker_helpers_reset_select_interaction_state_and_search() { + let mut ui = EditorUiState::new(); + ui.icon_picker_replace_selection = true; + ui.icon_picker_search = "home".to_string(); + ui.icon_picker_select_all = true; + ui.icon_picker.hover = Some(1); + ui.icon_picker.pressed = Some(1); + + ui.open_icon_picker(false); + assert!(ui.icon_picker.open); + assert!(!ui.icon_picker_replace_selection); + assert!(ui.icon_picker_search.is_empty()); + assert!(!ui.icon_picker_select_all); + assert_eq!(ui.icon_picker.hover, None); + assert_eq!(ui.icon_picker.pressed, None); + + ui.icon_picker_replace_selection = true; + ui.icon_picker_search = "settings".to_string(); + ui.icon_picker_select_all = true; + ui.icon_picker.hover = Some(0); + ui.icon_picker.pressed = Some(0); + assert!(ui.close_icon_picker()); + assert!(!ui.icon_picker.open); + assert!(!ui.icon_picker_replace_selection); + assert!(ui.icon_picker_search.is_empty()); + assert!(!ui.icon_picker_select_all); + assert_eq!(ui.icon_picker.hover, None); + assert_eq!(ui.icon_picker.pressed, None); + } } diff --git a/crates/op-editor-core/src/icon_picker_button_state.rs b/crates/op-editor-core/src/icon_picker_button_state.rs deleted file mode 100644 index 6b6f7d7ef..000000000 --- a/crates/op-editor-core/src/icon_picker_button_state.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! State-layer mirror of the icon-picker panel's buttons. -//! -//! The panel's click enum (`IconPickerHit`) carries owned `String` -//! collection/name ids for a row select, so it isn't `Copy`. The row -//! list is index-addressable (paint + hit-test share the same row -//! order), so `Row(usize)` captures the hovered row without strings. -//! Same wasm32-clean discipline as the other `*_state` mirrors. - -/// Which icon-picker target the cursor is over. `None` = no hover wash. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IconPickerButton { - /// The header `✕` close button. - Close, - /// An icon row, by its index into the visible list. - Row(usize), - /// The "load more" row at the foot of a remote result set. - LoadMore, -} diff --git a/crates/op-editor-core/src/lib.rs b/crates/op-editor-core/src/lib.rs index c4380864d..7b4b9ecb3 100644 --- a/crates/op-editor-core/src/lib.rs +++ b/crates/op-editor-core/src/lib.rs @@ -42,7 +42,6 @@ pub mod git_button_state; pub mod grouping; pub mod history; pub mod host_support; -pub mod icon_picker_button_state; pub mod icon_picker_state; pub mod image_node_props; pub mod image_panel_state; @@ -173,7 +172,6 @@ pub use fills::{ pub use geometry::{aggregate_bounds, own_bounds, union_aggregate_bounds, DocRect}; pub use git_button_state::GitButton; pub use history::{EditorSnapshot, History, HISTORY_CAP}; -pub use icon_picker_button_state::IconPickerButton; pub use icon_picker_state::{IconPickerRemoteIcon, IconPickerRemoteState, IconifyLoadMoreRequest}; pub use image_node_props::image_node_summary; pub use instance_override::{ diff --git a/crates/op-editor-ui/src/widgets/icon_picker_panel.rs b/crates/op-editor-ui/src/widgets/icon_picker_panel.rs index 080423438..07131bab9 100644 --- a/crates/op-editor-ui/src/widgets/icon_picker_panel.rs +++ b/crates/op-editor-ui/src/widgets/icon_picker_panel.rs @@ -11,6 +11,8 @@ use op_editor_core::{EditorState, IconPickerRemoteIcon, Locale}; pub const ICON_PICKER_PANEL_W: f32 = 320.0; pub const ICON_PICKER_PANEL_H: f32 = 420.0; +pub const ICON_PICKER_CLOSE_HOVER: usize = usize::MAX - 1; +pub const ICON_PICKER_LOAD_MORE_HOVER: usize = usize::MAX; const PAD: f32 = 14.0; const HEADER_H: f32 = 40.0; const SEARCH_H: f32 = 42.0; @@ -57,36 +59,31 @@ pub struct IconPickerPanel<'a> { theme: Theme, locale: Locale, /// Which target the cursor is over — drives the hover wash. - hover: Option, + hover: Option, } impl<'a> IconPickerPanel<'a> { pub fn for_editor(state: &'a EditorState) -> Option> { - if !state.editor_ui.icon_picker_open { + if !state.editor_ui.icon_picker.open { return None; } Some(IconPickerPanel { state, theme: theme_for(&state.editor_ui), locale: state.editor_ui.locale, - hover: state.editor_ui.icon_picker_hover, + hover: state.editor_ui.icon_picker.hover, }) } /// Resolve a pointer to a hoverable target (close / row / load-more). /// Mirrors [`Self::hit_test`]'s row math; rows are index-addressable /// so no string ids leak into the hover state. - pub fn hover_at( - &self, - panel: Rect, - point: Point2D, - ) -> Option { - use op_editor_core::IconPickerButton as B; + pub fn hover_at(&self, panel: Rect, point: Point2D) -> Option { if !(panel).contains(point) { return None; } if (Self::close_rect(panel)).contains(point) { - return Some(B::Close); + return Some(ICON_PICKER_CLOSE_HOVER); } if point.y <= panel.origin.y + HEADER_H { return None; @@ -99,10 +96,10 @@ impl<'a> IconPickerPanel<'a> { let item_cap = capacity.saturating_sub(usize::from(has_more)); let items = self.rows(item_cap); if row < items.len() { - return Some(B::Row(row)); + return Some(row); } if has_more && row == items.len() && !self.state.editor_ui.icon_picker_remote.loading { - return Some(B::LoadMore); + return Some(ICON_PICKER_LOAD_MORE_HOVER); } } None @@ -245,7 +242,7 @@ impl<'a> IconPickerPanel<'a> { Point2D::new(panel.origin.x + PAD, panel.origin.y + 25.0), ); let close = Self::close_rect(panel); - let close_hovered = self.hover == Some(op_editor_core::IconPickerButton::Close); + let close_hovered = self.hover == Some(ICON_PICKER_CLOSE_HOVER); if close_hovered { cx.backend .fill_round_rect(close, 6.0, self.theme.button_hover); @@ -332,7 +329,7 @@ impl<'a> IconPickerPanel<'a> { size: Point2D::new(panel.size.x - 12.0, ROW_H), }; cx.backend.fill_round_rect(row, 6.0, self.theme.popover); - if self.hover == Some(op_editor_core::IconPickerButton::Row(idx)) { + if self.hover == Some(idx) { cx.backend .fill_round_rect(row, 6.0, self.theme.button_hover); } @@ -381,7 +378,7 @@ impl<'a> IconPickerPanel<'a> { size: Point2D::new(panel.size.x - 12.0, ROW_H - 4.0), }; cx.backend.fill_round_rect(row, 6.0, self.theme.muted); - if self.hover == Some(op_editor_core::IconPickerButton::LoadMore) { + if self.hover == Some(ICON_PICKER_LOAD_MORE_HOVER) { cx.backend .fill_round_rect(row, 6.0, self.theme.button_hover); } @@ -420,3 +417,35 @@ fn truncate(text: &str, max_chars: usize) -> String { out.push_str("..."); out } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hover_at_uses_select_state_indices_for_close_and_rows() { + let mut state = EditorState::default(); + state.editor_ui.open_icon_picker(false); + let panel = IconPickerPanel::for_editor(&state).unwrap(); + let rect = Rect::xywh(0.0, 0.0, ICON_PICKER_PANEL_W, ICON_PICKER_PANEL_H); + let close = IconPickerPanel::close_rect(rect); + + assert_eq!( + panel.hover_at( + rect, + Point2D::new(close.origin.x + close.size.x / 2.0, close.origin.y + 4.0) + ), + Some(ICON_PICKER_CLOSE_HOVER) + ); + assert_eq!( + panel.hover_at( + rect, + Point2D::new( + rect.origin.x + 20.0, + IconPickerPanel::list_top(rect) + ROW_H / 2.0 + ) + ), + Some(0) + ); + } +} diff --git a/crates/op-host-native/src/widget_host/icon_picker_press.rs b/crates/op-host-native/src/widget_host/icon_picker_press.rs index 5997b8899..b01a95730 100644 --- a/crates/op-host-native/src/widget_host/icon_picker_press.rs +++ b/crates/op-host-native/src/widget_host/icon_picker_press.rs @@ -24,10 +24,7 @@ impl WidgetHostNative { }; match hit { IconPickerHit::Close => { - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_hover = None; + self.editor_state.editor_ui.close_icon_picker(); } IconPickerHit::DragHeader => { self.icon_picker_drag = Some(IconPickerDragState { @@ -84,10 +81,7 @@ impl WidgetHostNative { doc.x as f64, doc.y as f64, ); - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_hover = None; + self.editor_state.editor_ui.close_icon_picker(); if inserted.is_some() { self.mark_dirty(); } diff --git a/crates/op-host-native/src/widget_host/ime.rs b/crates/op-host-native/src/widget_host/ime.rs index 75e96b311..e81db8f0d 100644 --- a/crates/op-host-native/src/widget_host/ime.rs +++ b/crates/op-host-native/src/widget_host/ime.rs @@ -32,7 +32,7 @@ impl WidgetHostNative { || ui.variable_row_focus.is_some() || self.editor_state.ui.property_focus.is_some() || self.editor_state.chat.focused - || ui.icon_picker_open + || ui.icon_picker.open || ui.component_browser_open } diff --git a/crates/op-host-native/src/widget_host/input.rs b/crates/op-host-native/src/widget_host/input.rs index 5e06b1bc7..f783c533d 100644 --- a/crates/op-host-native/src/widget_host/input.rs +++ b/crates/op-host-native/src/widget_host/input.rs @@ -38,7 +38,7 @@ impl WidgetHostNative { || self.variables_search_active() || self.editor_state.editor_ui.effect_param_focus.is_some() || self.editor_state.editor_ui.agent_settings.focus.is_some() - || self.editor_state.editor_ui.icon_picker_open + || self.editor_state.editor_ui.icon_picker.open || self.editor_state.editor_ui.chat_model_picker.open || self.editor_state.editor_ui.component_browser_open || self.editor_state.chat.focused @@ -472,15 +472,15 @@ impl WidgetHostNative { return true; } // Icon-picker panel hover (close / icon rows / load-more). - if self.editor_state.editor_ui.icon_picker_open { + if self.editor_state.editor_ui.icon_picker.open { use op_editor_ui::widgets::icon_picker_panel::IconPickerPanel; if let Some(panel_rect) = self.icon_picker_panel_rect(self.last_viewport_w, self.last_viewport_h) { let new_hover = IconPickerPanel::for_editor(&self.editor_state) .and_then(|p| p.hover_at(panel_rect, Point2D::new(x, y))); - if new_hover != self.editor_state.editor_ui.icon_picker_hover { - self.editor_state.editor_ui.icon_picker_hover = new_hover; + if new_hover != self.editor_state.editor_ui.icon_picker.hover { + self.editor_state.editor_ui.icon_picker.hover = new_hover; self.mark_dirty(); return true; } diff --git a/crates/op-host-native/src/widget_host/input_tests.rs b/crates/op-host-native/src/widget_host/input_tests.rs index c3f5e5f6c..8371d6e6c 100644 --- a/crates/op-host-native/src/widget_host/input_tests.rs +++ b/crates/op-host-native/src/widget_host/input_tests.rs @@ -1824,13 +1824,13 @@ fn shape_picker_icon_row_opens_icon_picker() { assert!(host.apply_press(x, icon_row_y, viewport_w, viewport_h)); assert!(!host.editor_state().editor_ui.shape_picker.open); - assert!(host.editor_state().editor_ui.icon_picker_open); + assert!(host.editor_state().editor_ui.icon_picker.open); } #[test] fn icon_picker_open_owns_keyboard_search() { let mut host = WidgetHostNative::new(); - host.editor_state_mut().editor_ui.icon_picker_open = true; + host.editor_state_mut().editor_ui.icon_picker.open = true; assert!(host.input_active_pub()); assert!(host.apply_text('h')); @@ -1839,7 +1839,7 @@ fn icon_picker_open_owns_keyboard_search() { assert!(host.apply_backspace()); assert_eq!(host.editor_state().editor_ui.icon_picker_search, "h"); assert!(host.apply_escape()); - assert!(!host.editor_state().editor_ui.icon_picker_open); + assert!(!host.editor_state().editor_ui.icon_picker.open); } #[test] @@ -1847,7 +1847,7 @@ fn icon_picker_click_inserts_icon_font_node() { let mut host = WidgetHostNative::new(); let viewport_w = 1440.0; let viewport_h = 900.0; - host.editor_state_mut().editor_ui.icon_picker_open = true; + host.editor_state_mut().editor_ui.icon_picker.open = true; host.editor_state_mut().editor_ui.icon_picker_search = "home".to_string(); let panel = host @@ -1856,7 +1856,7 @@ fn icon_picker_click_inserts_icon_font_node() { let row_y = panel.origin.y + 40.0 + 42.0 + 20.0; assert!(host.apply_press(panel.origin.x + 40.0, row_y, viewport_w, viewport_h)); - assert!(!host.editor_state().editor_ui.icon_picker_open); + assert!(!host.editor_state().editor_ui.icon_picker.open); let icon = host .editor_state() .doc @@ -1877,7 +1877,7 @@ fn icon_picker_header_drag_moves_the_panel() { let mut host = WidgetHostNative::new(); let viewport_w = 1440.0; let viewport_h = 900.0; - host.editor_state_mut().editor_ui.icon_picker_open = true; + host.editor_state_mut().editor_ui.icon_picker.open = true; let start = host .icon_picker_panel_rect(viewport_w, viewport_h) diff --git a/crates/op-host-native/src/widget_host/instance_panel_tests.rs b/crates/op-host-native/src/widget_host/instance_panel_tests.rs index 690e76253..e0e7cf0e8 100644 --- a/crates/op-host-native/src/widget_host/instance_panel_tests.rs +++ b/crates/op-host-native/src/widget_host/instance_panel_tests.rs @@ -178,7 +178,7 @@ fn context_menu_detach_component_sheds_reusable_flag() { fn remote_icon_insert_bakes_svg_d_as_path_node() { let mut host = seeded_host(); let state = host.editor_state_mut(); - state.editor_ui.icon_picker_open = true; + state.editor_ui.icon_picker.open = true; state.editor_ui.icon_picker_replace_selection = false; state.editor_ui.icon_picker_panel_pos = Some((0.0, 0.0)); state.editor_ui.icon_picker_search = "zwxq".to_string(); diff --git a/crates/op-host-native/src/widget_host/keyboard.rs b/crates/op-host-native/src/widget_host/keyboard.rs index 95ae1c8c7..23c9e920b 100644 --- a/crates/op-host-native/src/widget_host/keyboard.rs +++ b/crates/op-host-native/src/widget_host/keyboard.rs @@ -341,12 +341,16 @@ impl WidgetHostNative { if self.apply_image_panel_text(c) { return true; } - if self.editor_state.editor_ui.icon_picker_open && !c.is_control() { + if self.editor_state.editor_ui.icon_picker.open && !c.is_control() { if self.editor_state.editor_ui.icon_picker_select_all { self.editor_state.editor_ui.icon_picker_search.clear(); self.editor_state.editor_ui.icon_picker_select_all = false; + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; } self.editor_state.editor_ui.icon_picker_search.push(c); + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); return true; } @@ -644,10 +648,12 @@ impl WidgetHostNative { if self.apply_image_panel_backspace() { return true; } - if self.editor_state.editor_ui.icon_picker_open { + if self.editor_state.editor_ui.icon_picker.open { if self.editor_state.editor_ui.icon_picker_select_all { self.editor_state.editor_ui.icon_picker_search.clear(); self.editor_state.editor_ui.icon_picker_select_all = false; + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); return true; } @@ -658,6 +664,8 @@ impl WidgetHostNative { .pop() .is_some() { + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); return true; } @@ -859,7 +867,7 @@ impl WidgetHostNative { // would silently drop the node behind the focused field. if self.editor_state.ui.property_focus.is_some() || self.editor_state.editor_ui.effect_param_focus.is_some() - || self.editor_state.editor_ui.icon_picker_open + || self.editor_state.editor_ui.icon_picker.open || self.editor_state.editor_ui.chat_model_picker.open || self.editor_state.editor_ui.component_browser_open || self.editor_state.chat.focused @@ -1493,12 +1501,8 @@ impl WidgetHostNative { self.mark_dirty(); return true; } - if self.editor_state.editor_ui.icon_picker_open { - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_select_all = false; - self.editor_state.editor_ui.icon_picker_hover = None; + if self.editor_state.editor_ui.icon_picker.open { + self.editor_state.editor_ui.close_icon_picker(); self.mark_dirty(); return true; } diff --git a/crates/op-host-native/src/widget_host/overlay_rects.rs b/crates/op-host-native/src/widget_host/overlay_rects.rs index fc17899df..a1c93c682 100644 --- a/crates/op-host-native/src/widget_host/overlay_rects.rs +++ b/crates/op-host-native/src/widget_host/overlay_rects.rs @@ -190,7 +190,7 @@ impl WidgetHostNative { viewport_w: f32, viewport_h: f32, ) -> Option { - if !self.editor_state.editor_ui.icon_picker_open { + if !self.editor_state.editor_ui.icon_picker.open { return None; } let ui = &self.editor_state.editor_ui; diff --git a/crates/op-host-native/src/widget_host/property_dispatch.rs b/crates/op-host-native/src/widget_host/property_dispatch.rs index 8789a0e36..1180508de 100644 --- a/crates/op-host-native/src/widget_host/property_dispatch.rs +++ b/crates/op-host-native/src/widget_host/property_dispatch.rs @@ -162,9 +162,7 @@ impl WidgetHostNative { } A::OpenSelectedIconPicker => { let ui = &mut self.editor_state.editor_ui; - ui.icon_picker_open = true; - ui.icon_picker_replace_selection = true; - ui.icon_picker_search.clear(); + ui.open_icon_picker(true); ui.close_fill_type_picker(); ui.image_fill_popover_open = false; ui.close_font_picker(); diff --git a/crates/op-host-native/src/widget_host/shape_picker_press.rs b/crates/op-host-native/src/widget_host/shape_picker_press.rs index 1488748a6..0b04f9d73 100644 --- a/crates/op-host-native/src/widget_host/shape_picker_press.rs +++ b/crates/op-host-native/src/widget_host/shape_picker_press.rs @@ -31,9 +31,7 @@ impl WidgetHostNative { self.editor_state.tool = tool; } ShapeChoice::OpenIconPicker => { - self.editor_state.editor_ui.icon_picker_open = true; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); + self.editor_state.editor_ui.open_icon_picker(false); } ShapeChoice::ImportImageOrSvg => { self.editor_state.editor_ui.pending_file_action = diff --git a/crates/op-host-native/src/widget_host/shortcuts.rs b/crates/op-host-native/src/widget_host/shortcuts.rs index d85f19983..cc7151430 100644 --- a/crates/op-host-native/src/widget_host/shortcuts.rs +++ b/crates/op-host-native/src/widget_host/shortcuts.rs @@ -205,7 +205,7 @@ impl WidgetHostNative { self.mark_dirty(); return true; } - if self.editor_state.editor_ui.icon_picker_open { + if self.editor_state.editor_ui.icon_picker.open { self.editor_state.editor_ui.icon_picker_select_all = true; self.mark_dirty(); return true; diff --git a/crates/op-host-web/src/a11y.rs b/crates/op-host-web/src/a11y.rs index 28f768871..a33191b7b 100644 --- a/crates/op-host-web/src/a11y.rs +++ b/crates/op-host-web/src/a11y.rs @@ -535,7 +535,7 @@ fn focused_input(state: &EditorState) -> Option<(String, String, String)> { state.ui.property_input.text().to_owned(), )); } - if ui.icon_picker_open { + if ui.icon_picker.open { return Some(( "icon-search".to_string(), "Icon search".to_string(), @@ -698,7 +698,7 @@ mod tests { #[test] fn focused_input_reports_open_search_popovers() { let mut state = EditorState::new(); - state.editor_ui.icon_picker_open = true; + state.editor_ui.icon_picker.open = true; state.editor_ui.icon_picker_search = "arrow".to_string(); let (key, _label, value) = focused_input(&state).expect("icon search"); assert_eq!(key, "icon-search"); diff --git a/crates/op-host-web/src/widget_host/icon_picker_press.rs b/crates/op-host-web/src/widget_host/icon_picker_press.rs index 3e79ac4f9..e296c44fd 100644 --- a/crates/op-host-web/src/widget_host/icon_picker_press.rs +++ b/crates/op-host-web/src/widget_host/icon_picker_press.rs @@ -33,10 +33,7 @@ impl WidgetHost { }; match hit { IconPickerHit::Close => { - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_hover = None; + self.editor_state.editor_ui.close_icon_picker(); } IconPickerHit::DragHeader => { self.icon_picker_drag = Some(PanelDragState { @@ -93,10 +90,7 @@ impl WidgetHost { doc.x as f64, doc.y as f64, ); - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_hover = None; + self.editor_state.editor_ui.close_icon_picker(); if inserted.is_some() { self.mark_dirty(); } diff --git a/crates/op-host-web/src/widget_host/keyboard.rs b/crates/op-host-web/src/widget_host/keyboard.rs index 69f3d5306..f6e7399f5 100644 --- a/crates/op-host-web/src/widget_host/keyboard.rs +++ b/crates/op-host-web/src/widget_host/keyboard.rs @@ -688,12 +688,8 @@ impl WidgetHost { self.mark_dirty(); return true; } - if self.editor_state.editor_ui.icon_picker_open { - self.editor_state.editor_ui.icon_picker_open = false; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); - self.editor_state.editor_ui.icon_picker_select_all = false; - self.editor_state.editor_ui.icon_picker_hover = None; + if self.editor_state.editor_ui.icon_picker.open { + self.editor_state.editor_ui.close_icon_picker(); self.mark_dirty(); return true; } diff --git a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs index a38c172e7..0f141829b 100644 --- a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs +++ b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs @@ -135,7 +135,7 @@ impl WidgetHost { self.mark_dirty(); return true; } - if self.editor_state.editor_ui.icon_picker_open { + if self.editor_state.editor_ui.icon_picker.open { self.editor_state.editor_ui.icon_picker_select_all = true; self.mark_dirty(); return true; diff --git a/crates/op-host-web/src/widget_host/keyboard_text_inputs.rs b/crates/op-host-web/src/widget_host/keyboard_text_inputs.rs index 2d0014085..214568c30 100644 --- a/crates/op-host-web/src/widget_host/keyboard_text_inputs.rs +++ b/crates/op-host-web/src/widget_host/keyboard_text_inputs.rs @@ -25,7 +25,7 @@ impl WidgetHost { .is_some() || self.variables_search_active() || self.editor_state.editor_ui.agent_settings.focus.is_some() - || self.editor_state.editor_ui.icon_picker_open + || self.editor_state.editor_ui.icon_picker.open || self.editor_state.editor_ui.chat_model_picker.open || self.editor_state.editor_ui.component_browser_open || self.editor_state.chat.focused diff --git a/crates/op-host-web/src/widget_host/overlay_cursor.rs b/crates/op-host-web/src/widget_host/overlay_cursor.rs index 82edbe77c..d13ba61b4 100644 --- a/crates/op-host-web/src/widget_host/overlay_cursor.rs +++ b/crates/op-host-web/src/widget_host/overlay_cursor.rs @@ -89,15 +89,15 @@ impl WidgetHost { return true; } // Icon-picker panel hover (close / icon rows / load-more). - if self.editor_state.editor_ui.icon_picker_open { + if self.editor_state.editor_ui.icon_picker.open { use op_editor_ui::widgets::icon_picker_panel::IconPickerPanel; if let Some(panel_rect) = self.icon_picker_panel_rect(self.last_viewport_w, self.last_viewport_h) { let new_hover = IconPickerPanel::for_editor(&self.editor_state) .and_then(|p| p.hover_at(panel_rect, Point2D::new(x, y))); - if new_hover != self.editor_state.editor_ui.icon_picker_hover { - self.editor_state.editor_ui.icon_picker_hover = new_hover; + if new_hover != self.editor_state.editor_ui.icon_picker.hover { + self.editor_state.editor_ui.icon_picker.hover = new_hover; self.mark_dirty(); return true; } diff --git a/crates/op-host-web/src/widget_host/overlay_keys.rs b/crates/op-host-web/src/widget_host/overlay_keys.rs index c8334cebe..b988717ab 100644 --- a/crates/op-host-web/src/widget_host/overlay_keys.rs +++ b/crates/op-host-web/src/widget_host/overlay_keys.rs @@ -14,14 +14,18 @@ impl WidgetHost { /// Icon-picker search box typing — owns every printable char /// while the panel is open. pub(in crate::widget_host) fn icon_picker_text(&mut self, c: char) -> Option { - if !self.editor_state.editor_ui.icon_picker_open || c.is_control() { + if !self.editor_state.editor_ui.icon_picker.open || c.is_control() { return None; } if self.editor_state.editor_ui.icon_picker_select_all { self.editor_state.editor_ui.icon_picker_search.clear(); self.editor_state.editor_ui.icon_picker_select_all = false; + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; } self.editor_state.editor_ui.icon_picker_search.push(c); + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); Some(true) } @@ -29,12 +33,14 @@ impl WidgetHost { /// Icon-picker search box backspace — swallows the key while the /// panel is open even when the draft is already empty. pub(in crate::widget_host) fn icon_picker_backspace(&mut self) -> Option { - if !self.editor_state.editor_ui.icon_picker_open { + if !self.editor_state.editor_ui.icon_picker.open { return None; } if self.editor_state.editor_ui.icon_picker_select_all { self.editor_state.editor_ui.icon_picker_search.clear(); self.editor_state.editor_ui.icon_picker_select_all = false; + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); return Some(true); } @@ -45,6 +51,8 @@ impl WidgetHost { .pop() .is_some() { + self.editor_state.editor_ui.icon_picker.hover = None; + self.editor_state.editor_ui.icon_picker.pressed = None; self.mark_dirty(); return Some(true); } diff --git a/crates/op-host-web/src/widget_host/overlay_press_tests.rs b/crates/op-host-web/src/widget_host/overlay_press_tests.rs index a43764f27..ea016fc4e 100644 --- a/crates/op-host-web/src/widget_host/overlay_press_tests.rs +++ b/crates/op-host-web/src/widget_host/overlay_press_tests.rs @@ -267,7 +267,7 @@ fn shape_picker_icon_row_opens_icon_picker_panel() { let row_y = row_y.expect("icon row present in the shape picker"); assert!(host.apply_press(x, row_y, W, H)); - assert!(host.editor_state.editor_ui.icon_picker_open); + assert!(host.editor_state.editor_ui.icon_picker.open); assert!(!host.editor_state.editor_ui.shape_picker.open); // The icon-picker panel paints at its centred rect. diff --git a/crates/op-host-web/src/widget_host/overlay_rects.rs b/crates/op-host-web/src/widget_host/overlay_rects.rs index 771a88aaa..b2de4401d 100644 --- a/crates/op-host-web/src/widget_host/overlay_rects.rs +++ b/crates/op-host-web/src/widget_host/overlay_rects.rs @@ -186,7 +186,7 @@ impl WidgetHost { viewport_w: f32, viewport_h: f32, ) -> Option { - if !self.editor_state.editor_ui.icon_picker_open { + if !self.editor_state.editor_ui.icon_picker.open { return None; } let ui = &self.editor_state.editor_ui; diff --git a/crates/op-host-web/src/widget_host/property_dispatch.rs b/crates/op-host-web/src/widget_host/property_dispatch.rs index 02b9c117c..101fea27b 100644 --- a/crates/op-host-web/src/widget_host/property_dispatch.rs +++ b/crates/op-host-web/src/widget_host/property_dispatch.rs @@ -122,9 +122,7 @@ impl WidgetHost { // Property-panel icon section → replace-selection // picker (mirrors the native host's arm). let ui = &mut self.editor_state.editor_ui; - ui.icon_picker_open = true; - ui.icon_picker_replace_selection = true; - ui.icon_picker_search.clear(); + ui.open_icon_picker(true); ui.close_fill_type_picker(); ui.image_fill_popover_open = false; ui.close_font_picker(); diff --git a/crates/op-host-web/src/widget_host/shape_picker_press.rs b/crates/op-host-web/src/widget_host/shape_picker_press.rs index d68bd48e1..6954457f3 100644 --- a/crates/op-host-web/src/widget_host/shape_picker_press.rs +++ b/crates/op-host-web/src/widget_host/shape_picker_press.rs @@ -34,9 +34,7 @@ impl WidgetHost { self.editor_state.tool = tool; } ShapeChoice::OpenIconPicker => { - self.editor_state.editor_ui.icon_picker_open = true; - self.editor_state.editor_ui.icon_picker_replace_selection = false; - self.editor_state.editor_ui.icon_picker_search.clear(); + self.editor_state.editor_ui.open_icon_picker(false); } ShapeChoice::ImportImageOrSvg => { // No file-picker service on web yet — raise the same