use crate::get_global_color;
use egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, FontId};
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialRadio<'a, T: PartialEq + Clone> {
selected: &'a mut Option<T>,
value: T,
text: String,
enabled: bool,
toggleable: bool,
fill_color: Option<Color32>,
overlay_color: Option<Color32>,
background_color: Option<Color32>,
inner_radius: Option<f32>,
splash_radius: Option<f32>,
}
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialRadioGroup<'a, T: PartialEq + Clone> {
selected: &'a mut Option<T>,
options: Vec<RadioOption<T>>,
enabled: bool,
toggleable: bool,
}
pub struct RadioOption<T: PartialEq + Clone> {
text: String,
value: T,
}
impl<'a, T: PartialEq + Clone> MaterialRadio<'a, T> {
pub fn new(selected: &'a mut Option<T>, value: T, text: impl Into<String>) -> Self {
Self {
selected,
value,
text: text.into(),
enabled: true,
toggleable: false,
fill_color: None,
overlay_color: None,
background_color: None,
inner_radius: None,
splash_radius: None,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn toggleable(mut self, toggleable: bool) -> Self {
self.toggleable = toggleable;
self
}
pub fn fill_color(mut self, color: Color32) -> Self {
self.fill_color = Some(color);
self
}
pub fn overlay_color(mut self, color: Color32) -> Self {
self.overlay_color = Some(color);
self
}
pub fn background_color(mut self, color: Color32) -> Self {
self.background_color = Some(color);
self
}
pub fn inner_radius(mut self, radius: f32) -> Self {
self.inner_radius = Some(radius);
self
}
pub fn splash_radius(mut self, radius: f32) -> Self {
self.splash_radius = Some(radius);
self
}
}
impl<'a, T: PartialEq + Clone> Widget for MaterialRadio<'a, T> {
fn ui(self, ui: &mut Ui) -> Response {
let desired_size = Vec2::new(ui.available_width().min(300.0), 24.0);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
let is_selected = self.selected.as_ref() == Some(&self.value);
if response.clicked() && self.enabled {
if self.toggleable && is_selected {
*self.selected = None;
} else {
*self.selected = Some(self.value.clone());
}
response.mark_changed();
}
let primary = self.fill_color.unwrap_or_else(|| get_global_color("primary")); let on_surface = get_global_color("onSurface"); let on_surface_variant = get_global_color("onSurfaceVariant"); let outline = get_global_color("outline");
let radio_size = 20.0;
let radio_rect = Rect::from_min_size(
Pos2::new(rect.min.x, rect.center().y - radio_size / 2.0),
Vec2::splat(radio_size),
);
let (border_color, fill_color, inner_color) = if !self.enabled {
let disabled_color = on_surface_variant.linear_multiply(0.38);
(disabled_color, Color32::TRANSPARENT, disabled_color)
} else if is_selected {
(primary, self.background_color.unwrap_or(Color32::TRANSPARENT), primary)
} else if response.hovered() {
let hover_overlay = self.overlay_color.unwrap_or_else(||
on_surface.linear_multiply(0.08)
);
(
outline, hover_overlay,
on_surface_variant,
)
} else {
(outline, self.background_color.unwrap_or(Color32::TRANSPARENT), on_surface_variant)
};
if fill_color != Color32::TRANSPARENT {
ui.painter()
.circle_filled(radio_rect.center(), radio_size / 2.0 + 8.0, fill_color);
}
ui.painter().circle_stroke(
radio_rect.center(),
radio_size / 2.0,
Stroke::new(2.0, border_color),
);
if is_selected {
let inner_radius = self.inner_radius.unwrap_or(radio_size / 4.0);
ui.painter()
.circle_filled(radio_rect.center(), inner_radius, inner_color);
}
if !self.text.is_empty() {
let text_pos = Pos2::new(radio_rect.max.x + 8.0, rect.center().y);
let text_color = if self.enabled {
on_surface
} else {
on_surface_variant.linear_multiply(0.38)
};
ui.painter().text(
text_pos,
egui::Align2::LEFT_CENTER,
&self.text,
egui::FontId::default(),
text_color,
);
}
if response.hovered() && self.enabled {
let ripple_color = self.overlay_color.unwrap_or_else(|| {
if is_selected {
primary.linear_multiply(0.08)
} else {
on_surface.linear_multiply(0.08)
}
});
let ripple_radius = self.splash_radius.unwrap_or(radio_size / 2.0 + 12.0);
ui.painter()
.circle_filled(radio_rect.center(), ripple_radius, ripple_color);
}
response
}
}
impl<'a, T: PartialEq + Clone> MaterialRadioGroup<'a, T> {
pub fn new(selected: &'a mut Option<T>) -> Self {
Self {
selected,
options: Vec::new(),
enabled: true,
toggleable: false,
}
}
pub fn option(mut self, value: T, text: impl Into<String>) -> Self {
self.options.push(RadioOption {
text: text.into(),
value,
});
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn toggleable(mut self, toggleable: bool) -> Self {
self.toggleable = toggleable;
self
}
}
impl<'a, T: PartialEq + Clone> Widget for MaterialRadioGroup<'a, T> {
fn ui(self, ui: &mut Ui) -> Response {
let mut group_response = None;
ui.vertical(|ui| {
for option in self.options {
let radio = MaterialRadio::new(self.selected, option.value, option.text)
.enabled(self.enabled)
.toggleable(self.toggleable);
let response = ui.add(radio);
if group_response.is_none() {
group_response = Some(response);
} else if let Some(ref mut group_resp) = group_response {
*group_resp = group_resp.union(response);
}
}
});
group_response.unwrap_or_else(|| {
let (_rect, response) = ui.allocate_exact_size(Vec2::ZERO, Sense::hover());
response
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListTileControlAffinity {
Leading,
Trailing,
}
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct RadioListTile<'a, T: PartialEq + Clone> {
selected: &'a mut Option<T>,
value: T,
title: Option<String>,
subtitle: Option<String>,
enabled: bool,
toggleable: bool,
control_affinity: ListTileControlAffinity,
dense: bool,
fill_color: Option<Color32>,
tile_color: Option<Color32>,
selected_tile_color: Option<Color32>,
}
impl<'a, T: PartialEq + Clone> RadioListTile<'a, T> {
pub fn new(selected: &'a mut Option<T>, value: T) -> Self {
Self {
selected,
value,
title: None,
subtitle: None,
enabled: true,
toggleable: false,
control_affinity: ListTileControlAffinity::Leading,
dense: false,
fill_color: None,
tile_color: None,
selected_tile_color: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn toggleable(mut self, toggleable: bool) -> Self {
self.toggleable = toggleable;
self
}
pub fn control_affinity(mut self, affinity: ListTileControlAffinity) -> Self {
self.control_affinity = affinity;
self
}
pub fn dense(mut self, dense: bool) -> Self {
self.dense = dense;
self
}
pub fn fill_color(mut self, color: Color32) -> Self {
self.fill_color = Some(color);
self
}
pub fn tile_color(mut self, color: Color32) -> Self {
self.tile_color = Some(color);
self
}
pub fn selected_tile_color(mut self, color: Color32) -> Self {
self.selected_tile_color = Some(color);
self
}
}
impl<'a, T: PartialEq + Clone> Widget for RadioListTile<'a, T> {
fn ui(self, ui: &mut Ui) -> Response {
let is_selected = self.selected.as_ref() == Some(&self.value);
let height = if self.dense {
if self.subtitle.is_some() { 48.0 } else { 40.0 }
} else {
if self.subtitle.is_some() { 64.0 } else { 48.0 }
};
let available_width = ui.available_width();
let desired_size = Vec2::new(available_width, height);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked() && self.enabled {
if self.toggleable && is_selected {
*self.selected = None;
} else {
*self.selected = Some(self.value.clone());
}
response.mark_changed();
}
let on_surface = get_global_color("onSurface"); let on_surface_variant = get_global_color("onSurfaceVariant"); let surface_variant = get_global_color("surfaceVariant");
let bg_color = if is_selected {
self.selected_tile_color.unwrap_or_else(||
surface_variant.linear_multiply(0.5)
)
} else if response.hovered() && self.enabled {
self.tile_color.unwrap_or_else(||
on_surface.linear_multiply(0.04)
)
} else {
self.tile_color.unwrap_or(Color32::TRANSPARENT)
};
if bg_color != Color32::TRANSPARENT {
ui.painter().rect_filled(rect, 4.0, bg_color);
}
let radio_size = 20.0;
let padding = 16.0;
let gap = 16.0;
let (radio_x, text_x) = match self.control_affinity {
ListTileControlAffinity::Leading => {
let radio_x = rect.min.x + padding + radio_size / 2.0;
let text_x = radio_x + radio_size / 2.0 + gap;
(radio_x, text_x)
}
ListTileControlAffinity::Trailing => {
let radio_x = rect.max.x - padding - radio_size / 2.0;
let text_x = rect.min.x + padding;
(radio_x, text_x)
}
};
let radio_center = Pos2::new(radio_x, rect.center().y);
let primary = self.fill_color.unwrap_or_else(|| get_global_color("primary")); let outline = get_global_color("outline");
let (border_color, inner_color) = if !self.enabled {
let disabled_color = on_surface_variant.linear_multiply(0.38);
(disabled_color, disabled_color)
} else if is_selected {
(primary, primary)
} else {
(outline, outline)
};
ui.painter().circle_stroke(
radio_center,
radio_size / 2.0,
Stroke::new(2.0, border_color),
);
if is_selected {
ui.painter().circle_filled(radio_center, radio_size / 4.0, inner_color);
}
let text_color = if self.enabled {
on_surface } else {
on_surface_variant.linear_multiply(0.38) };
let _text_rect_width = match self.control_affinity {
ListTileControlAffinity::Leading => rect.max.x - text_x - padding,
ListTileControlAffinity::Trailing => radio_x - radio_size / 2.0 - gap - text_x,
};
if let Some(title) = &self.title {
let title_y = if self.subtitle.is_some() {
rect.min.y + height * 0.35
} else {
rect.center().y
};
let title_font = if self.dense {
FontId::proportional(14.0)
} else {
FontId::proportional(16.0)
};
ui.painter().text(
Pos2::new(text_x, title_y),
egui::Align2::LEFT_CENTER,
title,
title_font,
text_color,
);
}
if let Some(subtitle) = &self.subtitle {
let subtitle_y = rect.min.y + height * 0.65;
let subtitle_font = FontId::proportional(if self.dense { 12.0 } else { 14.0 });
ui.painter().text(
Pos2::new(text_x, subtitle_y),
egui::Align2::LEFT_CENTER,
subtitle,
subtitle_font,
on_surface_variant,
);
}
response
}
}
pub fn radio<'a, T: PartialEq + Clone>(
selected: &'a mut Option<T>,
value: T,
text: impl Into<String>,
) -> MaterialRadio<'a, T> {
MaterialRadio::new(selected, value, text)
}
pub fn radio_group<'a, T: PartialEq + Clone>(selected: &'a mut Option<T>) -> MaterialRadioGroup<'a, T> {
MaterialRadioGroup::new(selected)
}
pub fn radio_list_tile<'a, T: PartialEq + Clone>(
selected: &'a mut Option<T>,
value: T,
) -> RadioListTile<'a, T> {
RadioListTile::new(selected, value)
}