#![allow(non_snake_case)]
use std::cell::Cell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use repose_core::*;
use repose_ui::anim::{animate_color, animate_f32};
use repose_ui::{Box, Button, Column, Row, Stack, Text, TextStyle, ViewExt};
use crate::{Icon, Symbol};
pub fn TopAppBar(
title: impl Into<String>,
navigation_icon: Option<View>,
actions: Vec<View>,
) -> View {
let th = theme();
Row(Modifier::new()
.fill_max_width()
.height(64.0)
.background(th.surface)
.padding_values(PaddingValues {
left: 4.0,
right: 4.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center))
.child((
navigation_icon.unwrap_or(Box(Modifier::new().size(16.0, 1.0))),
Box(Modifier::new()
.padding_values(PaddingValues {
left: 16.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
})
.flex_grow(1.0))
.child(
Text(title)
.color(th.on_surface)
.size(th.typography.title_large),
),
Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
))
}
pub fn IconButton(icon: View, on_click: impl Fn() + 'static) -> View {
let th = theme();
let bg = Color::TRANSPARENT;
Box(Modifier::new()
.size(40.0, 40.0)
.clip_rounded(20.0)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08),
pressed: th.on_surface.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click()))
.child(icon)
}
pub fn FilledIconButton(icon: View, on_click: impl Fn() + 'static) -> View {
let th = theme();
let bg = th.primary;
Box(Modifier::new()
.size(40.0, 40.0)
.clip_rounded(20.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_primary.with_alpha_f32(0.08),
pressed: th.on_primary.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click()))
.child(icon)
}
pub fn FilledButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
) -> View {
let th = theme();
let content = with_content_color(th.on_primary, content);
let bg = th.primary;
Box(Modifier::new()
.height(40.0)
.min_width(48.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_primary.with_alpha_f32(0.08),
pressed: th.on_primary.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: 0.0,
hovered: 1.0,
pressed: 8.0,
disabled: 0.0,
})
.clip_rounded(20.0)
.padding_values(PaddingValues {
left: 24.0,
right: 24.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click())
.then(modifier))
.child(content)
}
pub fn FilledTonalButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
) -> View {
let th = theme();
let content = with_content_color(th.on_secondary_container, content);
let bg = th.secondary_container;
Box(Modifier::new()
.height(40.0)
.min_width(48.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_secondary_container.with_alpha_f32(0.08),
pressed: th.on_secondary_container.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: 0.0,
hovered: 1.0,
pressed: 8.0,
disabled: 0.0,
})
.clip_rounded(20.0)
.padding_values(PaddingValues {
left: 24.0,
right: 24.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click())
.then(modifier))
.child(content)
}
pub fn OutlinedButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
) -> View {
let th = theme();
let content = with_content_color(th.on_surface, content);
let bg = Color::TRANSPARENT;
Box(Modifier::new()
.height(40.0)
.min_width(48.0)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08),
pressed: th.on_surface.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
})
.border(1.0, th.outline_variant, 20.0)
.clip_rounded(20.0)
.padding_values(PaddingValues {
left: 24.0,
right: 24.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click())
.then(modifier))
.child(content)
}
pub fn TextButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
) -> View {
let th = theme();
let content = with_content_color(th.on_surface, content);
let bg = Color::TRANSPARENT;
Box(Modifier::new()
.height(40.0)
.min_width(48.0)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08),
pressed: th.on_surface.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
})
.clip_rounded(20.0)
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click())
.then(modifier))
.child(content)
}
pub fn ElevatedButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
) -> View {
let th = theme();
let content = with_content_color(th.primary, content);
let bg = th.surface_container_low;
Box(Modifier::new()
.height(40.0)
.min_width(48.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.primary.with_alpha_f32(0.08),
pressed: th.primary.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: th.elevation.level1,
hovered: th.elevation.level2,
pressed: th.elevation.level3,
disabled: 0.0,
})
.clip_rounded(20.0)
.padding_values(PaddingValues {
left: 24.0,
right: 24.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click())
.then(modifier))
.child(content)
}
pub fn FAB(icon: View, on_click: impl Fn() + 'static) -> View {
let th = theme();
let bg = th.primary_container;
Box(Modifier::new()
.size(56.0, 56.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_primary_container.with_alpha_f32(0.08),
pressed: th.on_primary_container.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: 6.0,
hovered: 8.0,
pressed: 12.0,
disabled: 0.0,
})
.clip_rounded(28.0)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click()))
.child(icon)
}
pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static) -> View {
let th = theme();
let bg = th.primary_container;
Box(Modifier::new()
.size(96.0, 96.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_primary_container.with_alpha_f32(0.08),
pressed: th.on_primary_container.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: 6.0,
hovered: 8.0,
pressed: 12.0,
disabled: 0.0,
})
.clip_rounded(28.0)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| on_click()))
.child(icon)
}
pub fn ExtendedFAB(
icon: Option<View>,
label: impl Into<String>,
on_click: impl Fn() + 'static,
) -> View {
let th = theme();
let has_icon = icon.is_some();
let bg = th.primary_container;
Row(Modifier::new()
.height(56.0)
.min_width(80.0)
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_primary_container.with_alpha_f32(0.08),
pressed: th.on_primary_container.with_alpha_f32(0.12),
disabled: th.on_surface.with_alpha_f32(0.12),
})
.state_elevation(StateElevation {
default: 6.0,
hovered: 8.0,
pressed: 12.0,
disabled: 0.0,
})
.clip_rounded(16.0)
.padding_values(PaddingValues {
left: 16.0,
right: 20.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.clickable()
.on_pointer_down(move |_| on_click()))
.child((
icon.unwrap_or(Box(Modifier::new())),
Box(Modifier::new().size(if has_icon { 12.0 } else { 0.0 }, 1.0)),
Text(label)
.color(th.on_primary_container)
.size(th.typography.label_large)
.single_line(),
))
}
pub fn Divider() -> View {
let th = theme();
Box(Modifier::new()
.fill_max_width()
.height(1.0)
.background(th.outline_variant))
}
pub fn VerticalDivider() -> View {
let th = theme();
Box(Modifier::new()
.width(1.0)
.fill_max_height()
.background(th.outline_variant))
}
pub fn Badge(label: Option<impl Into<String>>) -> View {
let th = theme();
match label {
None => Box(Modifier::new()
.size(6.0, 6.0)
.background(th.error)
.clip_rounded(3.0)),
Some(text) => {
let text = text.into();
Box(Modifier::new()
.min_width(16.0)
.height(16.0)
.background(th.error)
.clip_rounded(8.0)
.padding_values(PaddingValues {
left: 4.0,
right: 4.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center))
.child(
Text(text)
.color(th.on_error)
.size(th.typography.label_small)
.single_line(),
)
}
}
}
pub fn ListItem(
headline: impl Into<String>,
supporting_text: Option<String>,
leading: Option<View>,
trailing: Option<View>,
on_click: Option<Rc<dyn Fn()>>,
) -> View {
let th = theme();
let mut modifier = Modifier::new()
.fill_max_width()
.min_height(if supporting_text.is_some() {
72.0
} else {
56.0
})
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08),
pressed: th.on_surface.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
})
.padding_values(PaddingValues {
left: 16.0,
right: 24.0,
top: 8.0,
bottom: 8.0,
})
.align_items(AlignItems::Center);
if let Some(cb) = on_click {
modifier = modifier.clickable().on_pointer_down(move |_| cb());
}
Row(modifier).child((
leading
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 0.0,
right: 16.0,
top: 0.0,
bottom: 0.0,
}))
.child(v)
})
.unwrap_or(Box(Modifier::new())),
Column(
Modifier::new()
.flex_grow(1.0)
.justify_content(JustifyContent::Center),
)
.child((
Text(headline)
.color(th.on_surface)
.size(th.typography.body_large)
.single_line(),
supporting_text
.map(|st| {
Text(st)
.color(th.on_surface_variant)
.size(th.typography.body_medium)
.max_lines(2)
.overflow_ellipsize()
})
.unwrap_or(Box(Modifier::new())),
)),
trailing
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 16.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
}))
.child(v)
})
.unwrap_or(Box(Modifier::new())),
))
}
pub struct Tab {
pub label: String,
pub icon: Option<View>,
pub on_click: Rc<dyn Fn()>,
}
static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn TabRow(selected_index: usize, tabs: Vec<Tab>) -> View {
let th = theme();
let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color;
Column(Modifier::new().fill_max_width()).child((
Row(Modifier::new()
.fill_max_width()
.height(48.0)
.background(th.surface))
.child(
tabs.into_iter()
.enumerate()
.map(|(i, tab)| {
let selected = i == selected_index;
let color = animate_color(
format!("tab_clr_{}_{}", id, i),
if selected {
th.primary
} else {
th.on_surface_variant
},
spec,
);
let indicator_h = animate_f32(
format!("tab_ind_{}_{}", id, i),
if selected { 3.0 } else { 0.0 },
spec,
);
let cb = tab.on_click.clone();
Column(
Modifier::new()
.flex_grow(1.0)
.fill_max_height()
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08),
pressed: th.on_surface.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
})
.clickable()
.on_pointer_down(move |_| cb()),
)
.child((
tab.icon.unwrap_or(Box(Modifier::new())),
Text(tab.label)
.color(color)
.size(th.typography.title_small)
.single_line(),
Box(Modifier::new()
.fill_max_width()
.height(indicator_h)
.background(th.primary)
.clip_rounded(1.5)),
))
})
.collect::<Vec<_>>(),
),
Box(Modifier::new()
.fill_max_width()
.height(1.0)
.background(th.outline_variant)),
))
}
pub struct Segment {
pub label: String,
pub icon: Option<View>,
pub on_click: Rc<dyn Fn()>,
}
static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn SegmentedButton(selected: &[usize], segments: Vec<Segment>) -> View {
let th = theme();
let count = segments.len();
let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color;
Row(Modifier::new()
.height(40.0)
.border(1.0, th.outline, 20.0)
.clip_rounded(20.0))
.child(
segments
.into_iter()
.enumerate()
.map(|(i, seg)| {
let is_selected = selected.contains(&i);
let bg = animate_color(
format!("sb_bg_{}_{}", id, i),
if is_selected {
th.secondary_container
} else {
Color::TRANSPARENT
},
spec,
);
let fg = animate_color(
format!("sb_fg_{}_{}", id, i),
if is_selected {
th.on_secondary_container
} else {
th.on_surface
},
spec,
);
let cb = seg.on_click.clone();
let mut modifier = Modifier::new()
.flex_grow(1.0)
.fill_max_height()
.background(bg)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 0.0,
bottom: 0.0,
})
.clickable()
.on_pointer_down(move |_| cb());
if i < count - 1 {
modifier = modifier.border(1.0, th.outline, 0.0);
}
Row(modifier).child((
seg.icon.unwrap_or(Box(Modifier::new())),
Text(seg.label)
.color(fg)
.size(th.typography.label_large)
.single_line(),
))
})
.collect::<Vec<_>>(),
)
}
pub fn CircularProgressIndicator(value: Option<f32>) -> View {
View::new(
0,
ViewKind::ProgressBar {
value: value.unwrap_or(0.0),
min: 0.0,
max: 1.0,
circular: true,
},
)
.modifier(Modifier::new().size(48.0, 48.0))
.semantics(Semantics {
role: Role::ProgressBar,
label: None,
focused: false,
enabled: true,
})
}
pub fn LinearProgressIndicator(value: Option<f32>) -> View {
View::new(
0,
ViewKind::ProgressBar {
value: value.unwrap_or(0.0),
min: 0.0,
max: 1.0,
circular: false,
},
)
.modifier(Modifier::new().fill_max_width().height(4.0))
.semantics(Semantics {
role: Role::ProgressBar,
label: None,
focused: false,
enabled: true,
})
}
#[derive(Clone)]
pub struct OutlinedTextFieldConfig {
pub label: Option<String>,
pub placeholder: Option<String>,
pub leading_icon: Option<View>,
pub trailing_icon: Option<View>,
pub single_line: bool,
pub is_error: bool,
pub enabled: bool,
pub on_submit: Option<Rc<dyn Fn(String)>>,
}
impl Default for OutlinedTextFieldConfig {
fn default() -> Self {
Self {
label: None,
placeholder: None,
leading_icon: None,
trailing_icon: None,
single_line: true,
is_error: false,
enabled: true,
on_submit: None,
}
}
}
pub fn OutlinedTextField(
modifier: Modifier,
value: String,
on_value_change: impl Fn(String) + 'static,
config: OutlinedTextFieldConfig,
) -> View {
let th = theme();
let label_str: Option<Rc<str>> = config.label.map(|s| Rc::from(s));
let has_label = label_str.is_some();
let anim_key = match &label_str {
Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
None => "otf_nolabel".into(),
};
let focus_tracker: Rc<Cell<bool>> =
remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
let is_focused = focus_tracker.get();
let should_float = !value.is_empty() || is_focused;
let float_t = animate_f32(
anim_key.clone(),
if should_float { 1.0 } else { 0.0 },
th.motion.color,
);
let border_color = if config.is_error {
th.error
} else if float_t > 0.5 {
th.primary
} else {
th.outline
};
let label_color = if config.is_error {
th.error
} else if float_t > 0.5 {
th.primary
} else {
th.on_surface_variant
};
let label_size = 16.0 - 4.0 * float_t;
let label_y = 16.0 - 20.0 * float_t;
let tf_placeholder = if has_label {
String::new()
} else {
config.placeholder.unwrap_or_default()
};
Box(modifier
.clip_rounded(th.shapes.small)
.border(1.0, border_color, th.shapes.small)
.background(th.surface))
.child(
Stack(Modifier::new().fill_max_size()).child((
Row(Modifier::new()
.fill_max_size()
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 16.0,
bottom: 8.0,
})
.align_items(AlignItems::Center))
.child((
config.leading_icon.unwrap_or(Box(Modifier::new())),
View::new(
0,
ViewKind::TextField {
state_key: 0,
hint: tf_placeholder,
multiline: false,
on_change: Some(Rc::new(on_value_change) as _),
on_submit: config.on_submit.clone().map(|f| {
let f = f.clone();
Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
}),
focus_tracker: Some(focus_tracker.clone()),
value: value.clone(),
},
)
.modifier(
Modifier::new()
.flex_grow(1.0)
.padding_values(PaddingValues {
left: 8.0,
right: 8.0,
top: 0.0,
bottom: 0.0,
}),
)
.semantics(Semantics {
role: Role::TextField,
label: None,
focused: false,
enabled: true,
}),
config.trailing_icon.unwrap_or(Box(Modifier::new())),
)),
if let Some(lbl) = label_str {
Box(Modifier::new()
.fill_max_width()
.padding_values(PaddingValues {
left: 20.0,
right: 20.0,
top: 0.0,
bottom: 0.0,
})
.absolute()
.offset(Some(0.0), Some(label_y), None, None))
.child(
Box(Modifier::new()
.background(th.surface)
.padding_values(PaddingValues {
left: 4.0,
right: 4.0,
top: 2.0,
bottom: 2.0,
}))
.child(
Text(lbl.as_ref().to_string())
.color(label_color)
.size(label_size),
),
)
} else {
Box(Modifier::new())
},
)),
)
}
static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
let th = theme();
let sz = 18.0;
let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color_fast;
let fill = animate_color(
format!("cb_fill_{}", id),
if checked {
th.primary
} else {
Color::TRANSPARENT
},
spec,
);
let bd_w = animate_f32(
format!("cb_bw_{}", id),
if checked { 0.0 } else { 2.0 },
spec,
);
let bd = animate_color(
format!("cb_bd_{}", id),
if checked {
Color::TRANSPARENT
} else {
th.on_surface_variant
},
spec,
);
let check_alpha = animate_f32(
format!("cb_ca_{}", id),
if checked { 1.0 } else { 0.0 },
spec,
);
Button(
Box(Modifier::new()
.size(sz, sz)
.background(fill)
.border(bd_w, bd, 2.0)
.clip_rounded(2.0)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center))
.child(if check_alpha > 0.01 {
Box(Modifier::new().alpha(check_alpha)).child(
Icon(Symbol::new("done", '\u{E876}'))
.color(th.on_primary)
.size(14.0),
)
} else {
Box(Modifier::new())
}),
move || on_change(!checked),
)
.modifier(
Modifier::new()
.width(40.0)
.height(40.0)
.padding(0.0)
.clip_rounded(20.0)
.background(Color::TRANSPARENT),
)
}
static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn RadioButton(selected: bool, on_select: impl Fn() + 'static) -> View {
let th = theme();
let d = 20.0;
let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
let color_spec = th.motion.color_fast;
let spring = th.motion.spring;
let ring_col = animate_color(
format!("rb_ring_{}", id),
if selected {
th.primary
} else {
th.on_surface_variant
},
color_spec,
);
let dot_size = animate_f32(
format!("rb_dot_{}", id),
if selected { 10.0 } else { 0.0 },
spring,
);
Button(
Box(Modifier::new()
.size(d, d)
.border(2.0, ring_col, d * 0.5)
.clip_rounded(d * 0.5)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center))
.child(if dot_size > 0.5 {
Box(Modifier::new()
.size(dot_size, dot_size)
.background(th.primary)
.clip_rounded(dot_size * 0.5))
} else {
Box(Modifier::new())
}),
on_select,
)
.modifier(
Modifier::new()
.width(40.0)
.height(40.0)
.padding(0.0)
.clip_rounded(20.0)
.background(Color::TRANSPARENT),
)
}
static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
let th = theme();
let track_w = 52.0;
let track_h = 32.0;
let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
let thumb_target_pos = if checked { track_w - 24.0 - 4.0 } else { 8.0 };
let thumb_target_d = if checked { 24.0 } else { 16.0 };
let spring = th.motion.spring;
let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
let thumb_top = (track_h - thumb_d) * 0.5;
let color_spec = th.motion.color_fast;
let track_bg = animate_color(
format!("sw_tbg_{}", id),
if checked {
th.primary
} else {
th.surface_container_highest
},
color_spec,
);
let thumb_bg = animate_color(
format!("sw_tmbg_{}", id),
if checked { th.on_primary } else { th.outline },
color_spec,
);
let track_border = animate_f32(
format!("sw_tb_{}", id),
if checked { 0.0 } else { 2.0 },
color_spec,
);
let border_color = animate_color(
format!("sw_bc_{}", id),
if checked {
Color::TRANSPARENT
} else {
th.outline
},
color_spec,
);
Button(
Box(Modifier::new()
.size(track_w, track_h)
.background(track_bg)
.border(track_border, border_color, track_h * 0.5)
.clip_rounded(track_h * 0.5))
.child(Box(Modifier::new()
.size(thumb_d, thumb_d)
.background(thumb_bg)
.clip_rounded(thumb_d * 0.5)
.absolute()
.offset(Some(thumb_left), Some(thumb_top), None, None))),
move || on_change(!checked),
)
.modifier(
Modifier::new()
.size(track_w, track_h)
.padding(0.0)
.clip_rounded(track_h * 0.5)
.background(Color::TRANSPARENT),
)
}
pub fn M3Slider(
value: f32,
range: (f32, f32),
step: Option<f32>,
on_change: impl Fn(f32) + 'static,
) -> View {
View::new(
0,
ViewKind::Slider {
value,
min: range.0,
max: range.1,
step,
on_change: Some(Rc::new(on_change)),
},
)
.modifier(Modifier::new().height(28.0))
.semantics(Semantics {
role: Role::Slider,
label: None,
focused: false,
enabled: true,
})
}
pub fn M3RangeSlider(
start: f32,
end: f32,
range: (f32, f32),
step: Option<f32>,
on_change: impl Fn(f32, f32) + 'static,
) -> View {
View::new(
0,
ViewKind::RangeSlider {
start,
end,
min: range.0,
max: range.1,
step,
on_change: Some(Rc::new(on_change)),
},
)
.modifier(Modifier::new().height(28.0))
.semantics(Semantics {
role: Role::Slider,
label: None,
focused: false,
enabled: true,
})
}