use super::*;
#[derive(Debug, Clone)]
pub struct RadioButtonOptions {
pub layout: LayoutStyle,
pub outer_visual: UiVisual,
pub selected_outer_visual: Option<UiVisual>,
pub disabled_outer_visual: Option<UiVisual>,
pub dot_color: ColorRgba,
pub text_style: TextStyle,
pub enabled: bool,
pub action: Option<WidgetActionBinding>,
pub accessibility_label: Option<String>,
pub accessibility_hint: Option<String>,
}
impl Default for RadioButtonOptions {
fn default() -> Self {
Self {
layout: LayoutStyle::from_taffy_style(Style {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: Some(AlignItems::Center),
size: TaffySize {
width: Dimension::auto(),
height: length(28.0),
},
..Default::default()
}),
outer_visual: UiVisual::panel(
ColorRgba::new(29, 35, 43, 255),
Some(StrokeStyle::new(ColorRgba::new(98, 113, 135, 255), 1.0)),
8.0,
),
selected_outer_visual: Some(UiVisual::panel(
ColorRgba::new(21, 58, 92, 255),
Some(StrokeStyle::new(ColorRgba::new(108, 180, 255, 255), 1.0)),
8.0,
)),
disabled_outer_visual: Some(UiVisual::panel(
ColorRgba::new(28, 32, 38, 160),
Some(StrokeStyle::new(ColorRgba::new(67, 75, 88, 160), 1.0)),
8.0,
)),
dot_color: ColorRgba::new(108, 180, 255, 255),
text_style: TextStyle::default(),
enabled: true,
action: None,
accessibility_label: None,
accessibility_hint: None,
}
}
}
impl RadioButtonOptions {
pub fn with_layout(mut self, layout: impl Into<LayoutStyle>) -> Self {
self.layout = layout.into();
self
}
pub fn with_action(mut self, action: impl Into<WidgetActionBinding>) -> Self {
self.action = Some(action.into());
self
}
}
pub fn radio_button(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
label_text: impl Into<String>,
selected: bool,
options: RadioButtonOptions,
) -> UiNodeId {
let name = name.into();
let label_text = label_text.into();
let mut accessibility = AccessibilityMeta::new(AccessibilityRole::RadioButton)
.label(
options
.accessibility_label
.clone()
.unwrap_or_else(|| label_text.clone()),
)
.value(if selected { "selected" } else { "not selected" })
.checked(selected)
.action(AccessibilityAction::new("select", "Select"));
if let Some(hint) = options.accessibility_hint.clone() {
accessibility = accessibility.hint(hint);
}
if options.enabled {
accessibility = accessibility.focusable();
} else {
accessibility = accessibility.disabled();
}
let root_layout = options.layout.style.clone();
let mut root_node = UiNode::container(
name.clone(),
UiNodeStyle {
layout: root_layout.clone(),
clip: ClipBehavior::Clip,
..Default::default()
},
)
.with_input(if options.enabled {
InputBehavior::BUTTON
} else {
InputBehavior::NONE
})
.with_accessibility(accessibility);
if let Some(action) = options.action.clone() {
root_node = root_node.with_action(action);
}
let root = document.add_child(parent, root_node);
let outer_visual = if !options.enabled {
options
.disabled_outer_visual
.unwrap_or(options.outer_visual)
} else if selected {
options
.selected_outer_visual
.unwrap_or(options.outer_visual)
} else {
options.outer_visual
};
let outer_layout = Style {
display: Display::Flex,
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
size: TaffySize {
width: length(16.0),
height: length(16.0),
},
margin: taffy::prelude::Rect {
right: taffy::prelude::LengthPercentageAuto::length(8.0),
..taffy::prelude::Rect::length(0.0)
},
flex_shrink: 0.0,
..Default::default()
};
let outer = document.add_child(
root,
UiNode::container(
format!("{name}.outer"),
LayoutStyle::from_taffy_style(outer_layout.clone()),
)
.with_visual(outer_visual),
);
if selected {
document.add_child(
outer,
UiNode::scene(
format!("{name}.dot"),
vec![ScenePrimitive::Circle {
center: UiPoint::new(4.0, 4.0),
radius: 4.0,
fill: options.dot_color,
stroke: None,
}],
LayoutStyle::size(8.0, 8.0),
)
.with_accessibility(AccessibilityMeta::new(AccessibilityRole::Image).label("Selected")),
);
}
let label_style = single_line_text_style(options.text_style);
let label = label(
document,
root,
format!("{name}.label"),
label_text,
label_style,
LayoutStyle::new(),
);
publish_inline_intrinsic_size(
document,
root,
vec![label],
inline_intrinsic_base_size(&root_layout, &[&outer_layout], 2),
);
root
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadioOption {
pub id: String,
pub label: String,
pub enabled: bool,
pub action: Option<WidgetActionBinding>,
}
impl RadioOption {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
enabled: true,
action: None,
}
}
pub const fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_action(mut self, action: impl Into<WidgetActionBinding>) -> Self {
self.action = Some(action.into());
self
}
}
#[derive(Debug, Clone)]
pub struct RadioGroupOptions {
pub layout: LayoutStyle,
pub button_options: RadioButtonOptions,
pub accessibility_label: Option<String>,
}
impl Default for RadioGroupOptions {
fn default() -> Self {
Self {
layout: LayoutStyle::from_taffy_style(Style {
display: Display::Flex,
flex_direction: FlexDirection::Column,
gap: TaffySize {
width: taffy::prelude::LengthPercentage::length(4.0),
height: taffy::prelude::LengthPercentage::length(4.0),
},
..Default::default()
}),
button_options: RadioButtonOptions::default(),
accessibility_label: None,
}
}
}
pub fn radio_group(
document: &mut UiDocument,
parent: UiNodeId,
name: impl Into<String>,
options: &[RadioOption],
selected_id: Option<&str>,
group_options: RadioGroupOptions,
) -> UiNodeId {
let name = name.into();
let root = document.add_child(
parent,
UiNode::container(
name.clone(),
UiNodeStyle {
layout: group_options.layout.style,
..Default::default()
},
)
.with_accessibility(
AccessibilityMeta::new(AccessibilityRole::Group).label(
group_options
.accessibility_label
.unwrap_or_else(|| name.clone()),
),
),
);
for option in options {
let mut button_options = group_options.button_options.clone();
button_options.enabled = option.enabled;
button_options.action = option.action.clone();
radio_button(
document,
root,
format!("{name}.{}", option.id),
option.label.clone(),
selected_id == Some(option.id.as_str()),
button_options,
);
}
root
}
pub fn radio_button_actions_from_input_result(
document: &UiDocument,
radio: UiNodeId,
options: &RadioButtonOptions,
result: &UiInputResult,
) -> WidgetActionQueue {
let mut queue = WidgetActionQueue::new();
if result
.clicked
.is_some_and(|target| document.node_is_descendant_or_self(radio, target))
&& action_target_enabled(document, radio)
{
if let Some(binding) = options.action.clone() {
queue.select(radio, binding, true);
}
}
queue
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn radio_button_builds_selected_accessible_control() {
let mut document = UiDocument::new(root_style(240.0, 120.0));
let root = document.root;
let node = radio_button(
&mut document,
root,
"choice.compact",
"Compact",
true,
RadioButtonOptions::default().with_action("choice.compact"),
);
let accessibility = document.node(node).accessibility.as_ref().unwrap();
assert_eq!(accessibility.role, AccessibilityRole::RadioButton);
assert_eq!(accessibility.checked, Some(AccessibilityChecked::True));
assert_eq!(
document.node(node).action.as_ref(),
Some(&WidgetActionBinding::action("choice.compact"))
);
assert_eq!(document.node(node).children.len(), 2);
}
#[test]
fn radio_group_adds_one_button_per_option() {
let mut document = UiDocument::new(root_style(240.0, 120.0));
let root = document.root;
let group = radio_group(
&mut document,
root,
"density",
&[
RadioOption::new("compact", "Compact"),
RadioOption::new("comfortable", "Comfortable"),
],
Some("comfortable"),
RadioGroupOptions::default(),
);
assert_eq!(document.node(group).children.len(), 2);
let selected = document.node(document.node(group).children[1]);
assert_eq!(
selected.accessibility.as_ref().unwrap().checked,
Some(AccessibilityChecked::True)
);
}
}