From 0113cccbcf88bb0ee196e53fed1eb376ecdfec16 Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 1 Jun 2026 00:46:41 +0800 Subject: [PATCH] fix(ai): reveal design json copy on hover --- crates/op-editor-core/src/editor_ui_state.rs | 4 + .../op-editor-ui/src/widgets/ai_chat_panel.rs | 21 +++- .../src/widgets/ai_chat_transcript.rs | 105 +++++------------- .../widgets/ai_chat_transcript_copy_tests.rs | 81 ++++++++++++++ .../src/widgets/ai_chat_transcript_design.rs | 25 +++-- .../src/widgets/ai_chat_transcript_hit.rs | 82 ++++++++++++++ crates/op-editor-ui/src/widgets/mod.rs | 1 + crates/op-host-native/src/widget_host.rs | 3 + .../src/widget_host/chat_design_hover.rs | 28 +++++ .../widget_host/chat_design_hover_tests.rs | 31 ++++++ .../src/widget_host/geometry.rs | 1 + .../op-host-native/src/widget_host/input.rs | 3 + 12 files changed, 296 insertions(+), 89 deletions(-) create mode 100644 crates/op-editor-ui/src/widgets/ai_chat_transcript_hit.rs create mode 100644 crates/op-host-native/src/widget_host/chat_design_hover.rs create mode 100644 crates/op-host-native/src/widget_host/chat_design_hover_tests.rs diff --git a/crates/op-editor-core/src/editor_ui_state.rs b/crates/op-editor-core/src/editor_ui_state.rs index d4c2e4d7f..c0a75adc1 100644 --- a/crates/op-editor-core/src/editor_ui_state.rs +++ b/crates/op-editor-core/src/editor_ui_state.rs @@ -698,6 +698,9 @@ pub struct EditorUiState { /// Index into `chat.available_models` of the model row the cursor /// is over, or `None`. Drives the picker's hover-row tint. pub chat_model_picker_hover: Option, + /// Hovered chat design JSON card `(message_index, block_index)`; + /// drives the TS-style hover reveal of the card's copy affordance. + pub chat_design_block_hover: Option<(usize, usize)>, /// Index into `AgentProvider::ALL` of the agent driving the chat. pub chat_selected_agent: usize, @@ -894,6 +897,7 @@ impl Default for EditorUiState { chat_model_picker_caret: None, chat_model_picker_caret_anchor_ms: 0, chat_model_picker_hover: None, + chat_design_block_hover: None, chat_selected_agent: 0, topbar_traffic_hover: false, window_fullscreen: false, diff --git a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs index a5cd511b8..d83e9c0ff 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_panel.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_panel.rs @@ -90,6 +90,7 @@ pub struct AIChatPlaceholder<'a> { pub model_picker_caret: Option, /// Last focus / edit timestamp for the model-picker search caret. pub model_picker_caret_anchor_ms: u64, + pub design_hover: Option<(usize, usize)>, /// Localised empty-state example cards. pub(crate) examples: [ExampleCard; 4], /// Active UI locale. @@ -122,6 +123,7 @@ impl<'a> AIChatPlaceholder<'a> { model_picker_search: ui.chat_model_picker_search.clone(), model_picker_caret: ui.chat_model_picker_caret, model_picker_caret_anchor_ms: ui.chat_model_picker_caret_anchor_ms, + design_hover: ui.chat_design_block_hover, examples: example_cards(ui.locale), locale: ui.locale, } @@ -150,7 +152,6 @@ impl<'a> AIChatPlaceholder<'a> { self.state.messages.iter().any(|message| message.streaming) } - /// Bounds of the transcript body region. fn body_rect(&self, rect: Rect) -> Rect { let body_top = rect.origin.y + HEADER_HEIGHT; let body_bottom = rect.origin.y + rect.size.y @@ -164,7 +165,6 @@ impl<'a> AIChatPlaceholder<'a> { } } - /// Bounds of the model-picker dropdown. fn model_picker_rect(&self, rect: Rect, input_rect: Rect) -> Rect { let height = crate::widgets::ai_chat_model_picker::picker_view_height( &self.state.available_models, @@ -178,7 +178,6 @@ impl<'a> AIChatPlaceholder<'a> { } } - /// Public bounds of the open model-picker dropdown. pub fn model_picker_bounds(&self, rect: Rect) -> Option { if !self.model_picker_open { return None; @@ -362,6 +361,19 @@ impl<'a> AIChatPlaceholder<'a> { } Some(AIChatHit::DragHandle) } + + pub fn design_block_hover_at(&self, rect: Rect, point: Point2D) -> Option<(usize, usize)> { + if self.state.messages.is_empty() { + return None; + } + crate::widgets::ai_chat_transcript_hit::design_block_at( + &self.state.messages, + self.body_rect(rect), + point.x, + point.y, + self.locale, + ) + } } fn rect_contains(r: Rect, p: Point2D) -> bool { @@ -489,13 +501,14 @@ impl<'a> Widget for AIChatPlaceholder<'a> { !can_use_model || self.is_streaming(), ); } else { - crate::widgets::ai_chat_transcript::paint_transcript( + crate::widgets::ai_chat_transcript::paint_transcript_with_design_hover( cx, &self.theme, self.body_rect(rect), &self.state.messages, self.now_ms, self.locale, + self.design_hover, ); } if checklist_h > 0.0 { diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs index 1ee1f2544..436515e22 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript.rs @@ -21,6 +21,7 @@ use op_editor_core::chat::{ChatMessage, ChatRole}; use super::ai_chat_transcript_design::{ extract_design_json_blocks, paint_design_block, place_design_blocks, DesignBlock, }; +pub(crate) use super::ai_chat_transcript_hit::{transcript_hit, TranscriptHit}; use super::ai_chat_transcript_steps::{ extract_step_blocks, split_design_progress, strip_tool_call_xml, ParsedStep, ParsedStepStatus, }; @@ -75,23 +76,6 @@ const CHAR_UNIT_PX: f32 = 6.6; /// Bubble width as a fraction of the transcript body width. const BUBBLE_FRAC: f32 = 0.84; -/// What a click inside the transcript resolved to. Both variants -/// carry the index into the full `messages` slice. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TranscriptHit { - /// The thinking-block header — toggle its collapsed state. - ToggleThinking(usize), - /// The tool-calls panel header — toggle its collapsed state. - ToggleToolCalls(usize), - /// One tool-call card header — set just that card's expanded - /// state. Carries `(message_index, tool_call_index, expanded)`. - SetToolCallCardExpanded(usize, usize, bool), - /// One design JSON card header — set just that card's expanded - /// state. Carries `(message_index, design_block_index, expanded)`. - SetDesignBlockExpanded(usize, usize, bool), - CopyDesignBlock(String), -} - /// A collapsible block (thinking text or tool-call list) — a /// clickable `header` row plus an optional `body` box. When /// `collapsed`, `body` has zero height and `lines` is empty. @@ -138,10 +122,6 @@ pub(crate) struct TranscriptItem { pub streaming: bool, } -fn rect_contains(r: Rect, x: f32, y: f32) -> bool { - x >= r.origin.x && x <= r.origin.x + r.size.x && y >= r.origin.y && y <= r.origin.y + r.size.y -} - /// Wrap-unit budget for an inner text width. fn unit_budget(inner_w: f32) -> u32 { (inner_w / CHAR_UNIT_PX).floor().max(1.0) as u32 @@ -203,6 +183,7 @@ fn build_item( top: f32, body: Rect, locale: op_editor_core::Locale, + design_hover: Option<(usize, usize)>, ) -> (TranscriptItem, f32) { let is_user = msg.role == ChatRole::User; let bubble_w = if is_user { @@ -318,6 +299,9 @@ fn build_item( bubble_w, SUB_GAP, &msg.design_block_expanded_overrides, + design_hover.and_then(|(message_index, block_index)| { + (message_index == msg_index).then_some(block_index) + }), ); y = next_y; @@ -426,6 +410,15 @@ pub(crate) fn build_transcript( messages: &[ChatMessage], body_rect: Rect, locale: op_editor_core::Locale, +) -> Vec { + build_transcript_with_design_hover(messages, body_rect, locale, None) +} + +pub(crate) fn build_transcript_with_design_hover( + messages: &[ChatMessage], + body_rect: Rect, + locale: op_editor_core::Locale, + design_hover: Option<(usize, usize)>, ) -> Vec { if messages.is_empty() { return Vec::new(); @@ -435,7 +428,7 @@ pub(crate) fn build_transcript( let mut start = messages.len(); let mut used = 0.0f32; for i in (0..messages.len()).rev() { - let (_, bottom) = build_item(&messages[i], i, 0.0, body_rect, locale); + let (_, bottom) = build_item(&messages[i], i, 0.0, body_rect, locale, None); let h = bottom + if start == messages.len() { 0.0 @@ -452,64 +445,13 @@ pub(crate) fn build_transcript( let mut items = Vec::new(); let mut top = body_rect.origin.y; for (i, msg) in messages.iter().enumerate().skip(start) { - let (item, bottom) = build_item(msg, i, top, body_rect, locale); + let (item, bottom) = build_item(msg, i, top, body_rect, locale, design_hover); items.push(item); top = bottom + MSG_GAP; } items } -/// Resolve a click inside the transcript body to a [`TranscriptHit`] -/// — only the collapsible headers are interactive. -pub(crate) fn transcript_hit( - messages: &[ChatMessage], - body_rect: Rect, - x: f32, - y: f32, - locale: op_editor_core::Locale, -) -> Option { - // Paint clips the transcript to `body_rect`; gate hit-test the - // same way so a click in the body/input gap can't toggle a - // header that an over-tall latest message placed off-screen. - if !rect_contains(body_rect, x, y) { - return None; - } - for item in build_transcript(messages, body_rect, locale) { - if let Some(t) = &item.thinking { - if rect_contains(t.header, x, y) { - return Some(TranscriptHit::ToggleThinking(item.msg_index)); - } - } - if let Some(t) = &item.tools { - if rect_contains(t.header, x, y) { - return Some(TranscriptHit::ToggleToolCalls(item.msg_index)); - } - for (tool_index, card) in t.cards.iter().enumerate() { - if rect_contains(card.header, x, y) { - return Some(TranscriptHit::SetToolCallCardExpanded( - item.msg_index, - tool_index, - !card.expanded, - )); - } - } - } - for (block_index, block) in item.design_blocks.iter().enumerate() { - if rect_contains(block.copy, x, y) { - return Some(TranscriptHit::CopyDesignBlock(block.code.clone())); - } - if rect_contains(block.header, x, y) { - return Some(TranscriptHit::SetDesignBlockExpanded( - item.msg_index, - block_index, - !block.expanded, - )); - } - } - } - None -} - /// Draw one wrapped text line. Small shared helper so the bubble, /// thinking body and tool list paint identically. fn draw_line( @@ -685,6 +627,7 @@ fn paint_typing_dots( /// Paint the chat transcript — the tail of `messages` that fits in /// `body_rect`, with collapsible thinking / tool blocks, image /// thumbnails and the streaming animation on the in-flight message. +#[cfg(test)] pub(crate) fn paint_transcript( cx: &mut PaintCx<'_>, theme: &Theme, @@ -692,10 +635,22 @@ pub(crate) fn paint_transcript( messages: &[ChatMessage], now_ms: u64, locale: op_editor_core::Locale, +) { + paint_transcript_with_design_hover(cx, theme, body_rect, messages, now_ms, locale, None); +} + +pub(crate) fn paint_transcript_with_design_hover( + cx: &mut PaintCx<'_>, + theme: &Theme, + body_rect: Rect, + messages: &[ChatMessage], + now_ms: u64, + locale: op_editor_core::Locale, + design_hover: Option<(usize, usize)>, ) { cx.backend.save(); cx.backend.clip_rect(body_rect); - for item in build_transcript(messages, body_rect, locale) { + for item in build_transcript_with_design_hover(messages, body_rect, locale, design_hover) { for step in &item.steps { paint_action_step(cx, theme, step); } diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript_copy_tests.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript_copy_tests.rs index f65e23591..c25f44d6b 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_transcript_copy_tests.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript_copy_tests.rs @@ -23,3 +23,84 @@ fn transcript_hit_resolves_design_json_copy_button_like_ts() { fn body() -> Rect { Rect::xywh(0.0, 0.0, 340.0, 300.0) } + +#[test] +fn paint_design_json_copy_icon_is_hidden_until_card_hover_like_ts() { + let message = ChatMessage::assistant( + r#"```json +[{"id":"frame-1","type":"Frame"}] +```"#, + ); + let messages = [message]; + let mut backend = CopyPaintBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + paint_transcript( + &mut cx, + &crate::Theme::dark(), + body(), + &messages, + 0, + op_editor_core::Locale::EnUs, + ); + + assert_eq!(backend.copy_icon_strokes, 0); +} + +#[test] +fn paint_design_json_copy_icon_is_visible_for_hovered_card_like_ts() { + let message = ChatMessage::assistant( + r#"```json +[{"id":"frame-1","type":"Frame"}] +```"#, + ); + let messages = [message]; + let mut backend = CopyPaintBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + paint_transcript_with_design_hover( + &mut cx, + &crate::Theme::dark(), + body(), + &messages, + 0, + op_editor_core::Locale::EnUs, + Some((0, 0)), + ); + + assert!(backend.copy_icon_strokes > 0); +} + +#[derive(Default)] +struct CopyPaintBackend { + copy_icon_strokes: usize, +} + +impl crate::RenderBackend for CopyPaintBackend { + fn begin_frame(&mut self) {} + fn end_frame(&mut self) {} + fn fill_rect(&mut self, _: Rect, _: crate::Color) {} + fn stroke_rect(&mut self, _: Rect, _: crate::Color, _: f32) {} + fn draw_text(&mut self, _: &crate::TextLayout, _: Point2D) {} + fn clip_rect(&mut self, _: Rect) {} + fn save(&mut self) {} + fn restore(&mut self) {} + fn translate(&mut self, _: Point2D) {} + fn stroke_line(&mut self, _: Point2D, _: Point2D, _: crate::Color, _: f32) {} + fn fill_round_rect(&mut self, _: Rect, _: f32, _: crate::Color) {} + fn stroke_round_rect(&mut self, _: Rect, _: f32, _: crate::Color, _: f32) {} + fn stroke_svg_path(&mut self, _: &str, point: Point2D, _: f32, _: crate::Color, _: f32) { + if (290.0..=305.0).contains(&point.x) { + self.copy_icon_strokes += 1; + } + } + fn fill_oval(&mut self, _: Rect, _: crate::Color) {} + fn resize(&mut self, _: u32, _: u32) {} + fn dpi_scale(&self) -> f32 { + 1.0 + } +} diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript_design.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript_design.rs index 78e6366d6..52d543940 100644 --- a/crates/op-editor-ui/src/widgets/ai_chat_transcript_design.rs +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript_design.rs @@ -25,6 +25,7 @@ pub(crate) struct DesignBlock { pub label: String, pub streaming: bool, pub code: String, + pub copy_visible: bool, pub code_lines: Vec, } @@ -146,6 +147,7 @@ pub(crate) fn place_design_blocks( width: f32, gap: f32, expanded_overrides: &[Option], + hovered_index: Option, ) -> (Vec, f32) { const BODY_PAD_Y: f32 = 8.0; const BODY_LINE_H: f32 = 13.0; @@ -189,6 +191,7 @@ pub(crate) fn place_design_blocks( label: pending.label, streaming: pending.streaming, code: pending.code, + copy_visible: hovered_index == Some(index), code_lines, }); y += DESIGN_BLOCK_H + body_h + gap; @@ -252,16 +255,18 @@ pub(crate) fn paint_design_block(cx: &mut PaintCx<'_>, theme: &Theme, block: &De ); cx.backend.restore(); - let mut copy_color = theme.muted_foreground; - copy_color.a *= 0.5; - draw_icon( - cx.backend, - Icon::Copy, - Point2D::new(block.copy.origin.x + 4.0, block.copy.origin.y + 4.0), - 12.0, - copy_color, - 1.5, - ); + if block.copy_visible { + let mut copy_color = theme.muted_foreground; + copy_color.a *= 0.5; + draw_icon( + cx.backend, + Icon::Copy, + Point2D::new(block.copy.origin.x + 4.0, block.copy.origin.y + 4.0), + 12.0, + copy_color, + 1.5, + ); + } let mut chevron_color = theme.muted_foreground; chevron_color.a *= 0.45; diff --git a/crates/op-editor-ui/src/widgets/ai_chat_transcript_hit.rs b/crates/op-editor-ui/src/widgets/ai_chat_transcript_hit.rs new file mode 100644 index 000000000..78b873cd5 --- /dev/null +++ b/crates/op-editor-ui/src/widgets/ai_chat_transcript_hit.rs @@ -0,0 +1,82 @@ +use super::ai_chat_transcript::build_transcript; +use crate::Rect; +use op_editor_core::chat::ChatMessage; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TranscriptHit { + ToggleThinking(usize), + ToggleToolCalls(usize), + SetToolCallCardExpanded(usize, usize, bool), + SetDesignBlockExpanded(usize, usize, bool), + CopyDesignBlock(String), +} + +fn rect_contains(r: Rect, x: f32, y: f32) -> bool { + x >= r.origin.x && x <= r.origin.x + r.size.x && y >= r.origin.y && y <= r.origin.y + r.size.y +} + +pub(crate) fn transcript_hit( + messages: &[ChatMessage], + body_rect: Rect, + x: f32, + y: f32, + locale: op_editor_core::Locale, +) -> Option { + if !rect_contains(body_rect, x, y) { + return None; + } + for item in build_transcript(messages, body_rect, locale) { + if let Some(t) = &item.thinking { + if rect_contains(t.header, x, y) { + return Some(TranscriptHit::ToggleThinking(item.msg_index)); + } + } + if let Some(t) = &item.tools { + if rect_contains(t.header, x, y) { + return Some(TranscriptHit::ToggleToolCalls(item.msg_index)); + } + for (tool_index, card) in t.cards.iter().enumerate() { + if rect_contains(card.header, x, y) { + return Some(TranscriptHit::SetToolCallCardExpanded( + item.msg_index, + tool_index, + !card.expanded, + )); + } + } + } + for (block_index, block) in item.design_blocks.iter().enumerate() { + if rect_contains(block.copy, x, y) { + return Some(TranscriptHit::CopyDesignBlock(block.code.clone())); + } + if rect_contains(block.header, x, y) { + return Some(TranscriptHit::SetDesignBlockExpanded( + item.msg_index, + block_index, + !block.expanded, + )); + } + } + } + None +} + +pub(crate) fn design_block_at( + messages: &[ChatMessage], + body_rect: Rect, + x: f32, + y: f32, + locale: op_editor_core::Locale, +) -> Option<(usize, usize)> { + if !rect_contains(body_rect, x, y) { + return None; + } + for item in build_transcript(messages, body_rect, locale) { + for (block_index, block) in item.design_blocks.iter().enumerate() { + if rect_contains(block.rect, x, y) { + return Some((item.msg_index, block_index)); + } + } + } + None +} diff --git a/crates/op-editor-ui/src/widgets/mod.rs b/crates/op-editor-ui/src/widgets/mod.rs index 9ad46fea1..6519410d3 100644 --- a/crates/op-editor-ui/src/widgets/mod.rs +++ b/crates/op-editor-ui/src/widgets/mod.rs @@ -126,6 +126,7 @@ pub mod ai_chat_panel_controls; pub mod ai_chat_panel_paint; pub mod ai_chat_transcript; pub(crate) mod ai_chat_transcript_design; +pub(crate) mod ai_chat_transcript_hit; pub(crate) mod ai_chat_transcript_steps; pub(crate) mod ai_chat_transcript_text; pub(crate) mod ai_chat_transcript_tools; diff --git a/crates/op-host-native/src/widget_host.rs b/crates/op-host-native/src/widget_host.rs index 1e65c8b46..de89d758e 100644 --- a/crates/op-host-native/src/widget_host.rs +++ b/crates/op-host-native/src/widget_host.rs @@ -40,6 +40,9 @@ mod agent_settings_draft_dispatch; #[cfg(test)] mod agent_settings_tests; mod ai_chat_geometry; +mod chat_design_hover; +#[cfg(test)] +mod chat_design_hover_tests; mod chat_model_picker_caret; #[cfg(test)] mod chat_model_picker_caret_tests; diff --git a/crates/op-host-native/src/widget_host/chat_design_hover.rs b/crates/op-host-native/src/widget_host/chat_design_hover.rs new file mode 100644 index 000000000..5ea048702 --- /dev/null +++ b/crates/op-host-native/src/widget_host/chat_design_hover.rs @@ -0,0 +1,28 @@ +use super::WidgetHostNative; +use op_editor_ui::widgets::AIChatPlaceholder; +use op_editor_ui::Point2D; + +impl WidgetHostNative { + pub(in crate::widget_host) fn update_chat_design_hover( + &mut self, + x: f32, + y: f32, + over_topmost: bool, + ) -> bool { + let new_hover = if !over_topmost { + self.ai_chat_rect(self.last_viewport_w, self.last_viewport_h) + .and_then(|chat_rect| { + let panel = AIChatPlaceholder::from_editor_at(&self.editor_state, self.now_ms); + panel.design_block_hover_at(chat_rect, Point2D::new(x, y)) + }) + } else { + None + }; + if new_hover == self.editor_state.editor_ui.chat_design_block_hover { + return false; + } + self.editor_state.editor_ui.chat_design_block_hover = new_hover; + self.mark_dirty(); + true + } +} diff --git a/crates/op-host-native/src/widget_host/chat_design_hover_tests.rs b/crates/op-host-native/src/widget_host/chat_design_hover_tests.rs new file mode 100644 index 000000000..78b38d5ff --- /dev/null +++ b/crates/op-host-native/src/widget_host/chat_design_hover_tests.rs @@ -0,0 +1,31 @@ +use super::*; +use op_editor_ui::widgets::AIChatPlaceholder; +use op_editor_ui::Point2D; + +#[test] +fn cursor_move_tracks_hovered_design_json_card_for_copy_reveal() { + let mut host = WidgetHostNative::new(); + host.editor_state_mut() + .chat + .messages + .push(op_editor_core::ChatMessage::assistant( + r#"```json +[{"id":"frame-1","type":"Frame"}] +```"#, + )); + let viewport_w = 1440.0; + let viewport_h = 900.0; + host.last_viewport_w = viewport_w; + host.last_viewport_h = viewport_h; + let chat_rect = host.ai_chat_rect(viewport_w, viewport_h).unwrap(); + let panel = AIChatPlaceholder::from_editor(host.editor_state()); + let point = Point2D::new(chat_rect.origin.x + 24.0, chat_rect.origin.y + 52.0); + assert_eq!(panel.design_block_hover_at(chat_rect, point), Some((0, 0))); + + assert!(host.apply_cursor_move(point.x, point.y)); + + assert_eq!( + host.editor_state().editor_ui.chat_design_block_hover, + Some((0, 0)) + ); +} diff --git a/crates/op-host-native/src/widget_host/geometry.rs b/crates/op-host-native/src/widget_host/geometry.rs index 2def108ef..4b96384f2 100644 --- a/crates/op-host-native/src/widget_host/geometry.rs +++ b/crates/op-host-native/src/widget_host/geometry.rs @@ -73,6 +73,7 @@ impl WidgetHostNative { changed |= ui.toolbar_hover.take().is_some(); changed |= ui.align_toolbar_hover.take().is_some(); changed |= ui.chat_model_picker_hover.take().is_some(); + changed |= ui.chat_design_block_hover.take().is_some(); changed |= ui.export_picker_hover.take().is_some(); if let Some(menu) = ui.layer_context_menu.as_mut() { changed |= menu.hovered_row.take().is_some(); diff --git a/crates/op-host-native/src/widget_host/input.rs b/crates/op-host-native/src/widget_host/input.rs index eb9a50a8b..1e6bf894a 100644 --- a/crates/op-host-native/src/widget_host/input.rs +++ b/crates/op-host-native/src/widget_host/input.rs @@ -314,6 +314,9 @@ impl WidgetHostNative { } } } + if self.update_chat_design_hover(x, y, over_topmost) { + return true; + } if let Some(drag) = self.rotate_drag { let cursor_angle = (y - drag.center_screen_y).atan2(x - drag.center_screen_x); let new_rotation = drag.start_rotation + (cursor_angle - drag.start_cursor_angle);