use crate::{
Background,
EventCtx,
EventStatus,
InputEvent,
Interaction,
LayoutBox,
LayoutContext,
PaintContext,
RectCommand,
Style,
StyleBuilder,
StylePatch,
TextCommand,
Widget,
WidgetContent,
};
use smol_str::SmolStr;
use std::cell::Cell;
use winit::window::CursorIcon;
pub struct Button {
dirty: bool,
label: SmolStr,
font: Option<SmolStr>,
style: Style,
hover_style: Option<Style>,
pressed_style: Option<Style>,
disabled_style: Option<Style>,
computed_style: Style,
interaction: Interaction,
layout_box: LayoutBox,
content_size: Cell<(f32, f32)>,
}
impl Button {
pub fn new() -> Self {
let mut interaction = Interaction::new();
interaction.focusable = true;
interaction.hover_cursor = Some(CursorIcon::Pointer);
Self {
dirty: true,
label: SmolStr::new(""),
font: None,
style: Style::default(),
hover_style: None,
pressed_style: None,
disabled_style: None,
computed_style: Style::default(),
interaction,
layout_box: LayoutBox::default(),
content_size: Cell::new((0.0, 0.0)),
}
}
pub fn label(mut self, label: impl Into<SmolStr>) -> Self {
self.label = label.into();
self.mark_dirty();
self
}
pub fn font(mut self, font: impl Into<SmolStr>) -> Self {
self.font = Some(font.into());
self.mark_dirty();
self
}
pub fn hover_style(mut self, build: impl FnOnce(StylePatch) -> StylePatch) -> Self {
self.hover_style = Some(build(StylePatch::new()).build());
self.mark_dirty();
self
}
pub fn pressed_style(mut self, build: impl FnOnce(StylePatch) -> StylePatch) -> Self {
self.pressed_style = Some(build(StylePatch::new()).build());
self.mark_dirty();
self
}
pub fn disabled_style(mut self, build: impl FnOnce(StylePatch) -> StylePatch) -> Self {
self.disabled_style = Some(build(StylePatch::new()).build());
self.mark_dirty();
self
}
pub fn hover_background<B: Into<Background>>(mut self, background: B) -> Self {
self.hover_style.get_or_insert_with(Style::default).background = Some(background.into());
self.mark_dirty();
self
}
pub fn pressed_background<B: Into<Background>>(mut self, background: B) -> Self {
self.pressed_style.get_or_insert_with(Style::default).background = Some(background.into());
self.mark_dirty();
self
}
pub fn disabled_background<B: Into<Background>>(mut self, background: B) -> Self {
self.disabled_style.get_or_insert_with(Style::default).background = Some(background.into());
self.mark_dirty();
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.interaction.set_enabled(enabled);
self.mark_dirty();
self
}
fn recompute_style(&mut self) {
let patch = if !self.interaction.enabled {
self.disabled_style.as_ref()
} else if self.interaction.pressed {
self.pressed_style.as_ref().or(self.hover_style.as_ref())
} else if self.interaction.hovered {
self.hover_style.as_ref()
} else {
None
};
self.computed_style = match patch {
Some(patch) => self.style.overlay(patch),
None => self.style.clone(),
};
}
}
impl Default for Button {
fn default() -> Self {
Self::new()
}
}
impl StyleBuilder for Button {
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn mark_dirty(&mut self) {
self.dirty = true;
self.recompute_style();
}
}
impl WidgetContent for Button {
fn with_content(self, content: impl Into<SmolStr>) -> Self {
self.label(content)
}
}
crate::impl_interaction_builders!(Button);
impl Widget for Button {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self, dirty: bool) {
self.dirty = dirty;
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn interaction(&self) -> Option<&Interaction> {
Some(&self.interaction)
}
fn interaction_mut(&mut self) -> Option<&mut Interaction> {
Some(&mut self.interaction)
}
fn measure(&self, ctx: &mut LayoutContext) -> (f32, f32) {
let scale_factor = ctx.scale_factor;
let style = &self.computed_style;
let font_size = style.font_size
.map(|s| s.to_physical(scale_factor))
.unwrap_or(20.0 * scale_factor);
let letter_spacing = style.letter_spacing
.map(|ls| ls.value().to_physical(scale_factor))
.unwrap_or(0.0);
let line_height = style.line_height
.map(|lh| lh.value().to_physical(scale_factor))
.unwrap_or(0.0);
let (text_w, text_h) = ctx.text.measure(
&self.label,
self.font.as_deref(),
font_size,
style.font_weight.unwrap_or_default(),
style.font_style.unwrap_or_default(),
letter_spacing,
line_height
);
self.content_size.set((text_w, text_h));
let padding = &style.padding.unwrap_or_default();
(
text_w + padding.left.value() + padding.right.value(),
text_h + padding.top.value() + padding.bottom.value(),
)
}
fn layout(&mut self, rect: LayoutBox) {
self.layout_box = rect;
}
fn layout_box(&self) -> &LayoutBox {
&self.layout_box
}
fn paint(&self, ctx: &mut PaintContext) {
let style = &self.computed_style;
if let Some(background) = style.background.clone() {
let border = style.border.as_ref();
ctx.draw_rect(RectCommand {
position: (self.layout_box.x, self.layout_box.y),
size: (self.layout_box.width, self.layout_box.height),
background: Some(background),
border_radius: border.map(|b| b.radius),
border_width: border.map(|b| b.width),
border_color: border.map(|b| b.color),
});
}
let (content_w, content_h) = self.content_size.get();
let padding = style.padding.unwrap_or_default();
let text_x =
self.layout_box.x +
padding.left.value() +
(self.layout_box.width - padding.left.value() - padding.right.value() - content_w).max(
0.0
) *
0.5;
let text_y =
self.layout_box.y +
padding.top.value() +
(self.layout_box.height - padding.top.value() - padding.bottom.value() - content_h).max(
0.0
) *
0.5;
ctx.draw_text(TextCommand {
text: self.label.clone(),
position: (text_x, text_y),
style: style.clone(),
font: self.font.clone(),
});
}
fn event(&mut self, event: &InputEvent, ctx: &mut EventCtx) -> EventStatus {
if !self.interaction.is_active() {
return EventStatus::Ignored;
}
let status = self.interaction.handle(event, ctx);
if matches!(status, EventStatus::Handled) {
self.recompute_style();
self.dirty = true;
}
status
}
fn content_eq(&self, other: &dyn Widget) -> bool {
let Some(other) = other.as_any().downcast_ref::<Button>() else {
return false;
};
self.label == other.label &&
self.font == other.font &&
format!("{:?}", self.style) == format!("{:?}", other.style) &&
format!("{:?}", self.hover_style) == format!("{:?}", other.hover_style) &&
format!("{:?}", self.pressed_style) == format!("{:?}", other.pressed_style) &&
format!("{:?}", self.disabled_style) == format!("{:?}", other.disabled_style)
}
fn after_interaction_transfer(&mut self) {
self.recompute_style();
}
fn transfer_measured_state(&mut self, old: &dyn Widget) {
if let Some(old) = old.as_any().downcast_ref::<Button>() {
self.content_size.set(old.content_size.get());
}
}
}