//! This module contains the code for the ignore button shown inline next to autosuggestions. use galaxy_core::ui::theme::Fill; use galaxyui::elements::{ ChildAnchor, ConstrainedBox, Container, CornerRadius, Element, Hoverable, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Stack, }; use galaxyui::platform::Cursor; use galaxyui::ui_components::components::UiComponent; use galaxyui::{Entity, SingletonEntity, TypedActionView, View, ViewContext}; use pathfinder_geometry::vector::vec2f; use super::EditorElement; use crate::appearance::Appearance; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; pub const AUTOSUGGESTION_IGNORE_MINIMUM_HEIGHT: f32 = 12.; pub struct AutosuggestionIgnore { autosuggestion_ignore_mouse_handle: MouseStateHandle, current_autosuggestion: Option, } pub enum AutosuggestionIgnoreEvent { IgnoreAutosuggestion { suggestion: String }, } impl Default for AutosuggestionIgnore { fn default() -> Self { Self::new() } } impl Entity for AutosuggestionIgnore { type Event = AutosuggestionIgnoreEvent; } impl AutosuggestionIgnore { pub fn new() -> Self { Self { autosuggestion_ignore_mouse_handle: Default::default(), current_autosuggestion: None, } } pub fn set_current_autosuggestion(&mut self, suggestion: Option) { self.current_autosuggestion = suggestion; } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AutosuggestionIgnoreAction { IgnoreCurrentAutosuggestion, } impl TypedActionView for AutosuggestionIgnore { type Action = AutosuggestionIgnoreAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { AutosuggestionIgnoreAction::IgnoreCurrentAutosuggestion => { if let Some(suggestion) = &self.current_autosuggestion { ctx.emit(AutosuggestionIgnoreEvent::IgnoreAutosuggestion { suggestion: suggestion.clone(), }); } } } ctx.notify(); } } impl View for AutosuggestionIgnore { fn ui_name() -> &'static str { "AutosuggestionIgnore" } fn render(&self, ctx: &galaxyui::AppContext) -> Box { let appearance = Appearance::as_ref(ctx); // Because the icon's origin is NOT the top of the line but the top of the cursor, // we should render it with line height ratio no larger than DEFAULT_UI_LINE_HEIGHT_RATIO. // With larger line height ratios, there wouldn't be enough space in the icon to render the text. // But we do need to account for smaller line height ratios so the text can be rendered in the smaller space. let line_height_ratio = appearance .line_height_ratio() .min(galaxyui::elements::DEFAULT_UI_LINE_HEIGHT_RATIO); // We want the ignore icon to be the same height as the cursor in the input. let height = EditorElement::cursor_height(appearance.monospace_font_size(), line_height_ratio) .max(AUTOSUGGESTION_IGNORE_MINIMUM_HEIGHT); let disabled_color = blended_colors::semantic_text_disabled(appearance.theme()); let border_width = 1.; // The stack contains the ignore button and tooltip that shows upon mouse hover. let mut stack = Stack::new(); Hoverable::new(self.autosuggestion_ignore_mouse_handle.clone(), |state| { // Colors are inverted when hovered. let (icon_color, background_color) = if state.is_hovered() { ( appearance.theme().background().into(), Some(blended_colors::semantic_text_disabled(appearance.theme())), ) } else { ( blended_colors::semantic_text_disabled(appearance.theme()), None, ) }; let height_without_border = height - border_width * 2.; let close_icon = Container::new( ConstrainedBox::new(Icon::X.to_galaxyui_icon(Fill::Solid(icon_color)).finish()) .with_height(height_without_border) .with_width(height_without_border) .finish(), ) .finish(); let mut ignore_button = Container::new(close_icon) .with_uniform_padding(2.) .with_border( galaxyui::elements::Border::all(border_width).with_border_color(disabled_color), ) .with_corner_radius(CornerRadius::with_all(Radius::Percentage(25.))); if let Some(background_color) = background_color { ignore_button = ignore_button.with_background_color(background_color); } let ignore_button_element = ConstrainedBox::new(ignore_button.finish()) .with_max_height(height) .finish(); stack.add_child(ignore_button_element); if state.is_hovered() { let tool_tip = appearance .ui_builder() .autosuggestion_tool_tip("Ignore this suggestion".into()) .build() .finish(); stack.add_positioned_overlay_child( tool_tip, OffsetPositioning::offset_from_parent( vec2f(0., -5.), ParentOffsetBounds::WindowByPosition, ParentAnchor::TopMiddle, ChildAnchor::BottomMiddle, ), ); } stack.finish() }) .on_click(|ctx, _, _| { ctx.dispatch_typed_action(AutosuggestionIgnoreAction::IgnoreCurrentAutosuggestion); }) .with_cursor(Cursor::PointingHand) .finish() } }