use skia_safe::Color;
use crate::control::{Control, GestureCx, Handled, Has, part};
use crate::controls::control_style::PrebuiltControlStyle;
use crate::controls::label::{LabelBuild, LabelSet, SkiaLabel};
use crate::controls::layout::{LayoutProps, SkiaLayout};
use crate::controls::shape::{ShapeBuild, ShapeSet, ShapeType, SkiaShape};
use crate::controls::toggle::{SkiaToggle, ToggleColor, ToggleProps};
use crate::gestures::{Gesture, GestureKind};
use crate::props;
use crate::tree::{Build, ControlId, Cx, Handle};
use crate::types::{CacheType, Dirty, LayoutOptions, Thickness};
use crate::ui::Aria;
const DEFAULT_ACCENT: Color = Color::from_rgb(0xDC, 0x14, 0x3C);
const DEFAULT_OUTLINE: Color = Color::from_rgb(0x8E, 0x95, 0x9D);
props!(RadioProps, RadioBuild, RadioSet {
text / set_text: String = String::new(), APPLY;
group_name / set_group_name: String = String::new(), NONE;
});
pub struct SkiaRadioButton {
toggle: SkiaToggle,
pub p: RadioProps,
off: Handle<SkiaShape>,
on: Handle<SkiaShape>,
dot: Handle<SkiaShape>,
label: Handle<SkiaLabel>,
}
impl SkiaRadioButton {
#[allow(clippy::new_ret_no_self)]
pub fn new(text: impl Into<String>) -> Build<SkiaRadioButton> {
let radio = SkiaRadioButton {
toggle: SkiaToggle::new(),
p: RadioProps { text: text.into(), ..RadioProps::default() },
off: Handle::default(),
on: Handle::default(),
dot: Handle::default(),
label: Handle::default(),
};
let mut build = Build::new(radio).use_cache(CacheType::Image);
let id = build.id();
build.control_mut().toggle.id = id;
build
}
fn style_default(style: PrebuiltControlStyle, which: ToggleColor) -> Option<Color> {
use PrebuiltControlStyle::*;
use ToggleColor::*;
let rgb = |r, g, b| Some(Color::from_rgb(r, g, b));
match (style, which) {
(Cupertino, ThumbOff) => rgb(0xBF, 0xBF, 0xBF),
(Cupertino, ThumbOn) => rgb(0x00, 0x7A, 0xFF),
(Material, ThumbOff) => rgb(0x75, 0x75, 0x75),
(Material, ThumbOn) => rgb(0x21, 0x96, 0xF3),
(Material3, ThumbOff) => rgb(0x49, 0x45, 0x4F),
(Material3, ThumbOn) => rgb(0x67, 0x50, 0xA4),
(Windows, ThumbOff) => rgb(0x76, 0x76, 0x76),
(Windows, ThumbOn) => rgb(0x00, 0x78, 0xD7),
(_, ThumbOff) => Some(DEFAULT_OUTLINE),
(_, ThumbOn) => Some(DEFAULT_ACCENT),
_ => None,
}
}
fn color(&self, which: ToggleColor) -> Color {
self.toggle.color(which, Self::style_default(self.toggle.using_control_style(), which))
}
fn build_content(&mut self, cx: &mut Cx) {
let (mut off_handle, mut on_handle, mut dot_handle, mut label_handle) = Default::default();
self.toggle.rebuild(cx, |style| {
use PrebuiltControlStyle::*;
let flat = style == Unset;
let circle = || SkiaShape::new().shape_type(ShapeType::Circle).fill();
let mut off = circle();
let mut on = circle().tag("On");
let mut dot = None;
match style {
Cupertino => {
off = off.stroke_width(1.5);
dot = Some(6.5);
}
Windows => {
off = off.stroke_width(1.0);
on = on.stroke_width(5.0);
}
Material | Material3 => {
off = off.stroke_width(2.0);
on = on.stroke_width(2.0);
dot = Some(5.0);
}
_ => {
off = off.stroke_width(2.0);
on = on.stroke_width(2.0);
dot = Some(4.0);
}
}
let dot = dot.map(|margin| {
let dot = circle().margin(margin).assign(&mut dot_handle);
if style == Cupertino { dot.background_color(Color::WHITE) } else { dot }
});
let indicator = SkiaLayout::new()
.height_request(if flat { 18.0 } else { 20.0 })
.lock_ratio(1.0)
.vertical_options(LayoutOptions::Center)
.children((off.assign(&mut off_handle), on.assign(&mut on_handle).children(dot)));
let label = SkiaLabel::new("")
.tag("Text")
.margin(Thickness::new(if flat { 26.0 } else { 28.0 }, 0.0, 0.0, 0.0))
.font_size(14.0)
.max_lines(2)
.text_color(Color::BLACK)
.vertical_options(LayoutOptions::Center)
.accessibility_role(Aria::PRESENTATION)
.assign(&mut label_handle);
vec![indicator.into(), label.into()]
});
(self.off, self.on, self.dot, self.label) = (off_handle, on_handle, dot_handle, label_handle);
self.toggle.pin_minimum_height(cx, 24.0);
}
fn apply(&mut self, cx: &mut Cx) {
let (on, off_color, on_color) = (self.toggle.p.is_toggled, self.color(ToggleColor::ThumbOff), self.color(ToggleColor::ThumbOn));
let cupertino = self.toggle.using_control_style() == PrebuiltControlStyle::Cupertino;
if let Some(mut off) = cx.get_mut(self.off) {
off.set_stroke_color(off_color);
}
if let Some(mut indicator) = cx.get_mut(self.on) {
if cupertino {
indicator.set_background_color(on_color);
} else {
indicator.set_stroke_color(on_color);
}
indicator.set_is_visible(on);
}
if !cupertino && let Some(mut dot) = cx.get_mut(self.dot) {
dot.set_background_color(on_color);
}
if let Some(mut label) = cx.get_mut(self.label) {
label.set_text(self.p.text.clone());
}
}
fn others_on(&self, cx: &Cx) -> Vec<ControlId> {
let me = self.toggle.id;
let is_on = |id: ControlId, same: &dyn Fn(&SkiaRadioButton) -> bool| {
let node = cx.tree.node(id)?;
let radio = part::<SkiaRadioButton>(node.kind.as_deref()?)?;
(id != me && radio.toggle.p.is_toggled && same(radio)).then_some(id)
};
if self.p.group_name.is_empty() {
let Some(parent) = cx.tree.parent(me) else { return Vec::new() };
cx.tree.children(parent).iter().filter_map(|c| is_on(*c, &|r| r.p.group_name.is_empty())).collect()
} else {
let name = self.p.group_name.as_str();
cx.tree.nodes.iter().flatten().filter_map(|n| is_on(n.id, &|r| r.p.group_name == name)).collect()
}
}
}
impl Has<RadioProps> for SkiaRadioButton {
fn part(&self) -> &RadioProps {
&self.p
}
fn part_mut(&mut self) -> &mut RadioProps {
&mut self.p
}
}
impl Has<ToggleProps> for SkiaRadioButton {
fn part(&self) -> &ToggleProps {
&self.toggle.p
}
fn part_mut(&mut self) -> &mut ToggleProps {
&mut self.toggle.p
}
}
impl Has<LayoutProps> for SkiaRadioButton {
fn part(&self) -> &LayoutProps {
Has::<LayoutProps>::part(&self.toggle)
}
fn part_mut(&mut self) -> &mut LayoutProps {
Has::<LayoutProps>::part_mut(&mut self.toggle)
}
}
impl Control for SkiaRadioButton {
fn inner(&self) -> Option<&dyn Control> {
Some(&self.toggle)
}
fn inner_mut(&mut self) -> Option<&mut dyn Control> {
Some(&mut self.toggle)
}
fn on_props_changed(&mut self, cx: &mut Cx) {
if self.toggle.needs_content() {
self.build_content(cx);
}
let came_on = self.toggle.p.is_toggled && self.toggle.reported() != Some(true);
self.toggle.take_change(cx);
if came_on {
for other in self.others_on(cx) {
if let Some(mut radio) = cx.tree.find_mut::<SkiaRadioButton>(other) {
radio.control_mut().toggle.p.is_toggled = false;
radio.mark(Dirty::APPLY);
}
}
}
self.apply(cx);
}
fn on_gesture(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
match gesture.kind {
GestureKind::Tapped if !self.toggle.p.is_toggled && self.toggle.p.responds_to_gestures => {
self.toggle.p.is_toggled = true;
cx.invalidate(Dirty::APPLY);
Handled::Tapped
}
_ => Handled::No,
}
}
fn accessibility_label(&self) -> Option<std::borrow::Cow<'_, str>> {
(!self.p.text.is_empty()).then(|| self.p.text.as_str().into())
}
fn accessibility_role(&self) -> Option<&'static str> {
Some(Aria::RADIO)
}
}