Files
galaxy/app/src/chip_configurator/modal_shell.rs
T

328 lines
11 KiB
Rust

//! 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<A: Action + Clone + Copy + 'static>(
chip_configurator: &ChipConfigurator,
config: ChipEditorModalConfig<A>,
appearance: &Appearance,
) -> Box<dyn Element> {
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<dyn Element> {
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<A: Action + Clone + Copy + 'static>(
is_at_defaults: bool,
reset_action: A,
mouse_handle: &MouseStateHandle,
appearance: &Appearance,
) -> Box<dyn Element> {
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<dyn Element> {
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<A: Action + Clone + Copy + 'static>(
chip_configurator: &ChipConfigurator,
config: ChipEditorSectionsConfig<A>,
appearance: &Appearance,
) -> Box<dyn Element> {
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<A: Action + Clone + Copy + 'static>(
label: String,
variant: ButtonVariant,
disabled: bool,
mouse_state_handle: &MouseStateHandle,
on_click_action: A,
appearance: &Appearance,
) -> Box<dyn Element> {
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<A: Action + Clone + Copy + 'static>(
config: &ChipEditorModalConfig<A>,
appearance: &Appearance,
) -> Box<dyn Element> {
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()
}