diff --git a/crates/op-editor-core/src/button_press_state.rs b/crates/op-editor-core/src/button_press_state.rs index dd92964ec..69fb8a08d 100644 --- a/crates/op-editor-core/src/button_press_state.rs +++ b/crates/op-editor-core/src/button_press_state.rs @@ -14,6 +14,7 @@ pub enum ButtonPressTarget { ChatFooter(crate::chat_button_state::ChatFooterButton), Git(crate::git_button_state::GitButton), PropertyPanel(usize), + VariablesPanel(crate::variables_panel_state::VariablesPanelButton), DesignMd(crate::design_md_button_state::DesignMdButton), ComponentBrowser(crate::component_browser_state::ComponentBrowserButton), ExportDialog(crate::export_dialog_state::ExportDialogButton), diff --git a/crates/op-editor-ui/src/widgets/variables_panel.rs b/crates/op-editor-ui/src/widgets/variables_panel.rs index 718d46656..b189615e8 100644 --- a/crates/op-editor-ui/src/widgets/variables_panel.rs +++ b/crates/op-editor-ui/src/widgets/variables_panel.rs @@ -4,7 +4,7 @@ use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId}; use crate::{Point2D, Rect}; use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue}; use op_editor_core::editor_ui_state::VariableRowFocus; -use op_editor_core::{EditorState, Locale, VariablesPanelButton}; +use op_editor_core::{ButtonPressTarget, EditorState, Locale, VariablesPanelButton}; mod geometry; mod header; @@ -146,6 +146,7 @@ pub struct VariablesPanel { /// Open `⋯` row menu, keyed by UNFILTERED row index. row_menu_open: Option, hover: Option, + pressed: Option, editing_name_row: Option, editing_value_cell: Option<(usize, usize)>, header_input: jian_core::text_input::TextInputState, @@ -254,6 +255,10 @@ impl VariablesPanel { scroll: state.editor_ui.variables_scroll.offset, row_menu_open: state.editor_ui.variables_row_menu, hover: state.editor_ui.variables_panel_hover, + pressed: match state.editor_ui.pressed_button { + Some(ButtonPressTarget::VariablesPanel(button)) => Some(button), + _ => None, + }, editing_name_row: state.editor_ui.variable_row_focus.and_then(|f| match f { VariableRowFocus::Name(i) => Some(i), VariableRowFocus::Number(_) diff --git a/crates/op-editor-ui/src/widgets/variables_panel/hover.rs b/crates/op-editor-ui/src/widgets/variables_panel/hover.rs index f6288ca48..b6c2a07cd 100644 --- a/crates/op-editor-ui/src/widgets/variables_panel/hover.rs +++ b/crates/op-editor-ui/src/widgets/variables_panel/hover.rs @@ -54,6 +54,7 @@ mod tests { use crate::Color; use jian_ops_schema::variable::{VariableKind, VariableScalar}; use op_editor_core::variables_panel_state::VariablesPanelButton; + use op_editor_core::ButtonPressTarget; #[derive(Default)] struct HoverBackend { @@ -158,4 +159,36 @@ mod tests { "add-theme button should paint a hover wash" ); } + + #[test] + fn pressed_header_targets_paint_shared_feedback() { + let mut state = themed_state(); + state.editor_ui.pressed_button = Some(ButtonPressTarget::VariablesPanel( + VariablesPanelButton::AddTheme, + )); + let panel = VariablesPanel::for_editor(&state); + assert_eq!(panel.pressed, Some(VariablesPanelButton::AddTheme)); + let rect = Rect { + origin: Point2D::new(0.0, 0.0), + size: Point2D::new(VARIABLES_PANEL_WIDTH, panel.intrinsic_height()), + }; + let expected = panel + .theme + .button_hover + .with_alpha(panel.theme.button_hover.a * 1.8); + let mut backend = HoverBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + panel.paint(&mut cx, rect); + + assert!( + backend + .round_fills + .iter() + .any(|(fill, color)| *fill == panel.add_theme_rect(rect) && *color == expected), + "pressed add-theme button should paint shared pressed feedback" + ); + } } diff --git a/crates/op-editor-ui/src/widgets/variables_panel/paint.rs b/crates/op-editor-ui/src/widgets/variables_panel/paint.rs index 9b5fecd2e..8459c9e65 100644 --- a/crates/op-editor-ui/src/widgets/variables_panel/paint.rs +++ b/crates/op-editor-ui/src/widgets/variables_panel/paint.rs @@ -1,4 +1,5 @@ use super::*; +use crate::widgets::button::paint_button_feedback_wash; use crate::widgets::property_panel_text_input::paint_text_input_view_value; use crate::widgets::{draw_icon, Icon, PaintCx}; use crate::{Color, Point2D, Rect}; @@ -13,6 +14,20 @@ const VALUE_INPUT_MIN_WIDTH: f32 = 96.0; const VALUE_INPUT_MAX_WIDTH: f32 = 160.0; const FOOTER_CHEVRON_LABEL_GAP: f32 = 12.0; +fn paint_feedback( + panel: &VariablesPanel, + cx: &mut PaintCx<'_>, + target: VariablesPanelButton, + rect: Rect, + radius: f32, +) { + let hovered = panel.hover == Some(target); + let pressed = panel.pressed == Some(target); + if hovered || pressed { + paint_button_feedback_wash(cx.backend, &panel.theme, rect, radius, hovered, pressed); + } +} + pub(super) fn paint_panel(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) { let theme = panel.theme; let labels = panel.labels(); @@ -117,10 +132,13 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) let active_axis = panel.active_axis_label(); for (idx, axis) in panel.theme_tab_labels().iter().enumerate() { let is_active = *axis == active_axis; - if panel.hover == Some(VariablesPanelButton::ThemeTab(idx)) { - cx.backend - .fill_round_rect(panel.theme_tab_rect(rect, idx), 8.0, theme.button_hover); - } + paint_feedback( + panel, + cx, + VariablesPanelButton::ThemeTab(idx), + panel.theme_tab_rect(rect, idx), + 8.0, + ); let color = if is_active { theme.foreground } else { @@ -160,10 +178,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) } let add_theme = panel.add_theme_rect(rect); - if panel.hover == Some(VariablesPanelButton::AddTheme) { - cx.backend - .fill_round_rect(add_theme, 8.0, theme.button_hover); - } + paint_feedback(panel, cx, VariablesPanelButton::AddTheme, add_theme, 8.0); draw_icon( cx.backend, Icon::Plus, @@ -174,9 +189,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) ); let preset = panel.preset_rect(rect); - if panel.hover == Some(VariablesPanelButton::PresetMenu) { - cx.backend.fill_round_rect(preset, 8.0, theme.button_hover); - } + paint_feedback(panel, cx, VariablesPanelButton::PresetMenu, preset, 8.0); let preset_label = panel.labels().preset; let preset_label_size = 13.0; let preset_label_x = preset.origin.x + 29.0; @@ -208,9 +221,7 @@ fn paint_theme_header(panel: &VariablesPanel, cx: &mut PaintCx<'_>, rect: Rect) ); let close = close_rect(rect); - if panel.hover == Some(VariablesPanelButton::Close) { - cx.backend.fill_round_rect(close, 8.0, theme.button_hover); - } + paint_feedback(panel, cx, VariablesPanelButton::Close, close, 8.0); draw_icon( cx.backend, Icon::Close, @@ -243,13 +254,13 @@ fn paint_variant_header( let col_w = variant_column_width(rect, variants.len()); for (idx, variant) in variants.iter().enumerate() { let x = value_x + col_w * idx as f32; - if panel.hover == Some(VariablesPanelButton::VariantHeader(idx)) { - cx.backend.fill_round_rect( - panel.variant_header_rect(rect, idx), - 8.0, - theme.button_hover, - ); - } + paint_feedback( + panel, + cx, + VariablesPanelButton::VariantHeader(idx), + panel.variant_header_rect(rect, idx), + 8.0, + ); if panel.renaming_variant.as_deref() == Some(*variant) { let input_state = panel.rename_text_input(RenameTarget::Variant(variant)); let value = input_state.map(|input| input.text()).unwrap_or(variant); @@ -286,10 +297,13 @@ fn paint_variant_header( ); } } - if panel.hover == Some(VariablesPanelButton::AddVariant) { - cx.backend - .fill_round_rect(add_variant_rect(rect), 8.0, theme.button_hover); - } + paint_feedback( + panel, + cx, + VariablesPanelButton::AddVariant, + add_variant_rect(rect), + 8.0, + ); draw_icon( cx.backend, Icon::Plus, @@ -346,21 +360,21 @@ fn paint_rows( break; } let source = var.source_idx; - let row_hovered = matches!( - panel.hover, - Some(VariablesPanelButton::Row(i)) if i == source - ) || matches!( - panel.hover, - Some(VariablesPanelButton::RowMenuButton(i)) if i == source - ); - if row_hovered { - cx.backend.fill_round_rect( + let row_hovered = panel.hover == Some(VariablesPanelButton::Row(source)) + || panel.hover == Some(VariablesPanelButton::RowMenuButton(source)); + let row_pressed = panel.pressed == Some(VariablesPanelButton::Row(source)) + || panel.pressed == Some(VariablesPanelButton::RowMenuButton(source)); + if row_hovered || row_pressed { + paint_button_feedback_wash( + cx.backend, + &theme, Rect { origin: Point2D::new(rect.origin.x + 8.0, y + 3.0), size: Point2D::new(rect.size.x - 16.0, ROW_HEIGHT - 6.0), }, 8.0, - theme.button_hover, + row_hovered, + row_pressed, ); } paint_variable_name_cell(panel, cx, rect, var, idx, y); @@ -585,9 +599,7 @@ fn paint_footer( ) { let theme = panel.theme; let button = add_variable_rect(rect); - if panel.hover == Some(VariablesPanelButton::AddVariable) { - cx.backend.fill_round_rect(button, 8.0, theme.button_hover); - } + paint_feedback(panel, cx, VariablesPanelButton::AddVariable, button, 8.0); let center_y = button.origin.y + button.size.y / 2.0; let icon_size = 16.0; let label_size = 14.0; diff --git a/crates/op-host-native/src/widget_host/variables_panel_press.rs b/crates/op-host-native/src/widget_host/variables_panel_press.rs index 18ee935aa..91d7c94c0 100644 --- a/crates/op-host-native/src/widget_host/variables_panel_press.rs +++ b/crates/op-host-native/src/widget_host/variables_panel_press.rs @@ -32,6 +32,9 @@ impl WidgetHostNative { self.mark_dirty(); return true; }; + self.editor_state.editor_ui.pressed_button = vars + .hover_at(vars_rect, point) + .map(op_editor_core::ButtonPressTarget::VariablesPanel); match hit { VariablesPanelHit::Resize(edge) => { // Edge press arms a resize drag; cursor moves write the diff --git a/crates/op-host-native/src/widget_host/variables_panel_tests.rs b/crates/op-host-native/src/widget_host/variables_panel_tests.rs index ec2a7793d..af3b7259f 100644 --- a/crates/op-host-native/src/widget_host/variables_panel_tests.rs +++ b/crates/op-host-native/src/widget_host/variables_panel_tests.rs @@ -4,11 +4,59 @@ use super::{ }; use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue}; use op_editor_core::editor_ui_state::VariableRowFocus; +use op_editor_core::{ButtonPressTarget, VariablesPanelButton}; +use op_editor_ui::widgets::variables_panel::{VariablesPanel, VariablesPanelHit}; use op_editor_ui::widgets::{TOOLBAR_WIDTH, TOP_BAR_HEIGHT}; +use op_editor_ui::Point2D; const VIEWPORT_W: f32 = 1280.0; const VIEWPORT_H: f32 = 900.0; +fn point_for_hit(host: &WidgetHostNative, want: &VariablesPanelHit) -> (f32, f32) { + let rect = host + .variables_panel_rect(VIEWPORT_W, VIEWPORT_H) + .expect("variables panel rect"); + let panel = VariablesPanel::for_editor(host.editor_state()); + let mut y = rect.origin.y; + while y < rect.origin.y + rect.size.y { + let mut x = rect.origin.x; + while x < rect.origin.x + rect.size.x { + if panel + .hit_test(rect, Point2D::new(x, y)) + .is_some_and(|hit| &hit == want) + { + return (x, y); + } + x += 2.0; + } + y += 2.0; + } + panic!("no panel point maps to {want:?}"); +} + +#[test] +fn variables_panel_press_sets_and_release_clears_pressed_button() { + let mut host = WidgetHostNative::new(); + host.editor_state_mut().editor_ui.variables_panel_open = true; + assert!(host.editor_state_mut().create_variable( + "spacing", + VariableKind::Number, + VariableScalar::Num(8.0), + )); + + let (x, y) = point_for_hit(&host, &VariablesPanelHit::AddTheme); + assert!(host.apply_press(x, y, VIEWPORT_W, VIEWPORT_H)); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::VariablesPanel( + VariablesPanelButton::AddTheme + )) + ); + + assert!(host.apply_release_with_viewport(VIEWPORT_W, VIEWPORT_H)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); +} + #[test] fn variables_panel_floats_next_to_toolbar_like_ts() { let mut host = WidgetHostNative::new(); diff --git a/crates/op-host-web/src/widget_host/variables_panel_press.rs b/crates/op-host-web/src/widget_host/variables_panel_press.rs index ed9fc3417..807f829e2 100644 --- a/crates/op-host-web/src/widget_host/variables_panel_press.rs +++ b/crates/op-host-web/src/widget_host/variables_panel_press.rs @@ -39,6 +39,9 @@ impl WidgetHost { self.mark_dirty(); return true; }; + self.editor_state.editor_ui.pressed_button = vars + .hover_at(vars_rect, point) + .map(op_editor_core::ButtonPressTarget::VariablesPanel); self.commit_property_family_focus_if_any(); match hit { VariablesPanelHit::Resize(edge) => { diff --git a/crates/op-host-web/src/widget_host/variables_panel_tests.rs b/crates/op-host-web/src/widget_host/variables_panel_tests.rs index 24da661cc..50a7a6e48 100644 --- a/crates/op-host-web/src/widget_host/variables_panel_tests.rs +++ b/crates/op-host-web/src/widget_host/variables_panel_tests.rs @@ -5,7 +5,7 @@ use super::WidgetHost; use jian_ops_schema::variable::{VariableKind, VariableScalar, VariableValue}; use op_editor_core::editor_ui_state::VariableRowFocus; -use op_editor_core::{own_bounds, NodeId, PropertyFocus}; +use op_editor_core::{own_bounds, ButtonPressTarget, NodeId, PropertyFocus, VariablesPanelButton}; use op_editor_ui::widgets::variables_panel::{ VariablesPanel, VariablesPanelHit, VariablesResizeEdge, }; @@ -118,6 +118,29 @@ fn themed_value_for<'a>( .map(|e| &e.value) } +#[test] +fn variables_panel_press_sets_and_release_clears_pressed_button() { + let mut host = WidgetHost::new(); + host.editor_state.editor_ui.variables_panel_open = true; + assert!(host.editor_state.create_variable( + "spacing", + VariableKind::Number, + VariableScalar::Num(8.0), + )); + + let (x, y) = point_for_hit(&host, &VariablesPanelHit::AddTheme); + assert!(host.apply_press(x, y, W, H)); + assert_eq!( + host.editor_state.editor_ui.pressed_button, + Some(ButtonPressTarget::VariablesPanel( + VariablesPanelButton::AddTheme + )) + ); + + assert!(host.apply_release_with_viewport(W, H)); + assert_eq!(host.editor_state.editor_ui.pressed_button, None); +} + #[test] fn close_button_closes_floating_panel() { let mut host = two_variant_color_host();