//! A clickable, labeled button use std::borrow::Cow; use std::panic::UnwindSafe; use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; use kludgine::app::winit::keyboard::KeyCode; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::Shape; use kludgine::text::Text; use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::names::Name; use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor}; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; /// A clickable button. #[derive(Debug)] pub struct Button { /// The label to display on the button. pub label: Value, /// The callback that is invoked when the button is clicked. pub on_click: Option>, buttons_pressed: usize, background_color: Option>, background_color_animation: AnimationHandle, } impl Button { /// Returns a new button with the provided label. pub fn new(label: impl IntoValue) -> Self { Self { label: label.into_value(), on_click: None, buttons_pressed: 0, background_color: None, background_color_animation: AnimationHandle::default(), } } /// Sets the `on_click` callback and returns self. /// /// This callback will be invoked each time the button is clicked. #[must_use] pub fn on_click(mut self, callback: F) -> Self where F: FnMut(()) + Send + UnwindSafe + 'static, { self.on_click = Some(Callback::new(callback)); self } fn invoke_on_click(&mut self) { if let Some(on_click) = self.on_click.as_mut() { on_click.invoke(()); } } fn update_background_color(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { let styles = context.query_styles(&[ &ButtonActiveBackground, &ButtonBackground, &ButtonHoverBackground, &Easing, ]); let background_color = if context.active() { styles.get_or_default(&ButtonActiveBackground) } else if context.hovered() { styles.get_or_default(&ButtonHoverBackground) } else { styles.get_or_default(&ButtonBackground) }; match (immediate, &self.background_color) { (false, Some(dynamic)) => { self.background_color_animation = dynamic .transition_to(background_color) .over(Duration::from_millis(150)) .with_easing(styles.get_or_default(&Easing)) .spawn(); } (true, Some(dynamic)) => { dynamic.set(background_color); self.background_color_animation.clear(); } (_, None) => { let dynamic = Dynamic::new(background_color); self.background_color = Some(dynamic); } } } fn current_background_color(&mut self, context: &WidgetContext<'_, '_>) -> Color { if self.background_color.is_none() { self.update_background_color(context, false); } let background_color = self.background_color.as_ref().expect("always initialized"); context.redraw_when_changed(background_color); background_color.get() } } impl Widget for Button { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let size = context.graphics.region().size; let center = Point::from(size) / 2; self.label.redraw_when_changed(context); let styles = context.query_styles(&[ &TextColor, &HighlightColor, &ButtonActiveBackground, &ButtonBackground, &ButtonHoverBackground, ]); let visible_rect = Rect::from(size - (Px(1), Px(1))); let background = self.current_background_color(context); let background = Shape::filled_rect(visible_rect, background); context .graphics .draw_shape(&background, Point::default(), None, None); if context.focused() { context.draw_focus_ring_using(&styles); } self.label.map(|label| { context.graphics.draw_text( Text::new(label, styles.get_or_default(&TextColor)) .origin(kludgine::text::TextOrigin::Center) .wrap_at(size.width), center, None, None, ); }); } fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) -> bool { true } fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { // TODO this should be driven by a "focus_all_widgets" setting that hopefully can be queried from the OS. true } fn mouse_down( &mut self, _location: Point, _device_id: DeviceId, _button: MouseButton, context: &mut EventContext<'_, '_>, ) -> EventHandling { self.buttons_pressed += 1; context.activate(); HANDLED } fn mouse_drag( &mut self, location: Point, _device_id: DeviceId, _button: MouseButton, context: &mut EventContext<'_, '_>, ) { let changed = if Rect::from(context.last_layout().expect("must have been rendered").size) .contains(location) { context.activate() } else { context.deactivate() }; if changed { context.set_needs_redraw(); } } fn mouse_up( &mut self, location: Option>, _device_id: DeviceId, _button: MouseButton, context: &mut EventContext<'_, '_>, ) { self.buttons_pressed -= 1; if self.buttons_pressed == 0 { context.deactivate(); if let Some(location) = location { if Rect::from(context.last_layout().expect("must have been rendered").size) .contains(location) { context.focus(); self.invoke_on_click(); } } } } fn layout( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let padding = context .query_style(&IntrinsicPadding) .into_px(context.graphics.scale()) .into_unsigned(); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); self.label.map(|label| { let measured = context .graphics .measure_text::(Text::from(label).wrap_at(width)); let mut size = measured.size.into_unsigned(); size.width += padding * 2; size.height = size.height.max(measured.line_height.into_unsigned()) + padding * 2; size }) } fn keyboard_input( &mut self, _device_id: DeviceId, input: KeyEvent, _is_synthetic: bool, context: &mut EventContext<'_, '_>, ) -> EventHandling { if input.physical_key == KeyCode::Space { let changed = match input.state { ElementState::Pressed => context.activate(), ElementState::Released => { self.invoke_on_click(); context.deactivate() } }; if changed { context.set_needs_redraw(); } HANDLED } else { IGNORED } } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { self.update_background_color(context, false); } fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { self.update_background_color(context, false); } fn focus(&mut self, context: &mut EventContext<'_, '_>) { context.set_needs_redraw(); } fn blur(&mut self, context: &mut EventContext<'_, '_>) { context.set_needs_redraw(); } fn activate(&mut self, context: &mut EventContext<'_, '_>) { self.update_background_color(context, true); } fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { self.update_background_color(context, false); } } impl ComponentGroup for Button { fn name() -> Name { Name::new("button") } } /// The background color of the button. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct ButtonBackground; impl NamedComponent for ButtonBackground { fn name(&self) -> Cow<'_, ComponentName> { Cow::Owned(ComponentName::named::