//! Shared rendering for chip editors that use a left/right zones layout. //! //! The agent toolbar editor and header toolbar editor share the same editable //! chip sections (available bank, left/right drop zones, restore-default link). //! Modal consumers wrap those sections in a title, cancel/save buttons, and blur //! overlay, while settings consumers can render the sections inline. use galaxy_core::ui::theme::Fill; use galaxyui::elements::{ Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Stack, }; use galaxyui::platform::Cursor; use galaxyui::ui_components::button::ButtonVariant; use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use galaxyui::{Action, Element}; use pathfinder_geometry::vector::vec2f; use super::{ChipConfigurator, ChipConfiguratorAction}; use crate::Appearance; const MODAL_WIDTH: f32 = 700.; const BORDER_WIDTH: f32 = 1.; const MODAL_TITLE_FONT_SIZE: f32 = 16.; const MODAL_UNIFORM_PADDING: f32 = 24.; const CORNER_RADIUS_PIXELS: f32 = 8.; const PRIMARY_BUTTON_HEIGHT: f32 = 40.; const SECTION_UNIFORM_PADDING: f32 = 16.; const MARGIN_BETWEEN_MODAL_SECTIONS: f32 = 16.; const MODAL_CONTENT_FONT_SIZE: f32 = 14.; const RESTORE_DEFAULT_LABEL: &str = "Restore default"; /// Mouse state handles for interactive controls in chip editor sections and modals. #[derive(Default)] pub struct ChipEditorMouseHandles { pub cancel: MouseStateHandle, pub save: MouseStateHandle, pub restore_default: MouseStateHandle, } /// Everything that varies between chip editor modal-shell consumers. pub struct ChipEditorModalConfig<'a, A> { pub title: &'a str, pub available_section_label: &'a str, pub is_at_defaults: bool, pub is_dirty: bool, pub cancel_action: A, pub save_action: A, pub reset_action: A, pub activate_action: A, pub chip_action_wrapper: fn(ChipConfiguratorAction) -> A, pub mouse_handles: &'a ChipEditorMouseHandles, } /// Everything needed to render the editable chip sections without the modal shell. pub struct ChipEditorSectionsConfig<'a, A> { pub available_section_label: &'a str, pub is_at_defaults: bool, pub reset_action: A, pub activate_action: A, pub chip_action_wrapper: fn(ChipConfiguratorAction) -> A, pub mouse_handles: &'a ChipEditorMouseHandles, } /// Render a complete chip editor modal: blur overlay, centered card with title, /// chip sections (available bank + left/right drop zones), and cancel/save buttons. pub fn render_chip_editor_modal( chip_configurator: &ChipConfigurator, config: ChipEditorModalConfig, appearance: &Appearance, ) -> Box { let theme = appearance.theme(); let column = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child( Container::new(render_header(config.title, appearance)) .with_margin_bottom(MARGIN_BETWEEN_MODAL_SECTIONS) .finish(), ) .with_child( Container::new(render_chip_editor_sections( chip_configurator, ChipEditorSectionsConfig { available_section_label: config.available_section_label, is_at_defaults: config.is_at_defaults, reset_action: config.reset_action, activate_action: config.activate_action, chip_action_wrapper: config.chip_action_wrapper, mouse_handles: config.mouse_handles, }, appearance, )) .with_margin_bottom(MARGIN_BETWEEN_MODAL_SECTIONS) .finish(), ) .with_child(render_buttons(&config, appearance)) .finish(); let modal = Container::new( ConstrainedBox::new(column) .with_max_width(MODAL_WIDTH) .finish(), ) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(CORNER_RADIUS_PIXELS))) .with_border(Border::all(BORDER_WIDTH).with_border_fill(theme.outline())) .with_background(theme.surface_1()) .with_uniform_padding(MODAL_UNIFORM_PADDING) .with_margin_top(35.) .finish(); let mut stack = Stack::new(); stack.add_positioned_child( modal, OffsetPositioning::offset_from_parent( vec2f(0., 0.), ParentOffsetBounds::WindowByPosition, ParentAnchor::Center, ChildAnchor::Center, ), ); Container::new(Align::new(stack.finish()).finish()) .with_background_color(Fill::blur().into()) .finish() } fn render_header(title: &str, appearance: &Appearance) -> Box { appearance .ui_builder() .span(title.to_string()) .with_style(UiComponentStyles { font_size: Some(MODAL_TITLE_FONT_SIZE), font_weight: Some(galaxyui::fonts::Weight::Bold), ..Default::default() }) .build() .finish() } fn render_restore_default_button( is_at_defaults: bool, reset_action: A, mouse_handle: &MouseStateHandle, appearance: &Appearance, ) -> Box { let button = Hoverable::new(mouse_handle.clone(), |_state| { appearance .ui_builder() .span(RESTORE_DEFAULT_LABEL.to_string()) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() }) .build() .finish() }) .on_click(move |ctx, _, _| ctx.dispatch_typed_action(reset_action)) .with_cursor(Cursor::PointingHand); if is_at_defaults { button.disable().finish() } else { button.finish() } } fn render_section_label(label: &str, appearance: &Appearance) -> Box { appearance .ui_builder() .span(label.to_string()) .with_style(UiComponentStyles { font_size: Some(MODAL_CONTENT_FONT_SIZE), font_weight: Some(galaxyui::fonts::Weight::Semibold), ..Default::default() }) .build() .finish() } /// Render the editable chip sections that are shared by inline editors and modals. pub fn render_chip_editor_sections( chip_configurator: &ChipConfigurator, config: ChipEditorSectionsConfig, appearance: &Appearance, ) -> Box { let header_row = Flex::row() .with_child(render_section_label( config.available_section_label, appearance, )) .with_child(render_restore_default_button( config.is_at_defaults, config.reset_action, &config.mouse_handles.restore_default, appearance, )) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_main_axis_size(MainAxisSize::Max) .finish(); let unused_chips = chip_configurator.render_unused_chips_bank( config.activate_action, config.chip_action_wrapper, appearance, ); let left_section = Flex::column() .with_child(render_section_label("Left side", appearance)) .with_child( Container::new(chip_configurator.render_left_drop_zone( config.activate_action, config.chip_action_wrapper, appearance, )) .with_margin_top(8.) .finish(), ) .finish(); let right_section = Flex::column() .with_child(render_section_label("Right side", appearance)) .with_child( Container::new(chip_configurator.render_right_drop_zone( config.activate_action, config.chip_action_wrapper, appearance, )) .with_margin_top(8.) .finish(), ) .finish(); let drop_zones = Flex::row() .with_child(Box::new(Expanded::new(1.0, left_section))) .with_child(Box::new(Expanded::new( 1.0, Container::new(right_section).with_margin_left(8.).finish(), ))) .with_main_axis_size(MainAxisSize::Max) .finish(); Container::new( Flex::column() .with_child(header_row) .with_child(Container::new(unused_chips).with_margin_top(10.).finish()) .with_child(Container::new(drop_zones).with_margin_top(10.).finish()) .finish(), ) .with_uniform_padding(SECTION_UNIFORM_PADDING) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(CORNER_RADIUS_PIXELS))) .with_background(appearance.theme().surface_2()) .finish() } fn render_primary_button( label: String, variant: ButtonVariant, disabled: bool, mouse_state_handle: &MouseStateHandle, on_click_action: A, appearance: &Appearance, ) -> Box { let padding = Coords { top: 10., bottom: 10., right: 140., left: 140., }; let mut button = appearance .ui_builder() .button(variant, mouse_state_handle.clone()) .with_text_label(label) .with_style(UiComponentStyles { padding: Some(padding), font_size: Some(MODAL_CONTENT_FONT_SIZE), ..Default::default() }); if disabled { button = button.disabled(); } button .build() .on_click(move |ctx, _, _| ctx.dispatch_typed_action(on_click_action)) .with_cursor(Cursor::PointingHand) .finish() } fn render_buttons( config: &ChipEditorModalConfig, appearance: &Appearance, ) -> Box { let cancel_button = render_primary_button( "Cancel".to_string(), ButtonVariant::Outlined, false, &config.mouse_handles.cancel, config.cancel_action, appearance, ); let save_button = render_primary_button( "Save changes".to_string(), ButtonVariant::Accent, !config.is_dirty, &config.mouse_handles.save, config.save_action, appearance, ); Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child( ConstrainedBox::new(cancel_button) .with_height(PRIMARY_BUTTON_HEIGHT) .finish(), ) .with_child( ConstrainedBox::new(Container::new(save_button).with_margin_left(5.).finish()) .with_height(PRIMARY_BUTTON_HEIGHT) .finish(), ) .finish() }