use super::agent_slide::AgentSlideAction; use pathfinder_geometry::vector::vec2f; use warp_core::ui::{ appearance::Appearance, icons::Icon, theme::{color::internal_colors, Fill}, }; use warpui::{ elements::{ Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Stack, Text, }, fonts::{Properties, Weight}, platform::Cursor, Element, }; pub(super) struct TwoLineButtonSpec { pub(super) is_selected: bool, pub(super) title: String, pub(super) subtitle: String, pub(super) height: f32, pub(super) mouse_state: MouseStateHandle, pub(super) click_action: AgentSlideAction, pub(super) subtitle_font_size: f32, pub(super) title_color: Fill, pub(super) subtitle_color: Fill, /// Optional icon to display before the title. pub(super) icon: Option, /// If set, the button is disabled and this text is shown as a badge. pub(super) disabled_badge: Option, /// When true, the button is fully disabled: muted colors, no selected /// state, no click cursor, and no click handler. pub(super) is_disabled: bool, } pub(super) fn render_two_line_button( appearance: &Appearance, spec: TwoLineButtonSpec, ) -> Box { const RADIUS: f32 = 8.; let TwoLineButtonSpec { is_selected, title, subtitle, height, mouse_state, click_action, subtitle_font_size, title_color, subtitle_color, icon, disabled_badge, is_disabled, } = spec; let theme = appearance.theme(); let is_disabled = is_disabled || disabled_badge.is_some(); // Disabled models always use muted colors and never show as selected. let effective_selected = is_selected && !is_disabled; let (title_fill, subtitle_fill, background) = match (effective_selected, is_disabled) { (true, _) => { let bg_color = internal_colors::accent_overlay_1(theme); let selected_color = internal_colors::accent_fg_strong(theme); (selected_color, selected_color, Some(bg_color)) } (false, true) => { let bg_color = theme.surface_2(); let disabled_color = theme.disabled_text_color(bg_color); (disabled_color, disabled_color, Some(bg_color)) } (false, false) => (title_color, subtitle_color, None), }; let border_color = if effective_selected { theme.accent() } else { Fill::Solid(internal_colors::neutral_4(theme)) }; let ui_font_family = appearance.ui_font_family(); let hoverable = Hoverable::new(mouse_state, move |_| { let title_text = Text::new(title.clone(), ui_font_family, 14.0) .with_color(title_fill.into_solid()) .with_style(Properties { weight: Weight::Normal, ..Default::default() }) .with_line_height_ratio(1.0) .finish(); // Build title row with optional icon let title_el: Box = if let Some(icon) = icon { const ICON_SIZE: f32 = 14.; let icon_el = ConstrainedBox::new(Box::new(icon.to_warpui_icon(title_fill))) .with_width(ICON_SIZE) .with_height(ICON_SIZE) .finish(); Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(icon_el) .with_child(Container::new(title_text).with_margin_left(4.).finish()) .finish() } else { title_text }; let subtitle_el = Text::new(subtitle.clone(), ui_font_family, subtitle_font_size) .with_color(subtitle_fill.into_solid()) .with_style(Properties { weight: Weight::Normal, ..Default::default() }) .with_line_height_ratio(1.0) .finish(); let content = Flex::column() .with_main_axis_size(MainAxisSize::Min) .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(title_el) .with_child(Container::new(subtitle_el).with_margin_top(8.).finish()) .finish(); let aligned = Align::new(content).left().finish(); let mut container = Container::new(aligned) .with_uniform_padding(24.) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(RADIUS))) .with_border(Border::all(1.).with_border_fill(border_color)); if let Some(bg) = background { container = container.with_background(bg); } let button_el = ConstrainedBox::new(container.finish()) .with_min_height(height) .finish(); // Overlay the badge pill at bottom-right if disabled if let Some(ref badge_text) = disabled_badge { let badge_label = Text::new(badge_text.clone(), ui_font_family, 11.0) .with_color(internal_colors::neutral_1(theme)) .with_style(Properties { weight: Weight::Medium, ..Default::default() }) .with_line_height_ratio(1.0) .finish(); let badge = Container::new(badge_label) .with_padding_left(6.) .with_padding_right(6.) .with_padding_top(2.) .with_padding_bottom(2.) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))) .with_background_color(theme.ansi_fg_green()) .finish(); let mut stack = Stack::new(); stack.add_child(button_el); stack.add_positioned_child( badge, OffsetPositioning::offset_from_parent( vec2f(-6., -6.), ParentOffsetBounds::ParentByPosition, ParentAnchor::BottomRight, ChildAnchor::BottomRight, ), ); stack.finish() } else { button_el } }); if is_disabled { // Disabled: no click handler, default cursor hoverable.finish() } else { hoverable .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(click_action.clone()); }) .finish() } }