mod params; pub mod themes; use warp_core::ui::{ appearance::Appearance, color::{ContrastingColor as _, contrast::MinimumAllowedContrast}, }; use warpui::{ elements::{MouseState, MouseStateHandle}, prelude::*, }; pub use params::*; pub use themes::Theme; use crate::{keyboard_shortcut, tooltip}; #[derive(Default)] pub struct Button { mouse_state: MouseStateHandle, tooltip: tooltip::Tooltip, } impl crate::Component for Button { type Params<'a> = Params<'a>; fn render<'a>(&self, appearance: &Appearance, params: Self::Params<'a>) -> Box { let theme: &dyn Theme = if params.options.disabled { &themes::Disabled } else { params.theme }; let mut hoverable = Hoverable::new(self.mouse_state.clone(), |mouse_state| { let size = params.options.size; let is_icon_button = matches!(params.content, Content::Icon(_)); let background = theme.background(mouse_state.into(), appearance); let mut text_color = theme.text_color(background, appearance); // Ensures that the action button text is always rendered with sufficient contrast. // For hovered states that use a semi-transparent background, we apply the contrast adjustment using the base background. if let Some(base_bg) = theme.background(State::Default, appearance) { text_color = text_color.on_background(base_bg.into_solid(), MinimumAllowedContrast::Text); } let mut row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_alignment(MainAxisAlignment::Center) .with_spacing(size.inner_spacing()); // Add icon, if any. match ¶ms.content { Content::Icon(icon) | Content::IconAndLabel(icon, _) => { let icon_size = size.icon_size(); row.add_child( ConstrainedBox::new(icon.to_warpui_icon(text_color.into()).finish()) .with_width(icon_size) .with_height(icon_size) .finish(), ); } Content::Label(_) => {} } // Add label, if any. match params.content { Content::Label(label) | Content::IconAndLabel(_, label) => { let font_size = size.font_size(); let font_properties = size.font_properties(); row.add_child( Text::new_inline(label, appearance.ui_font_family(), font_size) .with_color(text_color) .with_style(font_properties) .with_selectable(false) .finish(), ); } Content::Icon(_) => {} } // Add keystroke, if any. if let Some(keystroke) = params.options.keystroke { let sizing = size.keyboard_shortcut_sizing(); row.add_child( Container::new( keyboard_shortcut::KeyboardShortcut.render( appearance, keyboard_shortcut::Params { keystroke, options: keyboard_shortcut::Options { font_color: Some(text_color), background: theme .keyboard_shortcut_background(appearance) .map(Into::into), border_fill: theme .keyboard_shortcut_border(text_color, appearance) .map(Into::into), sizing, }, }, ), ) .with_margin_left(2.) .finish(), ); } let mut button = Container::new(row.finish()) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))); if let Some(background) = background { button = button.with_background(background); } if let Some(border) = theme.border(appearance) { button = button.with_border(Border::all(1.).with_border_color(border)); } if is_icon_button { // Make the button evenly padded when there is no label. let vertical_padding = (size.height() - size.icon_size()) / 2.; button = button.with_horizontal_padding(vertical_padding); } else { button = button.with_horizontal_padding(size.horizontal_padding()); } // Constrain the button sizing. // // It is important that this is done after styling changes are applied above, // otherwise the presence of a border or padding will affect the final size. let height = size.height(); let mut button = ConstrainedBox::new(button.finish()).with_height(height); // If the content is an icon, make it square. if is_icon_button { button = button.with_width(height); } let mut stack = stack::Stack::new().with_child(button.finish()); if mouse_state.is_hovered() && let Some(tooltip) = params.options.tooltip { stack.add_positioned_overlay_child( self.tooltip.render(appearance, tooltip.params), stack::OffsetPositioning::offset_from_parent( vec2f(0., -4.), stack::ParentOffsetBounds::WindowByPosition, tooltip.alignment.parent_anchor(), tooltip.alignment.child_anchor(), ), ); } stack.finish() }); if !params.options.disabled && let Some(on_click) = params.options.on_click { hoverable = hoverable .with_cursor(Cursor::PointingHand) .on_click(on_click); } hoverable.finish() } } /// The current state of the button. pub enum State { Default, Hovered, Pressed, } impl From<&MouseState> for State { fn from(mouse_state: &MouseState) -> Self { if mouse_state.is_clicked() { return Self::Pressed; } if mouse_state.is_hovered() { return Self::Hovered; } Self::Default } }