diff --git a/crates/op-editor-core/src/agent_settings_button_state.rs b/crates/op-editor-core/src/agent_settings_button_state.rs index 5a7dcbcac..0153e6dc4 100644 --- a/crates/op-editor-core/src/agent_settings_button_state.rs +++ b/crates/op-editor-core/src/agent_settings_button_state.rs @@ -9,4 +9,8 @@ pub enum AgentSettingsButton { McpClientConfigCopy, ImageSearchTest, ImageGenAdd, + ImageProfileHeader(usize), + ImageProfileRemove(usize), + ImageProfileProvider(usize), + ImageProfileTest(usize), } diff --git a/crates/op-editor-ui/src/widgets/agent_settings_images.rs b/crates/op-editor-ui/src/widgets/agent_settings_images.rs index ae5c50cc2..402c744e5 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_images.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_images.rs @@ -639,10 +639,15 @@ fn paint_profile_row( } else { cx.backend.stroke_round_rect(row, 6.0, theme.border, 1.0); } - if settings.hover_image_gen_profile_header == Some(index) { - cx.backend - .fill_round_rect(profile_header_rect(row), 6.0, theme.button_hover); - } + crate::widgets::button::paint_ghost_button_feedback( + cx.backend, + theme, + profile_header_rect(row), + settings.hover_image_gen_profile_header == Some(index), + ui.button_pressed(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileHeader(index), + )), + ); let dot = profile_active_rect(row); if active { cx.backend.fill_oval(dot, theme.primary); @@ -708,10 +713,15 @@ fn paint_profile_row( ); let remove_hover = profile_remove_hover_rect(row); - if settings.hover_image_gen_profile_remove == Some(index) { - cx.backend - .fill_round_rect(remove_hover, 6.0, theme.button_hover); - } + crate::widgets::button::paint_ghost_button_feedback( + cx.backend, + theme, + remove_hover, + settings.hover_image_gen_profile_remove == Some(index), + ui.button_pressed(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileRemove(index), + )), + ); draw_icon( cx.backend, Icon::Trash, @@ -746,6 +756,9 @@ fn paint_profile_row( profile, profile_test_btn_rect(row), settings.hover_image_gen_profile_test == Some(index), + ui.button_pressed(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileTest(index), + )), ); let provider_rect = profile_provider_rect(row); paint_provider_field( @@ -755,6 +768,9 @@ fn paint_profile_row( provider_rect, row.origin.x + 12.0, settings.hover_image_gen_profile_provider == Some(index), + ui.button_pressed(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileProvider(index), + )), ); if settings.image_gen_provider_menu_open == Some(index) { let hovered = settings diff --git a/crates/op-editor-ui/src/widgets/agent_settings_images_parts.rs b/crates/op-editor-ui/src/widgets/agent_settings_images_parts.rs index c1fa16874..b7f51671d 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_images_parts.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_images_parts.rs @@ -4,7 +4,7 @@ use crate::theme::Theme; use crate::widgets::agent_settings_caret::{paint_settings_input_view, settings_input_text}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::PaintCx; -use crate::{Color, Point2D, Rect, TextLayout}; +use crate::{Point2D, Rect, TextLayout}; use op_editor_core::agent_settings::{ AgentSettings, ImageGenField, ImageGenProfile, ImageGenProvider, ImageSearchField, ImageTestStatus, SettingsFocus, @@ -222,15 +222,13 @@ pub(super) fn paint_profile_test_button( profile: &ImageGenProfile, btn: Rect, hovered: bool, + pressed: bool, ) { let enabled = !profile.api_key.trim().is_empty() && profile.test_status != ImageTestStatus::Testing; paint_profile_test_status(cx, theme, profile, btn); cx.backend.fill_round_rect(btn, 6.0, theme.muted); - if hovered { - cx.backend - .fill_round_rect(btn, 6.0, profile_button_hover_color(theme)); - } + crate::widgets::button::paint_ghost_button_feedback(cx.backend, theme, btn, hovered, pressed); cx.backend.stroke_round_rect(btn, 6.0, theme.border, 1.0); let label = crate::widgets::agent_settings_i18n::t(ui, "settings.images.test"); let label_w = cx.backend.measure_text(label, 11.0); @@ -255,15 +253,6 @@ pub(super) fn paint_profile_test_button( ); } -fn profile_button_hover_color(theme: &Theme) -> Color { - Color { - r: theme.foreground.r, - g: theme.foreground.g, - b: theme.foreground.b, - a: 0.12, - } -} - fn paint_profile_test_status( cx: &mut PaintCx<'_>, theme: &Theme, @@ -312,6 +301,7 @@ pub(super) fn paint_provider_field( input: Rect, label_x: f32, hovered: bool, + pressed: bool, ) { let label_lay = TextLayout::single_run( "Provider", @@ -323,9 +313,7 @@ pub(super) fn paint_provider_field( cx.backend .draw_text(&label_lay, Point2D::new(label_x, input.origin.y + 16.0)); cx.backend.fill_round_rect(input, 6.0, theme.card); - if hovered { - cx.backend.fill_round_rect(input, 6.0, theme.button_hover); - } + crate::widgets::button::paint_ghost_button_feedback(cx.backend, theme, input, hovered, pressed); cx.backend.stroke_round_rect(input, 6.0, theme.border, 1.0); let value = ellipsize(cx, profile.provider.label(), input.size.x - 28.0, 11.0); let value_lay = TextLayout::single_run( diff --git a/crates/op-editor-ui/src/widgets/agent_settings_images_profile_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_images_profile_tests.rs index effc00488..3a71604a0 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_images_profile_tests.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_images_profile_tests.rs @@ -4,7 +4,7 @@ use crate::{Color, Point2D, Rect, RenderBackend, TextLayout}; use op_editor_core::agent_settings::{ AgentSettingsTab, ImageGenField, ImageGenProvider, ImageTestStatus, SettingsFocus, }; -use op_editor_core::EditorState; +use op_editor_core::{AgentSettingsButton, ButtonPressTarget, EditorState}; #[derive(Default)] struct CaptureBackend { @@ -406,14 +406,80 @@ fn images_tab_profile_controls_hover_paints_visible_washes() { } assert!( backend.round_fills.iter().any(|(fill, color)| { - rect_eq(*fill, test_button) - && !color_eq(*color, panel.theme.muted) - && color.a > panel.theme.button_hover.a + 0.01 + rect_eq(*fill, test_button) && color_eq(*color, panel.theme.button_hover) }), - "hovered profile test button should paint a stronger visible hover wash" + "hovered profile test button should paint the shared hover token" ); } +#[test] +fn pressed_image_gen_profile_controls_use_shared_button_feedback() { + for button in [ + AgentSettingsButton::ImageProfileHeader(0), + AgentSettingsButton::ImageProfileRemove(0), + AgentSettingsButton::ImageProfileProvider(0), + AgentSettingsButton::ImageProfileTest(0), + ] { + let mut state = EditorState::default(); + state.editor_ui.agent_settings.tab = AgentSettingsTab::Images; + state.editor_ui.agent_settings.add_image_gen_profile(); + state.editor_ui.agent_settings.focus = Some(SettingsFocus::ImageGenProfile { + index: 0, + field: ImageGenField::Name, + }); + state.editor_ui.pressed_button = Some(ButtonPressTarget::AgentSettings(button)); + let panel = AgentSettingsPanel::for_editor(&state); + let rect = panel.rect(1200.0, 800.0); + let content_x = rect.origin.x + 200.0 + 24.0; + let content_y = rect.origin.y + 24.0; + let content_w = rect.size.x - 200.0 - 48.0; + let row = Rect { + origin: Point2D::new(content_x + 8.0, content_y + 36.0 + 24.0 + 28.0 + 36.0 + 8.0), + size: Point2D::new(content_w - 16.0, 32.0 + 8.0 + 5.0 * 36.0), + }; + let target = match button { + AgentSettingsButton::ImageProfileHeader(_) => Rect { + origin: row.origin, + size: Point2D::new(row.size.x, 32.0), + }, + AgentSettingsButton::ImageProfileRemove(_) => Rect { + origin: Point2D::new(row.origin.x + row.size.x - 30.0, row.origin.y + 2.0), + size: Point2D::new(28.0, 28.0), + }, + AgentSettingsButton::ImageProfileProvider(_) => Rect { + origin: Point2D::new(row.origin.x + 110.0, row.origin.y + 40.0 + 36.0), + size: Point2D::new(row.size.x - 110.0 - 12.0, 24.0), + }, + AgentSettingsButton::ImageProfileTest(_) => Rect { + origin: Point2D::new( + row.origin.x + row.size.x - 12.0 - 56.0, + row.origin.y + 40.0 + 72.0, + ), + size: Point2D::new(56.0, 24.0), + }, + _ => unreachable!("case list only includes image profile buttons"), + }; + let expected = panel + .theme + .button_hover + .with_alpha(panel.theme.button_hover.a * 1.8); + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + panel.paint(&mut cx, rect); + + assert!( + backend + .round_fills + .iter() + .any(|(fill, color)| rect_eq(*fill, target) && color_eq(*color, expected)), + "pressed {button:?} should paint the shared pressed feedback token" + ); + } +} + #[test] fn disabled_image_gen_profile_test_hover_uses_visible_wash() { let mut state = EditorState::default(); @@ -449,11 +515,9 @@ fn disabled_image_gen_profile_test_hover_uses_visible_wash() { assert!( backend.round_fills.iter().any(|(fill, color)| { - rect_eq(*fill, test_button) - && !color_eq(*color, panel.theme.muted) - && color.a > panel.theme.button_hover.a + 0.01 + rect_eq(*fill, test_button) && color_eq(*color, panel.theme.button_hover) }), - "disabled profile test button hover should paint a visible wash" + "disabled profile test button hover should paint the shared hover token" ); } diff --git a/crates/op-editor-ui/src/widgets/editor_state_ext.rs b/crates/op-editor-ui/src/widgets/editor_state_ext.rs index ad8bad5d7..7b5b9d51c 100644 --- a/crates/op-editor-ui/src/widgets/editor_state_ext.rs +++ b/crates/op-editor-ui/src/widgets/editor_state_ext.rs @@ -106,6 +106,10 @@ pub fn agent_settings_button( W::CopyMcpClientConfig => Some(O::McpClientConfigCopy), W::TestImageSearch => Some(O::ImageSearchTest), W::AddGenConfig => Some(O::ImageGenAdd), + W::ToggleGenConfigEditor(index) => Some(O::ImageProfileHeader(index)), + W::RemoveGenConfig(index) => Some(O::ImageProfileRemove(index)), + W::ToggleGenProviderMenu(index) => Some(O::ImageProfileProvider(index)), + W::TestGenConfig(index) => Some(O::ImageProfileTest(index)), _ => None, } } diff --git a/crates/op-host-native/src/widget_host/agent_settings_image_gen_tests.rs b/crates/op-host-native/src/widget_host/agent_settings_image_gen_tests.rs index 237f60854..f61d3f5f7 100644 --- a/crates/op-host-native/src/widget_host/agent_settings_image_gen_tests.rs +++ b/crates/op-host-native/src/widget_host/agent_settings_image_gen_tests.rs @@ -2,6 +2,7 @@ use super::WidgetHostNative; use op_editor_core::agent_settings::{ AgentSettingsTab, ImageGenField, ImageGenProvider, ImageTestStatus, SettingsFocus, }; +use op_editor_core::{AgentSettingsButton, ButtonPressTarget}; use op_editor_ui::widgets::agent_settings_panel::AgentSettingsPanel; #[test] @@ -43,6 +44,15 @@ fn image_generation_profile_test_tracks_testing_status_like_ts() { .agent_settings .image_gen_profiles[0]; assert_eq!(profile.test_status, ImageTestStatus::Testing); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileTest(0) + )) + ); + + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); } #[test] diff --git a/crates/op-host-native/src/widget_host/agent_settings_tests.rs b/crates/op-host-native/src/widget_host/agent_settings_tests.rs index 4bbf05510..6f0f82715 100644 --- a/crates/op-host-native/src/widget_host/agent_settings_tests.rs +++ b/crates/op-host-native/src/widget_host/agent_settings_tests.rs @@ -617,6 +617,15 @@ fn image_generation_profile_buttons_add_activate_and_remove() { 1200.0, 800.0 )); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileRemove(1) + )) + ); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); + let settings = &host.editor_state().editor_ui.agent_settings; assert_eq!(settings.image_gen_profiles.len(), 1); assert_eq!( @@ -738,6 +747,14 @@ fn image_generation_provider_click_opens_menu_without_changing_profile() { .image_gen_provider_menu_open, Some(0) ); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileProvider(0) + )) + ); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); assert!(host.dispatch_agent_settings_press( content_x + 110.0 + 20.0, @@ -784,6 +801,12 @@ fn image_generation_profile_header_click_toggles_editor_closed() { let row_y = gen_top + 36.0 + 8.0; assert!(host.dispatch_agent_settings_press(content_x + 72.0, row_y + 16.0, 1200.0, 800.0)); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileHeader(0) + )) + ); assert_eq!(host.editor_state().editor_ui.agent_settings.focus, None); assert!(host @@ -792,6 +815,8 @@ fn image_generation_profile_header_click_toggles_editor_closed() { .settings_input .text() .is_empty()); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); } #[test] diff --git a/crates/op-host-web/src/widget_host/agent_settings_press_tests.rs b/crates/op-host-web/src/widget_host/agent_settings_press_tests.rs index 7015464fa..9eeeee0f6 100644 --- a/crates/op-host-web/src/widget_host/agent_settings_press_tests.rs +++ b/crates/op-host-web/src/widget_host/agent_settings_press_tests.rs @@ -256,6 +256,15 @@ fn image_generation_profile_test_tracks_testing_status_like_ts() { .agent_settings .image_gen_profiles[0]; assert_eq!(profile.test_status, ImageTestStatus::Testing); + assert_eq!( + host.editor_state.editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileTest(0) + )) + ); + + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state.editor_ui.pressed_button, None); } #[test] @@ -304,6 +313,14 @@ fn image_generation_provider_select_commits_and_closes_menu() { field: ImageGenField::Name, }) ); + assert_eq!( + host.editor_state.editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileProvider(0) + )) + ); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state.editor_ui.pressed_button, None); assert!(host.dispatch_agent_settings_press( content_x + 110.0 + 20.0, @@ -351,9 +368,17 @@ fn image_generation_profile_header_click_toggles_editor_closed() { let row_y = gen_top + 36.0 + 8.0; assert!(host.dispatch_agent_settings_press(content_x + 72.0, row_y + 16.0, 1200.0, 800.0)); + assert_eq!( + host.editor_state.editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::ImageProfileHeader(0) + )) + ); assert_eq!(host.editor_state.editor_ui.agent_settings.focus, None); assert!(host.editor_state.editor_ui.settings_input.text().is_empty()); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state.editor_ui.pressed_button, None); } #[test]