#![allow(non_snake_case)]
mod components;
pub use components::*;
pub mod dialog;
pub use dialog::*;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use repose_core::*;
use repose_ui::lazy::{LazyRow, LazyRowState};
use repose_ui::{
Box, Column, Row, Spacer, Stack, Surface, Text, TextField, TextStyle, ViewExt,
anim::{animate_color, animate_f32, animate_f32_from},
overlay::OverlayHandle,
overlay::SnackbarAction,
overlay::snackbar_is_dismissing,
};
pub fn AlertDialog(
visible: bool,
on_dismiss: impl Fn() + 'static,
title: View,
text: View,
confirm_button: View,
dismiss_button: Option<View>,
) -> View {
if !visible {
return Box(Modifier::new());
}
let th = theme();
Stack(Modifier::new().fill_max_size()).child((
Box(Modifier::new()
.fill_max_size()
.background(th.scrim.with_alpha(170))
.clickable()
.on_pointer_down(move |_| on_dismiss())),
Surface(
Modifier::new()
.min_width(280.0)
.max_width(560.0)
.background(th.surface_container_high)
.clip_rounded(th.shapes.extra_large)
.padding(24.0),
Column(Modifier::new()).child((
title,
Box(Modifier::new().size(1.0, 16.0)),
text,
Spacer(),
Row(Modifier::new()).child((
dismiss_button.unwrap_or(Box(Modifier::new())),
Spacer(),
confirm_button,
)),
)),
),
))
}
pub fn BottomSheet(
visible: bool,
on_dismiss: impl Fn() + 'static,
modifier: Modifier,
content: View,
) -> View {
let max_h = animate_f32(
"sheet_height",
if visible { 500.0 } else { 0.0 },
theme().motion.layout,
);
Column(Modifier::new()).child((
Box(if visible {
modifier
.clone()
.fill_max_width()
.max_height(max_h)
.clip_rounded(4.0)
} else {
Modifier::new().width(0.0).height(0.0)
})
.child(if visible {
content
} else {
Box(Modifier::new())
}),
if visible {
Box(Modifier::new()
.width(1.0)
.height(0.0)
.fill_max_width()
.hit_passthrough()
.on_pointer_down(move |_| on_dismiss()))
} else {
Box(Modifier::new())
},
))
}
static NAVBAR_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn NavigationBar(selected_index: usize, items: Vec<NavItem>) -> View {
let th = theme();
let id = remember(|| NAVBAR_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.shape;
Row(Modifier::new()
.fill_max_size()
.min_height(80.0)
.background(th.surface_container)
.padding(8.0))
.child(
items
.into_iter()
.enumerate()
.map(|(i, item)| {
let selected = i == selected_index;
let fg = animate_color(
format!("nb_fg_{}_{}", id, i),
if selected {
th.primary
} else {
th.on_surface_variant
},
spec,
);
let bg_alpha = animate_f32(
format!("nb_bg_{}_{}", id, i),
if selected { 1.0 } else { 0.0 },
spec,
);
let indicator_bg = th.primary.with_alpha_f32(bg_alpha * 0.12);
let cb = item.on_click.clone();
Column(
Modifier::new()
.flex_grow(1.0)
.padding_values(PaddingValues {
left: 4.0,
right: 4.0,
top: 6.0,
bottom: 6.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.background(indicator_bg)
.clip_rounded(16.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,
})
.clickable()
.on_pointer_down(move |_| cb()),
)
.child((
item.icon,
Text(item.label)
.color(fg)
.size(th.typography.label_medium)
.single_line(),
))
})
.collect::<Vec<_>>(),
)
}
pub struct NavItem {
pub icon: View,
pub label: String,
pub on_click: Rc<dyn Fn()>,
}
pub fn Card(modifier: Modifier, content: View) -> View {
let th = theme();
Surface(
modifier
.background(th.surface_container_highest)
.clip_rounded(th.shapes.medium),
content,
)
}
pub fn ElevatedCard(modifier: Modifier, content: View) -> View {
let th = theme();
Surface(
modifier
.background(th.surface_container_low)
.state_elevation(StateElevation {
default: th.elevation.level1,
hovered: th.elevation.level2,
pressed: th.elevation.level3,
disabled: 0.0,
})
.clip_rounded(th.shapes.medium),
content,
)
}
pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
let th = theme();
Surface(
modifier
.background(th.surface)
.border(1.0, th.outline_variant, th.shapes.medium)
.clip_rounded(th.shapes.medium),
content,
)
}
fn card_state_colors(bg: Color) -> StateColors {
let th = theme();
StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
}
}
pub fn ClickableCard(on_click: impl Fn() + 'static, modifier: Modifier, content: View) -> View {
let th = theme();
let bg = th.surface_container_highest;
Surface(
modifier
.background(bg)
.state_colors(card_state_colors(bg))
.clip_rounded(th.shapes.medium)
.clickable()
.on_pointer_down(move |_| on_click()),
content,
)
}
pub fn ClickableElevatedCard(
on_click: impl Fn() + 'static,
modifier: Modifier,
content: View,
) -> View {
let th = theme();
let bg = th.surface;
Surface(
modifier
.background(bg)
.state_colors(card_state_colors(bg))
.state_elevation(StateElevation {
default: th.elevation.level1,
hovered: th.elevation.level2,
pressed: th.elevation.level3,
disabled: 0.0,
})
.clip_rounded(th.shapes.medium)
.clickable()
.on_pointer_down(move |_| on_click()),
content,
)
}
pub fn ClickableOutlinedCard(
on_click: impl Fn() + 'static,
modifier: Modifier,
content: View,
) -> View {
let th = theme();
let bg = th.surface;
Surface(
modifier
.background(bg)
.state_colors(card_state_colors(bg))
.border(1.0, th.outline_variant, th.shapes.medium)
.clip_rounded(th.shapes.medium)
.clickable()
.on_pointer_down(move |_| on_click()),
content,
)
}
pub fn Snackbar(
message: impl Into<String>,
action: Option<SnackbarAction>,
modifier: Modifier,
) -> View {
let msg = message.into();
let th = theme();
let bg = th.inverse_surface;
let fg = th.inverse_on_surface;
let action_color = th.inverse_primary;
let dismissing = snackbar_is_dismissing();
let slide_target = if dismissing { 80.0 } else { 0.0 };
let slide = animate_f32_from(
"snackbar_slide",
80.0,
slide_target,
th.motion.overlay,
);
let alpha_target = if dismissing { 0.0 } else { 1.0 };
let alpha = animate_f32_from(
"snackbar_alpha",
0.0,
alpha_target,
th.motion.overlay,
);
let snackbar = Surface(
Modifier::new()
.translate(0.0, slide)
.alpha(alpha)
.background(bg)
.clip_rounded(th.shapes.small)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 12.0,
bottom: 12.0,
})
.min_height(48.0)
.min_width(280.0)
.max_width(600.0),
Row(Modifier::new().align_items(repose_core::AlignItems::Center)).child((
Text(msg)
.color(fg)
.size(th.typography.body_medium)
.max_lines(2)
.overflow_ellipsize(),
Spacer(),
action
.map(|a| {
let label = a.label.clone();
Box(Modifier::new()
.padding_values(PaddingValues {
left: 8.0,
right: 8.0,
top: 6.0,
bottom: 6.0,
})
.clip_rounded(th.shapes.extra_small)
.clickable()
.on_pointer_down(move |_| (a.on_click)()))
.child(
Text(label)
.color(action_color)
.size(th.typography.label_large)
.single_line(),
)
})
.unwrap_or(Box(Modifier::new())),
)),
);
Box(Modifier::new()
.absolute()
.offset_bottom(0.0)
.fill_max_width()
.justify_content(repose_core::JustifyContent::Center)
.then(modifier),
).child(snackbar)
}
pub fn FilterChip(
selected: bool,
on_click: impl Fn() + 'static,
label: View,
leading_icon: Option<View>,
trailing_icon: Option<View>,
) -> View {
let th = theme();
let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color;
let bg = animate_color(
format!("fc_bg_{}", id),
if selected {
th.secondary_container
} else {
th.surface
},
spec,
);
let label_color = animate_color(
format!("fc_lc_{}", id),
if selected {
th.on_secondary_container
} else {
th.on_surface_variant
},
spec,
);
let leading_color = animate_color(
format!("fc_lic_{}", id),
if selected {
th.on_secondary_container
} else {
th.primary
},
spec,
);
Surface(
Modifier::new()
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
disabled: Color::TRANSPARENT,
})
.border(
1.0,
if selected {
Color::TRANSPARENT
} else {
th.outline_variant
},
8.0,
)
.clip_rounded(8.0)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 8.0,
bottom: 8.0,
})
.clickable()
.on_pointer_down(move |_| on_click()),
Row(Modifier::new().align_items(AlignItems::Center)).child((
leading_icon
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 0.0,
right: 8.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(leading_color, move || v))
})
.unwrap_or(Box(Modifier::new())),
with_content_color(label_color, move || label),
trailing_icon
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 8.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(th.on_surface_variant, move || v))
})
.unwrap_or(Box(Modifier::new())),
)),
)
}
pub fn AssistChip(
on_click: impl Fn() + 'static,
label: View,
leading_icon: Option<View>,
trailing_icon: Option<View>,
) -> View {
let th = theme();
Surface(
Modifier::new()
.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, 8.0)
.clip_rounded(8.0)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 8.0,
bottom: 8.0,
})
.clickable()
.on_pointer_down(move |_| on_click()),
Row(Modifier::new().align_items(AlignItems::Center)).child((
leading_icon
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 0.0,
right: 8.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(th.primary, move || v))
})
.unwrap_or(Box(Modifier::new())),
with_content_color(th.on_surface, move || label),
trailing_icon
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 8.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(th.primary, move || v))
})
.unwrap_or(Box(Modifier::new())),
)),
)
}
pub fn SuggestionChip(on_click: impl Fn() + 'static, label: View, icon: Option<View>) -> View {
let th = theme();
Surface(
Modifier::new()
.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, 8.0)
.clip_rounded(8.0)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 8.0,
bottom: 8.0,
})
.clickable()
.on_pointer_down(move |_| on_click()),
Row(Modifier::new().align_items(AlignItems::Center)).child((
icon.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 0.0,
right: 8.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(th.primary, move || v))
})
.unwrap_or(Box(Modifier::new())),
with_content_color(th.on_surface_variant, move || label),
)),
)
}
pub fn InputChip(
selected: bool,
on_click: impl Fn() + 'static,
label: View,
leading_icon: Option<View>,
avatar: Option<View>,
trailing_icon: Option<View>,
) -> View {
let th = theme();
let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color;
let bg = animate_color(
format!("ic_bg_{}", id),
if selected {
th.secondary_container
} else {
th.surface
},
spec,
);
let label_color = animate_color(
format!("ic_lc_{}", id),
if selected {
th.on_secondary_container
} else {
th.on_surface_variant
},
spec,
);
let leading_color = animate_color(
format!("ic_lic_{}", id),
if selected {
th.primary
} else {
th.on_surface_variant
},
spec,
);
Surface(
Modifier::new()
.background(bg)
.state_colors(StateColors {
default: Color::TRANSPARENT,
hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
disabled: Color::TRANSPARENT,
})
.border(
1.0,
if selected {
Color::TRANSPARENT
} else {
th.outline_variant
},
8.0,
)
.clip_rounded(8.0)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 8.0,
bottom: 8.0,
})
.clickable()
.on_pointer_down(move |_| on_click()),
Row(Modifier::new().align_items(AlignItems::Center)).child((
avatar
.or(leading_icon)
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 0.0,
right: 8.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(leading_color, move || v))
})
.unwrap_or(Box(Modifier::new())),
with_content_color(label_color, move || label),
trailing_icon
.map(|v| {
Box(Modifier::new().padding_values(PaddingValues {
left: 8.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
}))
.child(with_content_color(th.on_surface_variant, move || v))
})
.unwrap_or(Box(Modifier::new())),
)),
)
}
pub fn Scaffold(
top_bar: Option<View>,
bottom_bar: Option<View>,
floating_action_button: Option<View>,
content: impl Fn(PaddingValues) -> View,
) -> View {
let insets = window_insets();
let content_padding = PaddingValues {
top: if top_bar.is_some() { 64.0 } else { insets.top },
bottom: if bottom_bar.is_some() {
80.0 + insets.bottom + insets.ime_bottom
} else {
insets.bottom + insets.ime_bottom
},
left: insets.left,
right: insets.right,
};
Stack(Modifier::new().fill_max_size()).child((
Box(Modifier::new()
.fill_max_size()
.padding_values(PaddingValues {
top: if top_bar.is_some() {
64.0 + insets.top
} else {
0.0
},
bottom: if bottom_bar.is_some() {
80.0 + insets.bottom + insets.ime_bottom
} else {
insets.bottom + insets.ime_bottom
},
..Default::default()
}))
.child(content(content_padding)),
if let Some(bar) = top_bar {
Box(Modifier::new()
.absolute()
.offset(Some(0.0), Some(insets.top), Some(0.0), None))
.child(bar)
} else {
Box(Modifier::new())
},
if let Some(bar) = bottom_bar {
Box(Modifier::new().absolute().offset(
Some(0.0),
None,
Some(insets.bottom + insets.ime_bottom),
Some(0.0),
))
.child(bar)
} else {
Box(Modifier::new())
},
if let Some(fab) = floating_action_button {
Box(Modifier::new().absolute().offset(
None,
None,
Some(16.0 + insets.bottom + insets.ime_bottom),
Some(16.0),
))
.child(fab)
} else {
Box(Modifier::new())
},
))
}
pub struct TooltipState {
visible: Signal<bool>,
}
impl TooltipState {
pub fn new() -> Rc<Self> {
Rc::new(Self {
visible: signal(false),
})
}
pub fn is_visible(&self) -> bool {
self.visible.get()
}
pub fn show(&self) {
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
}
}
pub fn TooltipBox(
text: impl Into<String>,
state: Rc<TooltipState>,
modifier: Modifier,
content: View,
) -> View {
let text: Rc<str> = Rc::from(text.into());
let th = theme();
let spec = th.motion.overlay;
let alpha = animate_f32(
"tooltip_alpha",
if state.is_visible() { 1.0 } else { 0.0 },
spec,
);
let tooltip_visible = state.is_visible() || alpha > 0.01;
let scale = 0.92 + 0.08 * alpha;
Stack(modifier).child((
Box(Modifier::new().fill_max_size()).child(content),
if tooltip_visible {
Box(Modifier::new()
.background(th.inverse_surface)
.clip_rounded(th.shapes.extra_small)
.padding_values(PaddingValues {
left: 8.0,
right: 8.0,
top: 4.0,
bottom: 4.0,
})
.absolute()
.offset(None, Some(-28.0), None, None)
.align_self(AlignSelf::Center)
.render_z_index(10000.0)
.alpha(alpha)
.scale(scale))
.child(
Text((*text).to_string())
.color(th.inverse_on_surface)
.size(th.typography.label_medium)
.single_line(),
)
} else {
Box(Modifier::new())
},
))
}
pub struct DrawerState {
visible: Signal<bool>,
}
impl DrawerState {
pub fn new() -> Rc<Self> {
Rc::new(Self {
visible: signal(false),
})
}
pub fn is_open(&self) -> bool {
self.visible.get()
}
pub fn open(&self) {
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
}
}
pub fn ModalNavigationDrawer(
drawer_state: Rc<DrawerState>,
drawer_content: View,
content: View,
) -> View {
let th = theme();
let drawer_offset = animate_f32(
"modal_drawer_offset",
if drawer_state.is_open() { 0.0 } else { -360.0 },
theme().motion.spring,
);
Stack(Modifier::new().fill_max_size()).child((
Box(Modifier::new().fill_max_size()).child(content),
if drawer_state.is_open() {
Box(Modifier::new()
.fill_max_size()
.background(th.scrim.with_alpha(82))
.clickable()
.on_pointer_down({
let ds = drawer_state.clone();
move |_| ds.dismiss()
}))
.child(Box(Modifier::new()))
} else {
Box(Modifier::new())
},
Box(Modifier::new()
.absolute()
.offset(Some(drawer_offset), Some(0.0), None, Some(0.0))
.fill_max_height()
.width(300.0)
.background(th.surface_container_low)
.clip_rounded(th.shapes.large))
.child(drawer_content),
))
}
pub fn NavigationDrawerItem(
label: View,
selected: bool,
on_click: impl Fn() + 'static,
icon: Option<View>,
badge: Option<View>,
) -> View {
let th = theme();
let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.color;
let bg = animate_color(
format!("ndi_bg_{}", id),
if selected {
th.secondary_container
} else {
Color::TRANSPARENT
},
spec,
);
let fg = animate_color(
format!("ndi_fg_{}", id),
if selected {
th.on_secondary_container
} else {
th.on_surface_variant
},
spec,
);
Surface(
Modifier::new()
.fill_max_width()
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 0.0,
bottom: 0.0,
})
.min_height(56.0)
.background(bg)
.clip_rounded(28.0)
.clickable()
.on_pointer_down(move |_| on_click()),
with_content_color(fg, move || {
Row(Modifier::new()
.align_items(AlignItems::Center)
.padding_values(PaddingValues {
left: 16.0,
right: 24.0,
top: 0.0,
bottom: 0.0,
}))
.child((
icon.unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
Box(Modifier::new().width(12.0).height(1.0)),
Box(Modifier::new().flex_grow(1.0)).child(label),
badge.unwrap_or(Box(Modifier::new())),
))
}),
)
}
#[derive(Clone)]
pub struct DropdownMenuItem {
pub text: String,
pub leading_icon: Option<View>,
pub trailing_icon: Option<View>,
pub on_click: Rc<dyn Fn()>,
pub enabled: bool,
}
impl DropdownMenuItem {
pub fn new(text: impl Into<String>, on_click: impl Fn() + 'static) -> Self {
Self {
text: text.into(),
leading_icon: None,
trailing_icon: None,
on_click: Rc::new(on_click),
enabled: true,
}
}
pub fn leading_icon(mut self, icon: View) -> Self {
self.leading_icon = Some(icon);
self
}
pub fn trailing_icon(mut self, icon: View) -> Self {
self.trailing_icon = Some(icon);
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
pub struct MenuDivider;
pub struct MenuState {
visible: Signal<bool>,
anchor: Signal<Option<Vec2>>,
}
impl MenuState {
pub fn new() -> Self {
Self {
visible: signal(false),
anchor: signal(None),
}
}
pub fn is_open(&self) -> bool {
self.visible.get()
}
pub fn open(&self) {
self.visible.set(true);
}
pub fn open_at(&self, screen_pos: Vec2) {
self.anchor.set(Some(screen_pos));
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
}
}
static DROPDOWN_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn DropdownMenu(
state: Rc<MenuState>,
overlay: OverlayHandle,
modifier: Modifier,
trigger: View,
items: Vec<DropdownMenuEntry>,
) -> View {
let th = theme();
let ddm_id = remember(|| DROPDOWN_COUNTER.fetch_add(1, Ordering::Relaxed));
let overlay_id = remember_with_key(format!("ddm_oid_{}", ddm_id), || signal(0u64));
let anim = remember_state_with_key(format!("ddm_anim_{}", ddm_id), || {
AnimatedValue::new(0.0, theme().motion.overlay)
});
let last_target = remember_state_with_key(format!("ddm_lt_{}", ddm_id), || f32::NAN);
let anim_target = if state.is_open() { 1.0 } else { 0.0 };
{
let mut a = anim.borrow_mut();
let mut lt = last_target.borrow_mut();
if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
a.set_target(anim_target);
*lt = anim_target;
}
drop(lt);
if a.update() {
request_frame();
}
}
let progress = *anim.borrow().get();
let menu_visible = state.is_open() || progress > 0.01;
if menu_visible {
if overlay_id.get() == 0 {
let scrim = Box(Modifier::new().fill_max_size().absolute().on_pointer_down({
let s = state.clone();
move |_| s.dismiss()
}));
let id = overlay.show_with(scrim, 899.0, true);
overlay_id.set(id);
}
} else {
let prev = overlay_id.get();
if prev != 0 {
let _ = overlay.dismiss(prev);
overlay_id.set(0);
}
}
let scale = 0.92 + 0.08 * progress;
let alpha = progress;
Stack(modifier).child((
trigger,
if menu_visible {
Box(Modifier::new()
.absolute()
.offset(None, Some(40.0), None, None)
.render_z_index(900.0)
.scale(scale)
.alpha(alpha))
.child(render_dropdown_menu_content(&th, &items, state.clone()))
} else {
Box(Modifier::new())
},
))
}
#[derive(Clone)]
pub enum DropdownMenuEntry {
Item(DropdownMenuItem),
Divider,
}
fn render_dropdown_menu_content(
th: &Theme,
items: &[DropdownMenuEntry],
state: Rc<MenuState>,
) -> View {
let children: Vec<View> = items
.iter()
.map(|entry| match entry {
DropdownMenuEntry::Item(item) => {
let text_color = if item.enabled {
th.on_surface
} else {
th.on_surface.with_alpha_f32(0.38)
};
let on_click = item.on_click.clone();
let state = state.clone();
let mut modifier = Modifier::new()
.fill_max_width()
.min_height(40.0)
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::Center);
if item.enabled {
modifier = modifier
.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 |_| {
on_click();
state.dismiss();
});
}
Row(modifier).child((
item.leading_icon
.clone()
.unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
Box(Modifier::new().size(12.0, 1.0)),
Box(Modifier::new().flex_grow(1.0)).child(
Text(item.text.clone())
.color(text_color)
.size(th.typography.body_large)
.single_line(),
),
item.trailing_icon.clone().unwrap_or(Box(Modifier::new())),
))
}
DropdownMenuEntry::Divider => Box(Modifier::new()
.fill_max_width()
.height(1.0)
.margin(12.0)
.background(th.outline_variant)),
})
.collect();
Surface(
Modifier::new()
.background(th.surface_container)
.clip_rounded(th.shapes.small)
.state_elevation(StateElevation {
default: th.elevation.level2,
hovered: th.elevation.level3,
pressed: th.elevation.level3,
disabled: 0.0,
})
.min_width(112.0)
.padding(4.0),
Column(Modifier::new()).with_children(children),
)
}
pub struct SearchBarState {
pub query: Signal<String>,
pub expanded: Signal<bool>,
pub active: Signal<bool>,
}
impl SearchBarState {
pub fn new() -> Self {
Self {
query: signal(String::new()),
expanded: signal(false),
active: signal(false),
}
}
pub fn query(&self) -> String {
self.query.get()
}
pub fn set_query(&self, q: String) {
self.query.set(q);
}
pub fn is_expanded(&self) -> bool {
self.expanded.get()
}
pub fn expand(&self) {
self.expanded.set(true);
}
pub fn collapse(&self) {
self.expanded.set(false);
self.active.set(false);
}
pub fn is_active(&self) -> bool {
self.active.get()
}
pub fn activate(&self) {
self.active.set(true);
self.expanded.set(true);
}
pub fn deactivate(&self) {
self.active.set(false);
}
}
pub fn SearchBar(
state: Rc<SearchBarState>,
modifier: Modifier,
leading_icon: Option<View>,
trailing_icon: Option<View>,
placeholder: impl Into<String>,
on_query_change: Option<Rc<dyn Fn(String)>>,
content: View,
) -> View {
let th = theme();
let placeholder = placeholder.into();
let expanded = state.is_expanded();
let query = state.query();
let active = state.is_active();
let width = animate_f32(
"searchbar_width",
if expanded { 360.0 } else { 240.0 },
theme().motion.expand,
);
let input_field: View = if active {
TextField(
placeholder.clone(),
Modifier::new().flex_grow(1.0).padding(4.0),
Some({
let s = state.clone();
let cb = on_query_change.clone();
move |text| {
s.set_query(text);
if let Some(ref cb) = cb {
cb(s.query());
}
}
}),
None::<fn(String)>,
)
.color(th.on_surface)
.size(th.typography.body_large)
} else {
Box(Modifier::new().flex_grow(1.0)).child(
Text(if query.is_empty() {
placeholder.clone()
} else {
query.clone()
})
.color(if query.is_empty() {
th.on_surface_variant
} else {
th.on_surface
})
.size(th.typography.body_large)
.single_line(),
)
};
let bar_modifier = modifier.clone();
let bar = Surface(
bar_modifier
.width(width)
.height(56.0)
.background(if active {
th.surface_container_high
} else {
th.surface_container
})
.state_elevation(StateElevation {
default: if active { th.elevation.level3 } else { 0.0 },
hovered: th.elevation.level2,
pressed: th.elevation.level3,
disabled: 0.0,
})
.clip_rounded(th.shapes.large)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 0.0,
bottom: 0.0,
})
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| s.activate()
}),
Row(Modifier::new()
.fill_max_size()
.align_items(AlignItems::Center))
.child((
leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
Box(Modifier::new().size(8.0, 1.0)),
input_field,
trailing_icon.unwrap_or(Box(Modifier::new())),
)),
);
if expanded {
Stack(modifier).child((
bar,
Box(Modifier::new()
.width(width)
.max_height(400.0)
.background(th.surface_container)
.clip_rounded(th.shapes.small))
.child(content),
))
} else {
bar
}
}
pub fn DockedSearchBar(
state: Rc<SearchBarState>,
modifier: Modifier,
leading_icon: Option<View>,
placeholder: impl Into<String>,
on_query_change: Option<Rc<dyn Fn(String)>>,
content: View,
) -> View {
let th = theme();
let placeholder = placeholder.into();
let expanded = state.is_expanded();
let query = state.query();
let active = state.is_active();
let content_target = if expanded { 400.0 } else { 0.0 };
let content_height = animate_f32(
"docked_sh",
content_target,
theme().motion.expand,
);
let content_alpha = animate_f32(
"docked_sa",
if expanded { 1.0 } else { 0.0 },
theme().motion.color,
);
let input_field: View = if active {
TextField(
placeholder.clone(),
Modifier::new().flex_grow(1.0),
Some({
let s = state.clone();
let cb = on_query_change.clone();
move |text| {
s.set_query(text);
if let Some(ref cb) = cb {
cb(s.query());
}
}
}),
None::<fn(String)>,
)
.color(th.on_surface)
.size(th.typography.body_large)
} else {
Box(Modifier::new().flex_grow(1.0)).child(if query.is_empty() {
Text(placeholder.clone())
.color(th.on_surface_variant)
.size(th.typography.body_large)
.single_line()
} else {
Text(query.clone())
.color(th.on_surface)
.size(th.typography.body_large)
.single_line()
})
};
let bar = Surface(
modifier
.fill_max_width()
.height(56.0)
.background(if active {
th.surface_container_high
} else {
th.surface_container
})
.state_elevation(StateElevation {
default: if active { th.elevation.level3 } else { 0.0 },
hovered: th.elevation.level2,
pressed: th.elevation.level3,
disabled: 0.0,
})
.clip_rounded(th.shapes.large)
.padding_values(PaddingValues {
left: 16.0,
right: 16.0,
top: 0.0,
bottom: 0.0,
})
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| {
if !s.is_active() {
s.activate()
}
}
}),
Row(Modifier::new()
.fill_max_size()
.align_items(AlignItems::Center))
.child((
leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
Box(Modifier::new().size(12.0, 1.0)),
input_field,
if active {
Box(Modifier::new()
.size(24.0, 24.0)
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| {
s.set_query(String::new());
s.collapse();
}
}))
.child(Text("✕").size(16.0).color(th.on_surface_variant))
} else {
Box(Modifier::new())
},
)),
);
let show_content = expanded || content_height > 1.0;
if show_content {
Column(Modifier::new().fill_max_width()).child((
bar,
Box(Modifier::new()
.fill_max_width()
.height(content_height)
.alpha(content_alpha)
.clip_rounded(th.shapes.small)
.background(th.surface_container)
.state_elevation(StateElevation {
default: th.elevation.level3,
hovered: th.elevation.level3,
pressed: th.elevation.level3,
disabled: 0.0,
}))
.child(
Column(Modifier::new().fill_max_width()).child((
Box(Modifier::new()
.fill_max_width()
.height(1.0)
.background(th.outline_variant)),
content,
)),
),
))
} else {
bar
}
}
pub struct SheetState {
visible: Signal<bool>,
drag_offset: Signal<f32>,
peek_height: Signal<f32>,
}
impl SheetState {
pub fn new(peek_height: f32) -> Self {
Self {
visible: signal(false),
drag_offset: signal(0.0),
peek_height: signal(peek_height),
}
}
pub fn is_visible(&self) -> bool {
self.visible.get()
}
pub fn show(&self) {
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
self.drag_offset.set(0.0);
}
pub fn set_peek_height(&self, h: f32) {
self.peek_height.set(h);
}
}
pub fn ModalBottomSheet(
state: Rc<SheetState>,
overlay: OverlayHandle,
modifier: Modifier,
content: View,
) -> View {
let th = theme();
let peek = state.peek_height.get();
let overlay_id = remember_with_key("mbs_oid", || signal(0u64));
let anim = remember_state_with_key("mbs_anim", || {
AnimatedValue::new(600.0, theme().motion.spring)
});
let last_target = remember_state_with_key("mbs_anim_target", || f32::NAN);
let anim_target = if state.is_visible() { 0.0 } else { 600.0 };
{
let mut a = anim.borrow_mut();
let mut lt = last_target.borrow_mut();
if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
a.set_target(anim_target);
*lt = anim_target;
}
drop(lt);
let still_animating = a.update();
if still_animating {
request_frame();
}
}
let offset = *anim.borrow().get();
let sheet_visible = state.is_visible() || offset < 590.0;
if sheet_visible {
if overlay_id.get() == 0 {
let builder: Rc<dyn Fn() -> View> = Rc::new({
let state = state.clone();
let anim = anim.clone();
let modifier = modifier.clone();
let content = content.clone();
let th = th.clone();
move || {
let off = *anim.borrow().get();
let modif = modifier.clone();
let peek_h = state.peek_height.get();
let sheet = Box(modif
.fill_max_width()
.absolute()
.offset(None, Some(off), Some(0.0), Some(0.0))
.min_height(peek_h.max(48.0))
.background(th.surface_container_low)
.clip_rounded(th.shapes.large))
.child(
Column(Modifier::new().fill_max_width()).child((
Row(Modifier::new()
.fill_max_width()
.justify_content(JustifyContent::Center))
.child(Box(Modifier::new()
.margin_vertical(8.0)
.width(32.0)
.height(4.0)
.background(th.on_surface_variant.with_alpha_f32(0.4))
.clip_rounded(2.0))),
content.clone(),
)),
);
let visible = state.is_visible();
let scrim = if visible {
Box(Modifier::new()
.fill_max_size()
.background(th.scrim.with_alpha(85))
.on_pointer_down({
let s = state.clone();
move |_| s.dismiss()
}))
} else {
Box(Modifier::new())
};
Stack(Modifier::new().fill_max_size().absolute()).child((scrim, sheet))
}
});
let id = overlay.show_entry(builder, 900.0, false);
overlay_id.set(id);
}
} else {
let prev = overlay_id.get();
if prev != 0 {
let _ = overlay.dismiss(prev);
overlay_id.set(0);
}
}
Box(Modifier::new())
}
pub struct PullToRefreshState {
refreshing: Signal<bool>,
scroll_state: RefCell<Option<Rc<repose_ui::scroll::ScrollState>>>,
threshold: f32,
triggered: Cell<bool>,
}
impl PullToRefreshState {
pub fn new() -> Self {
Self {
refreshing: signal(false),
scroll_state: RefCell::new(None),
threshold: 64.0,
triggered: Cell::new(false),
}
}
pub fn set_scroll_state(&self, state: Rc<repose_ui::scroll::ScrollState>) {
*self.scroll_state.borrow_mut() = Some(state);
}
pub fn set_threshold(&mut self, px: f32) {
self.threshold = px;
}
pub fn is_refreshing(&self) -> bool {
self.refreshing.get()
}
pub fn set_refreshing(&self, v: bool) {
self.refreshing.set(v);
if !v {
if let Some(sc) = self.scroll_state.borrow().as_ref() {
sc.set_overscroll(0.0);
}
}
}
pub fn pull_offset(&self) -> f32 {
if let Some(sc) = self.scroll_state.borrow().as_ref() {
let os = sc.overscroll_offset();
if os < 0.0 { -os } else { 0.0 }
} else {
0.0
}
}
}
pub fn PullToRefresh(
state: Rc<PullToRefreshState>,
modifier: Modifier,
on_refresh: Rc<dyn Fn()>,
content: View,
) -> View {
let th = theme();
let pull = state.pull_offset();
let refreshing = state.is_refreshing();
if state.triggered.get() && !refreshing && pull < state.threshold {
state.triggered.set(false);
}
if !refreshing && !state.triggered.get() && pull >= state.threshold {
state.triggered.set(true);
state.refreshing.set(true);
(on_refresh)();
}
let frac_key = format!("ptr_frac_{}", Rc::as_ptr(&state) as u64);
let raw_frac = if refreshing {
1.0
} else if pull > 0.0 {
(pull / state.threshold).min(1.0)
} else {
0.0
};
let distance_fraction = animate_f32_from(
frac_key,
0.0,
raw_frac,
theme().motion.color,
);
let indicator_h = distance_fraction * state.threshold;
Column(modifier).child((
if distance_fraction > 0.01 {
Box(Modifier::new()
.fill_max_width()
.height(indicator_h)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center))
.child(if refreshing {
Text("↻").size(24.0).color(th.primary)
} else {
Text("↓")
.size((16.0 + distance_fraction * 8.0).min(24.0))
.color(th.primary.with_alpha_f32(distance_fraction.min(1.0)))
})
} else {
Box(Modifier::new())
},
content,
))
}
pub struct DatePickerState {
pub year: Signal<i32>,
pub month: Signal<u32>, pub day: Signal<u32>,
}
impl DatePickerState {
pub fn new(year: i32, month: u32, day: u32) -> Self {
Self {
year: signal(year),
month: signal(month.clamp(1, 12)),
day: signal(day.clamp(1, 31)),
}
}
pub fn selected_date(&self) -> (i32, u32, u32) {
(self.year.get(), self.month.get(), self.day.get())
}
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
29
} else {
28
}
}
_ => 30,
}
}
fn first_day_of_month(year: i32, month: u32) -> u32 {
let m = month as i32;
let (y, adj_m) = if m <= 2 {
(year - 1, m + 12)
} else {
(year, m)
};
let k = y % 100;
let j = y / 100;
let h = (1 + (13 * (adj_m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
((h + 5) % 7) as u32
}
struct ReposeDate {
year: i32,
month: u32,
day: u32,
}
impl ReposeDate {
fn now() -> Self {
let duration = web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.unwrap_or_default();
let days = (duration.as_secs() / 86_400) as i64;
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = (yoe as i64) + era as i64 * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
Self {
year: y as i32,
month: m as u32,
day: d as u32,
}
}
}
const MONTH_NAMES: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const DOW_HEADERS: [&str; 7] = ["M", "T", "W", "T", "F", "S", "S"];
pub fn DatePicker(
state: Rc<DatePickerState>,
on_confirm: Rc<dyn Fn(i32, u32, u32)>,
on_dismiss: Rc<dyn Fn()>,
) -> View {
let th = theme();
let (year, month, day) = state.selected_date();
let dim = days_in_month(year, month);
let start_dow = first_day_of_month(year, month);
let prev_year = {
let s = state.clone();
move || {
s.year.set(s.year.get() - 1);
let d = days_in_month(s.year.get(), s.month.get());
if s.day.get() > d {
s.day.set(d);
}
}
};
let next_year = {
let s = state.clone();
move || {
s.year.set(s.year.get() + 1);
let d = days_in_month(s.year.get(), s.month.get());
if s.day.get() > d {
s.day.set(d);
}
}
};
let prev_month = {
let s = state.clone();
move || {
if s.month.get() == 1 {
s.year.set(s.year.get() - 1);
s.month.set(12);
} else {
s.month.set(s.month.get() - 1);
}
let d = days_in_month(s.year.get(), s.month.get());
if s.day.get() > d {
s.day.set(d);
}
}
};
let next_month = {
let s = state.clone();
move || {
if s.month.get() == 12 {
s.year.set(s.year.get() + 1);
s.month.set(1);
} else {
s.month.set(s.month.get() + 1);
}
let d = days_in_month(s.year.get(), s.month.get());
if s.day.get() > d {
s.day.set(d);
}
}
};
let now = ReposeDate::now();
let today = (now.year, now.month, now.day);
Surface(
Modifier::new()
.width(328.0)
.background(th.surface_container_high)
.clip_rounded(th.shapes.extra_large)
.padding(16.0),
Column(Modifier::new()).child((
Row(Modifier::new()
.fill_max_width()
.align_items(AlignItems::Center))
.child((
IconButton(
Box(Modifier::new()).child(Text("◀").color(th.on_surface).size(16.0)),
prev_month,
),
Spacer(),
Column(Modifier::new().align_items(AlignItems::Center)).child((
Text(format!("{}", MONTH_NAMES[(month - 1) as usize]))
.size(th.typography.title_medium)
.color(th.on_surface),
Row(Modifier::new().gap(8.0).align_items(AlignItems::Center)).child((
IconButton(
Box(Modifier::new())
.child(Text("‹").color(th.on_surface_variant).size(14.0)),
prev_year,
),
Text(year.to_string())
.size(th.typography.body_small)
.color(th.on_surface_variant),
IconButton(
Box(Modifier::new())
.child(Text("›").color(th.on_surface_variant).size(14.0)),
next_year,
),
)),
)),
Spacer(),
IconButton(
Box(Modifier::new()).child(Text("▶").color(th.on_surface).size(16.0)),
next_month,
),
)),
Box(Modifier::new().size(1.0, 12.0)),
Column(Modifier::new()).child({
let mut rows: Vec<View> = Vec::new();
let dow_headers: Vec<View> = DOW_HEADERS
.iter()
.map(|d| {
Box(Modifier::new()
.width(40.0)
.height(40.0)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center))
.child(
Text(d.to_string())
.size(th.typography.label_small)
.color(th.on_surface_variant),
)
})
.collect();
rows.push(Row(Modifier::new()).with_children(dow_headers));
let total_cells = start_dow + dim;
let num_rows = ((total_cells + 6) / 7).min(6);
for w in 0..num_rows {
let mut week: Vec<View> = Vec::new();
for d in 0..7 {
let cell_idx = w * 7 + d;
if cell_idx < start_dow {
week.push(Box(Modifier::new().width(40.0).height(40.0)));
} else {
let day_num = (cell_idx - start_dow + 1) as i32;
if day_num <= dim as i32 {
let is_selected = day_num == day as i32;
let is_today = today.0 == year
&& today.1 == month
&& today.2 == day_num as u32;
let s = state.clone();
let on_confirm = on_confirm.clone();
let on_dismiss_fn = on_dismiss.clone();
week.push(
Box(Modifier::new()
.width(40.0)
.height(40.0)
.background(if is_selected {
th.primary
} else {
Color::TRANSPARENT
})
.clip_rounded(20.0)
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.clickable()
.on_pointer_down(move |_| {
s.day.set(day_num as u32);
on_confirm(s.year.get(), s.month.get(), day_num as u32);
on_dismiss_fn();
}))
.child({
let mut t = Text(day_num.to_string())
.size(th.typography.body_medium)
.color(if is_selected {
th.on_primary
} else {
th.on_surface
});
if is_today && !is_selected {
t = t.modifier(
Modifier::new().border(1.0, th.primary, 10.0),
);
}
t
}),
);
} else {
week.push(Box(Modifier::new().width(40.0).height(40.0)));
}
}
}
rows.push(Row(Modifier::new()).with_children(week));
}
rows
}),
Box(Modifier::new().size(1.0, 12.0)),
Row(Modifier::new()
.fill_max_width()
.justify_content(JustifyContent::End)
.gap(8.0))
.child((
TextButton(
Modifier::new(),
{
let on_dismiss = on_dismiss.clone();
move || (on_dismiss)()
},
|| Text("Cancel").size(14.0),
),
FilledButton(
Modifier::new(),
{
let on_confirm = on_confirm.clone();
let s = state.clone();
move || {
let (y, m, d) = s.selected_date();
on_confirm(y, m, d);
}
},
|| Text("OK").size(14.0),
),
)),
)),
)
}
pub struct TimePickerState {
pub hour: Signal<u32>,
pub minute: Signal<u32>,
pub is_am: Signal<bool>,
}
impl TimePickerState {
pub fn new(hour: u32, minute: u32) -> Self {
let h = hour % 12;
let am = hour < 12;
Self {
hour: signal(if h == 0 { 12 } else { h }),
minute: signal(minute.min(59)),
is_am: signal(am),
}
}
pub fn selected_time(&self) -> (u32, u32) {
let mut h = self.hour.get();
if !self.is_am.get() {
h = (h % 12) + 12;
} else if h == 12 {
h = 0;
}
(h, self.minute.get())
}
}
pub fn TimePicker(
state: Rc<TimePickerState>,
on_confirm: Rc<dyn Fn(u32, u32)>,
on_dismiss: Rc<dyn Fn()>,
) -> View {
let th = theme();
let hour = state.hour.get();
let minute = state.minute.get();
let is_am = state.is_am.get();
let hour_str = format!("{:02}", hour);
let min_str = format!("{:02}", minute);
Surface(
Modifier::new()
.width(256.0)
.background(th.surface_container_high)
.clip_rounded(th.shapes.extra_large)
.padding(24.0),
Column(Modifier::new().align_items(AlignItems::Center)).child((
Row(Modifier::new().align_items(AlignItems::Center)).child((
Box(Modifier::new()
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| s.hour.set((s.hour.get() % 12) + 1)
})
.padding(8.0))
.child(Text(hour_str).size(48.0).color(th.on_surface).single_line()),
Text(":")
.size(48.0)
.color(th.on_surface_variant)
.single_line(),
Box(Modifier::new()
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| s.minute.set((s.minute.get() + 1) % 60)
})
.padding(8.0))
.child(Text(min_str).size(48.0).color(th.on_surface).single_line()),
)),
Box(Modifier::new().size(1.0, 16.0)),
Row(Modifier::new().align_items(AlignItems::Center)).child((
Box(Modifier::new()
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 4.0,
bottom: 4.0,
})
.background(if is_am {
th.primary
} else {
Color::TRANSPARENT
})
.clip_rounded(8.0)
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| {
if !s.is_am.get() {
s.is_am.set(true);
let h = s.hour.get();
s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
if s.hour.get() == 0 {
s.hour.set(12);
}
}
}
}))
.child(
Text("AM").size(th.typography.label_large).color(if is_am {
th.on_primary
} else {
th.on_surface
}),
),
Box(Modifier::new().width(8.0).height(1.0)),
Box(Modifier::new()
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 4.0,
bottom: 4.0,
})
.background(if !is_am {
th.primary
} else {
Color::TRANSPARENT
})
.clip_rounded(8.0)
.clickable()
.on_pointer_down({
let s = state.clone();
move |_| {
if s.is_am.get() {
s.is_am.set(false);
let h = s.hour.get();
s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
if s.hour.get() == 0 {
s.hour.set(12);
}
}
}
}))
.child(
Text("PM").size(th.typography.label_large).color(if !is_am {
th.on_primary
} else {
th.on_surface
}),
),
)),
Box(Modifier::new().size(1.0, 16.0)),
Row(Modifier::new().fill_max_width()).child((
Spacer(),
Box(Modifier::new().padding(8.0).clickable().on_pointer_down({
let on_dismiss = on_dismiss.clone();
move |_| on_dismiss()
}))
.child(
Text("Cancel")
.color(th.primary)
.size(th.typography.label_large)
.single_line(),
),
Box(Modifier::new().width(8.0).height(1.0)),
Box(Modifier::new()
.padding(8.0)
.clickable()
.on_pointer_down(move |_| {
let (h, m) = state.selected_time();
on_confirm(h, m);
on_dismiss();
}))
.child(
Text("OK")
.color(th.primary)
.size(th.typography.label_large)
.single_line(),
),
)),
)),
)
}
pub struct NavRailItem {
pub icon: View,
pub label: String,
pub on_click: Rc<dyn Fn()>,
pub badge: Option<View>,
}
static NAVRAIL_COUNTER: AtomicU64 = AtomicU64::new(0);
static FILTERCHIP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn NavigationRail(
selected_index: usize,
items: Vec<NavRailItem>,
header: Option<View>,
fab: Option<View>,
) -> View {
let th = theme();
let id = remember(|| NAVRAIL_COUNTER.fetch_add(1, Ordering::Relaxed));
let spec = th.motion.shape;
let mut rail_children: Vec<View> = Vec::new();
let has_header = header.is_some();
let has_fab = fab.is_some();
if let Some(h) = header {
rail_children.push(
Box(Modifier::new()
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 12.0,
bottom: 12.0,
})
.align_self(AlignSelf::Center))
.child(h),
);
}
if let Some(f) = fab {
rail_children.push(
Box(Modifier::new()
.padding_values(PaddingValues {
left: 12.0,
right: 12.0,
top: 8.0,
bottom: 8.0,
})
.align_self(AlignSelf::Center))
.child(f),
);
}
if has_header || has_fab {
rail_children.push(Box(Modifier::new()
.size(80.0, 1.0)
.fill_max_width()
.background(th.outline_variant)));
}
for (i, item) in items.into_iter().enumerate() {
let selected = i == selected_index;
let fg = animate_color(
format!("nr_fg_{}_{}", id, i),
if selected {
th.on_secondary_container
} else {
th.on_surface_variant
},
spec,
);
let bg = animate_color(
format!("nr_bg_{}_{}", id, i),
if selected {
th.secondary_container
} else {
Color::TRANSPARENT
},
spec,
);
let cb = item.on_click.clone();
rail_children.push(
Column(
Modifier::new()
.fill_max_width()
.padding_values(PaddingValues {
left: 4.0,
right: 4.0,
top: 4.0,
bottom: 4.0,
})
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
.background(bg)
.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(16.0)
.clickable()
.on_pointer_down(move |_| cb()),
)
.child((
Stack(Modifier::new()).child((
Box(Modifier::new().size(24.0, 24.0))
.child(with_content_color(fg, move || item.icon)),
item.badge
.map(|b| {
Box(Modifier::new()
.absolute()
.offset(None, None, None, Some(0.0)))
.child(b)
})
.unwrap_or(Box(Modifier::new())),
)),
Box(Modifier::new().size(1.0, 4.0)),
Text(item.label)
.color(fg)
.size(th.typography.label_medium)
.single_line(),
)),
);
}
Column(
Modifier::new()
.width(80.0)
.fill_max_height()
.background(th.surface)
.align_items(AlignItems::Center),
)
.with_children(rail_children)
}
pub struct SwipeToDismissState {
anim: Rc<RefCell<AnimatedValue<f32>>>,
dismiss_handled: Rc<RefCell<bool>>,
}
impl SwipeToDismissState {
pub fn new() -> Self {
Self {
anim: Rc::new(RefCell::new(AnimatedValue::new(
0.0,
AnimationSpec::spring_gentle(),
))),
dismiss_handled: Rc::new(RefCell::new(true)),
}
}
pub fn offset(&self) -> f32 {
let mut anim = self.anim.borrow_mut();
if anim.update() {
request_frame();
}
*anim.get()
}
pub fn set_offset_instant(&self, off: f32) {
self.anim.borrow_mut().snap_to(off);
request_frame();
}
pub fn is_dismissed(&self) -> bool {
*self.anim.borrow().get() < -250.0
}
pub fn dismiss(&self) {
*self.dismiss_handled.borrow_mut() = false;
self.anim.borrow_mut().set_target(-300.0);
request_frame();
}
pub fn reset(&self) {
*self.dismiss_handled.borrow_mut() = true;
self.anim.borrow_mut().set_target(0.0);
request_frame();
}
fn try_handle_dismiss(&self, on_dismiss: &Option<Rc<dyn Fn()>>) {
let anim = self.anim.borrow();
if !anim.is_animating() && !*self.dismiss_handled.borrow() && *anim.get() < -150.0 {
*self.dismiss_handled.borrow_mut() = true;
if let Some(cb) = on_dismiss {
cb();
}
}
}
}
pub fn SwipeToDismiss(
state: Rc<SwipeToDismissState>,
on_dismiss: Option<Rc<dyn Fn()>>,
background: View,
content: View,
modifier: Modifier,
) -> View {
let offset = state.offset();
state.try_handle_dismiss(&on_dismiss);
let drag_start_x = remember_with_key("swipe_drag_start", || RefCell::new(None::<f32>));
let drag_base = remember_with_key("swipe_drag_base", || RefCell::new(0.0f32));
let st = state.clone();
let on_down = {
let d = drag_start_x.clone();
let base = drag_base.clone();
move |e: PointerEvent| {
*d.borrow_mut() = Some(e.position.x);
*base.borrow_mut() = *st.anim.borrow().get();
}
};
let st = state.clone();
let on_move = {
let d = drag_start_x.clone();
let base = drag_base.clone();
move |e: PointerEvent| {
if let Some(start) = *d.borrow() {
let dx = e.position.x - start;
st.set_offset_instant(*base.borrow() + dx);
}
}
};
let st = state.clone();
let on_up = {
let d = drag_start_x.clone();
move |_e: PointerEvent| {
*d.borrow_mut() = None;
let off = *st.anim.borrow().get();
if off > -50.0 {
st.reset();
} else {
st.dismiss();
}
}
};
let display_offset = offset.max(-300.0).min(0.0);
Stack(modifier.fill_max_width()).child((
Box(Modifier::new().fill_max_width()).child(background),
Box(Modifier::new()
.fill_max_width()
.translate(display_offset, 0.0)
.on_pointer_down(on_down)
.on_pointer_move(on_move)
.on_pointer_up(on_up))
.child(content),
))
}
pub fn Carousel<T, F>(
items: Vec<T>,
item_width: f32,
peek_amount: f32,
modifier: Modifier,
state: Rc<LazyRowState>,
item_builder: F,
) -> View
where
T: Clone + 'static,
F: Fn(T, usize) -> View + 'static,
{
let padded_modifier = modifier.clone().padding_values(PaddingValues {
left: peek_amount,
right: peek_amount,
top: 0.0,
bottom: 0.0,
});
LazyRow(items, item_width, state, padded_modifier, item_builder)
}