use super::*;
#[derive(Debug, Clone)]
pub struct ButtonOptions {
pub layout: LayoutStyle,
pub visual: UiVisual,
pub hovered_visual: Option<UiVisual>,
pub pressed_visual: Option<UiVisual>,
pub pressed_hovered_visual: Option<UiVisual>,
pub focused_visual: Option<UiVisual>,
pub disabled_visual: Option<UiVisual>,
pub text_style: TextStyle,
pub leading_image: Option<ImageContent>,
pub image_size: UiSize,
pub image_shader: Option<ShaderEffect>,
pub shader: Option<ShaderEffect>,
pub animation: Option<AnimationMachine>,
pub enabled: bool,
pub pressed: bool,
pub focused: bool,
pub action: Option<WidgetActionBinding>,
pub accessibility_label: Option<String>,
pub accessibility_hint: Option<String>,
}
impl ButtonOptions {
pub fn new(layout: impl Into<LayoutStyle>) -> Self {
let layout = layout.into();
Self {
layout,
..Default::default()
}
}
pub fn with_layout(mut self, layout: impl Into<LayoutStyle>) -> Self {
self.layout = layout.into();
self
}
pub fn with_visual(mut self, visual: UiVisual) -> Self {
self.visual = visual;
self
}
pub fn with_text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = text_style;
self
}
pub fn with_leading_image(mut self, image: impl Into<ImageContent>) -> Self {
self.leading_image = Some(image.into());
self
}
pub fn with_image_size(mut self, size: UiSize) -> Self {
self.image_size = size;
self
}
pub fn with_action(mut self, action: impl Into<WidgetActionBinding>) -> Self {
self.action = Some(action.into());
self
}
pub fn with_command(mut self, command: impl Into<CommandId>) -> Self {
self.action = Some(WidgetActionBinding::command(command));
self
}
pub fn with_accessibility_label(mut self, label: impl Into<String>) -> Self {
self.accessibility_label = Some(label.into());
self
}
pub fn with_accessibility_hint(mut self, hint: impl Into<String>) -> Self {
self.accessibility_hint = Some(hint.into());
self
}
pub const fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub const fn pressed(mut self, pressed: bool) -> Self {
self.pressed = pressed;
self
}
pub const fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
}
impl Default for ButtonOptions {
fn default() -> Self {
Self {
layout: LayoutStyle::from_taffy_style(Style {
display: Display::Flex,
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
size: TaffySize {
width: Dimension::auto(),
height: Dimension::auto(),
},
..Default::default()
}),
visual: UiVisual::panel(
ColorRgba::new(36, 42, 52, 255),
Some(StrokeStyle::new(ColorRgba::new(74, 85, 104, 255), 1.0)),
4.0,
),
hovered_visual: Some(UiVisual::panel(
ColorRgba::new(56, 70, 86, 255),
Some(StrokeStyle::new(ColorRgba::new(138, 164, 194, 255), 1.0)),
4.0,
)),
pressed_visual: Some(UiVisual::panel(
ColorRgba::new(22, 30, 40, 255),
Some(StrokeStyle::new(ColorRgba::new(78, 104, 134, 255), 1.0)),
4.0,
)),
pressed_hovered_visual: Some(UiVisual::panel(
ColorRgba::new(42, 58, 74, 255),
Some(StrokeStyle::new(ColorRgba::new(150, 184, 220, 255), 1.0)),
4.0,
)),
focused_visual: Some(UiVisual::panel(
ColorRgba::new(36, 42, 52, 255),
Some(StrokeStyle::new(ColorRgba::new(120, 170, 230, 255), 1.5)),
4.0,
)),
disabled_visual: Some(UiVisual::panel(
ColorRgba::new(30, 34, 40, 180),
Some(StrokeStyle::new(ColorRgba::new(64, 72, 84, 180), 1.0)),
4.0,
)),
text_style: TextStyle::default(),
leading_image: None,
image_size: UiSize::new(18.0, 18.0),
image_shader: None,
shader: None,
animation: None,
enabled: true,
pressed: false,
focused: false,
action: None,
accessibility_label: None,
accessibility_hint: None,
}
}
}
impl ButtonOptions {
fn resolved_visual(&self) -> UiVisual {
if !self.enabled {
self.disabled_visual.unwrap_or(self.visual)
} else if self.pressed {
self.pressed_visual.unwrap_or(self.visual)
} else if self.focused {
self.focused_visual.unwrap_or(self.visual)
} else {
self.visual
}
}
fn interaction_visuals(&self) -> InteractionVisuals {
InteractionVisuals::new(self.resolved_visual())
.hovered(self.hovered_visual.unwrap_or(self.visual))
.pressed(self.pressed_visual.unwrap_or(self.visual))
.pressed_hovered(
self.pressed_hovered_visual
.or(self.pressed_visual)
.unwrap_or(self.visual),
)
.focused(self.focused_visual.unwrap_or(self.visual))
.disabled(self.disabled_visual.unwrap_or(self.visual))
}
fn resolved_visual_for_interaction(
&self,
hovered: bool,
pressed: bool,
focused: bool,
) -> UiVisual {
self.interaction_visuals()
.resolve(self.enabled, hovered, pressed, focused)
}
}
impl ButtonOptions {
fn update_document_interaction_visual(&self, document: &mut UiDocument, button: UiNodeId) {
document.set_node_interaction_visuals(button, self.interaction_visuals());
let hovered = document.focus.hovered == Some(button);
let pressed = document.focus.pressed == Some(button) || self.pressed;
let focused = document.focus.focused == Some(button) || self.focused;
let visual = self.resolved_visual_for_interaction(hovered, pressed, focused);
document.set_node_visual(button, visual);
}
}
pub fn button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
label: impl Into<String>,
options: ButtonOptions,
) -> UiNodeId {
let name = name.into();
let label = label.into();
let mut layout = options.layout.style.clone();
layout.display = Display::Flex;
layout.flex_direction = FlexDirection::Row;
layout.align_items = Some(AlignItems::Center);
layout.justify_content = Some(JustifyContent::Center);
if !label.is_empty() && padding_is_zero(layout.padding) {
let auto_width = layout.size.width == Dimension::auto();
let auto_height = layout.size.height == Dimension::auto();
layout.padding = taffy::prelude::Rect {
left: taffy::prelude::LengthPercentage::length(if auto_width { 12.0 } else { 0.0 }),
right: taffy::prelude::LengthPercentage::length(if auto_width { 12.0 } else { 0.0 }),
top: taffy::prelude::LengthPercentage::length(if auto_height { 6.0 } else { 0.0 }),
bottom: taffy::prelude::LengthPercentage::length(if auto_height { 6.0 } else { 0.0 }),
};
}
if !label.is_empty() && layout.size.height == Dimension::auto() {
layout.min_size.height = length(32.0);
}
let mut fixed_intrinsic_items = Vec::<Style>::new();
let accessibility_label = options
.accessibility_label
.clone()
.unwrap_or_else(|| label.clone());
let mut accessibility = AccessibilityMeta::new(AccessibilityRole::Button)
.label(accessibility_label)
.pressed(options.pressed)
.action(AccessibilityAction::new("activate", "Activate"));
if let Some(hint) = options.accessibility_hint.clone() {
accessibility = accessibility.hint(hint);
}
if options.enabled {
accessibility = accessibility.focusable();
} else {
accessibility = accessibility.disabled();
}
let mut node = UiNode::container(
name.clone(),
UiNodeStyle {
layout: layout.clone(),
clip: ClipBehavior::Clip,
..Default::default()
},
)
.with_input(if options.enabled {
InputBehavior::BUTTON
} else {
InputBehavior::NONE
})
.with_interaction_visuals(options.interaction_visuals())
.with_accessibility(accessibility);
if let Some(shader) = options.shader.clone() {
node = node.with_shader(shader);
}
if let Some(animation) = options.animation.clone() {
node = node.with_animation(animation);
}
if let Some(action) = options.action.clone() {
node = node.with_action(action);
}
let button = document.add_child(parent, node);
options.update_document_interaction_visual(document, button);
if let Some(image) = options.leading_image {
let image_layout = Style {
size: TaffySize {
width: length(options.image_size.width),
height: length(options.image_size.height),
},
margin: taffy::prelude::Rect {
right: taffy::prelude::LengthPercentageAuto::length(6.0),
..taffy::prelude::Rect::length(0.0)
},
..Default::default()
};
fixed_intrinsic_items.push(image_layout.clone());
let mut image_node = UiNode::image(
format!("{name}.image"),
image,
LayoutStyle::from_taffy_style(image_layout),
)
.with_accessibility(AccessibilityMeta::new(AccessibilityRole::Image).label(label.clone()));
if let Some(shader) = options.image_shader {
image_node = image_node.with_shader(shader);
}
document.add_child(button, image_node);
}
let mut label_node_id = None;
if !label.is_empty() || document.node(button).children.is_empty() {
let text_style = single_line_text_style(options.text_style);
label_node_id = Some(document.add_child(
button,
UiNode::text(
format!("{name}.label"),
label,
text_style,
LayoutStyle::from_taffy_style(Style {
size: TaffySize {
width: Dimension::auto(),
height: Dimension::auto(),
},
..Default::default()
}),
),
));
}
let intrinsic_sources = label_node_id.into_iter().collect::<Vec<_>>();
if !intrinsic_sources.is_empty() || !fixed_intrinsic_items.is_empty() {
let fixed_items = fixed_intrinsic_items.iter().collect::<Vec<_>>();
let inline_items = fixed_items.len() + intrinsic_sources.len();
publish_inline_intrinsic_size(
document,
button,
intrinsic_sources,
inline_intrinsic_base_size(&layout, &fixed_items, inline_items),
);
}
button
}
fn padding_is_zero(padding: taffy::prelude::Rect<taffy::prelude::LengthPercentage>) -> bool {
length_percentage_is_zero(padding.left)
&& length_percentage_is_zero(padding.right)
&& length_percentage_is_zero(padding.top)
&& length_percentage_is_zero(padding.bottom)
}
fn length_percentage_is_zero(value: taffy::prelude::LengthPercentage) -> bool {
let raw = value.into_raw();
raw.tag() == taffy::prelude::CompactLength::LENGTH_TAG && raw.value().abs() <= f32::EPSILON
}
pub fn small_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
label: impl Into<String>,
options: ButtonOptions,
) -> UiNodeId {
let mut options = options;
options.layout = options.layout.with_height(28.0).with_padding(6.0);
options.text_style = small_text_style();
button(document, parent, name, label, options)
}
pub fn icon_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
icon: ImageContent,
accessibility_label: impl Into<String>,
options: ButtonOptions,
) -> UiNodeId {
let label = accessibility_label.into();
let mut options = options
.with_leading_image(icon)
.with_image_size(UiSize::new(18.0, 18.0))
.with_accessibility_label(label.clone());
if options.layout.style.size.width == Dimension::auto() {
options.layout = options.layout.with_width(36.0);
}
if options.layout.style.size.height == Dimension::auto() {
options.layout = options.layout.with_height(32.0);
}
button(document, parent, name, "", options)
}
pub fn image_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
image: ImageContent,
accessibility_label: impl Into<String>,
options: ButtonOptions,
) -> UiNodeId {
let label = accessibility_label.into();
let mut options = options
.with_leading_image(image)
.with_image_size(UiSize::new(24.0, 24.0))
.with_accessibility_label(label.clone());
if options.layout.style.size.width == Dimension::auto() {
options.layout = options.layout.with_width(44.0);
}
if options.layout.style.size.height == Dimension::auto() {
options.layout = options.layout.with_height(40.0);
}
button(document, parent, name, "", options)
}
pub fn toggle_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
label: impl Into<String>,
selected: bool,
options: ButtonOptions,
) -> UiNodeId {
button(document, parent, name, label, options.pressed(selected))
}
pub fn reset_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
dirty: bool,
options: ButtonOptions,
) -> UiNodeId {
let mut options = options;
options.enabled = options.enabled && dirty;
button(document, parent, name, "Reset", options)
}
pub fn button_actions_from_input_result(
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
result: &UiInputResult,
) -> WidgetActionQueue {
let mut queue = WidgetActionQueue::new();
push_button_input_result_actions(&mut queue, document, button, options, result);
queue
}
pub fn push_button_input_result_actions<'a>(
queue: &'a mut WidgetActionQueue,
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
result: &UiInputResult,
) -> &'a mut WidgetActionQueue {
let Some(clicked) = result.clicked else {
return queue;
};
if !document.node_is_descendant_or_self(button, clicked)
|| !action_target_enabled(document, button)
{
return queue;
}
if let Some(binding) = options.action.clone() {
queue.push(WidgetAction::pointer_activate(button, binding, 1));
}
queue
}
pub fn button_actions_from_key_event(
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
event: &UiInputEvent,
) -> WidgetActionQueue {
let mut queue = WidgetActionQueue::new();
push_button_key_event_actions(&mut queue, document, button, options, event);
queue
}
pub fn push_button_key_event_actions<'a>(
queue: &'a mut WidgetActionQueue,
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
event: &UiInputEvent,
) -> &'a mut WidgetActionQueue {
let UiInputEvent::Key { key, modifiers } = event else {
return queue;
};
if document.focus.focused != Some(button) || !action_target_enabled(document, button) {
return queue;
}
if let Some(binding) = options.action.clone() {
queue.push_key_activation(button, binding, *key, *modifiers);
}
queue
}
pub fn button_actions_from_gesture_event(
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
event: &GestureEvent,
) -> WidgetActionQueue {
let mut queue = WidgetActionQueue::new();
push_button_gesture_event_actions(&mut queue, document, button, options, event);
queue
}
pub fn push_button_gesture_event_actions<'a>(
queue: &'a mut WidgetActionQueue,
document: &UiDocument,
button: UiNodeId,
options: &ButtonOptions,
event: &GestureEvent,
) -> &'a mut WidgetActionQueue {
let GestureEvent::Click(click) = event else {
return queue;
};
if click.button != PointerButton::Primary
|| !document.node_is_descendant_or_self(button, click.target)
{
return queue;
}
if !action_target_enabled(document, button) {
return queue;
}
if let Some(binding) = options.action.clone() {
queue.push(WidgetAction::pointer_activate(button, binding, click.count));
}
queue
}