extern crate alloc;
use crate::anim::{BOUNCY, PlayMode, SMOOTH, Spring, Tween, ease};
use crate::prelude::*;
use crate::ui;
use crate::ui::icons::IconAsset;
use crate::ui::icons::{
ICON_ARROW_DOWN, ICON_ARROW_LEFT, ICON_ARROW_RIGHT, ICON_ARROW_UP, ICON_CHECK,
ICON_CHEVRON_DOWN, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_CHEVRON_UP, ICON_CIRCLE,
ICON_CROSS, ICON_HEART, ICON_HOME, ICON_MINUS, ICON_PAUSE, ICON_PLAY, ICON_PLUS, ICON_SQUARE,
ICON_STAR, ICON_STOP,
};
use crate::ui::theme::{ColorToken, ThemedColor};
use crate::ui::widgets::icon::Icon;
use crate::ui::widgets::{Image, ParagraphStyle, Text, TextAlign};
#[cfg(feature = "std")]
use crate::app::plugins::StdInstantClockPlugin;
mirui_macros::animate!(IconScale, |world, entity, value| {
if let Some(icon) = world.get_mut::<Icon>(entity) {
icon.scale = value;
}
world.invalidate(entity);
});
fn icons() -> [(IconAsset, ColorToken); 20] {
[
(ICON_HOME, ColorToken::Primary),
(ICON_CHECK, ColorToken::Success),
(ICON_CROSS, ColorToken::Error),
(ICON_PLUS, ColorToken::OnSurface),
(ICON_MINUS, ColorToken::OnSurface),
(ICON_ARROW_LEFT, ColorToken::OnSurfaceVariant),
(ICON_ARROW_RIGHT, ColorToken::OnSurfaceVariant),
(ICON_ARROW_UP, ColorToken::OnSurfaceVariant),
(ICON_ARROW_DOWN, ColorToken::OnSurfaceVariant),
(ICON_CHEVRON_LEFT, ColorToken::Primary),
(ICON_CHEVRON_RIGHT, ColorToken::Primary),
(ICON_CHEVRON_UP, ColorToken::Primary),
(ICON_CHEVRON_DOWN, ColorToken::Primary),
(ICON_STAR, ColorToken::Primary),
(ICON_HEART, ColorToken::Error),
(ICON_PLAY, ColorToken::Success),
(ICON_PAUSE, ColorToken::OnSurface),
(ICON_STOP, ColorToken::Error),
(ICON_CIRCLE, ColorToken::OnSurfaceVariant),
(ICON_SQUARE, ColorToken::OnSurfaceVariant),
]
}
fn beat() -> IconScale {
IconScale(
Tween::new(
Fixed::ONE,
Fixed::from_f32(1.4),
600,
ease::ease_in_out_cubic,
PlayMode::PingPong,
)
.into(),
)
}
fn breathe() -> IconScale {
IconScale(
Spring::preset(Fixed::ONE, Fixed::from_f32(1.25), SMOOTH)
.repeat()
.into(),
)
}
fn bounce() -> IconScale {
IconScale(
Spring::preset(Fixed::from_f32(0.8), Fixed::from_f32(1.2), BOUNCY)
.repeat()
.into(),
)
}
#[compose]
pub fn build_widgets() {
let table = icons();
ui! {
Column (
id: "icon_gallery_shell",
grow: 1.0,
padding: Padding::all(12),
row_gap: 8,
bg_color: ColorToken::Surface
) {
Text (
"VECTOR ICONS",
height: 24,
font_size: 18,
text_color: ColorToken::OnSurface,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Text (
"STATIC PATHS · LIVE THEME · RETAINED MOTION",
height: 18,
font_size: 9,
text_color: ColorToken::OnSurfaceVariant,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Row (
id: "icon_gallery_grid",
grow: 1.0,
wrap: FlexWrap::Wrap,
justify: JustifyContent::Center,
align: AlignItems::Center,
row_gap: 6,
column_gap: 6
) {
walk table.iter() with cell {
Image (
src: cell.0,
color: ThemedColor::Token(cell.1),
viewbox: Fixed::from_int(24),
scale: Fixed::from_ratio(7, 6),
width: 44,
height: 44,
grow: 0.0,
bg_color: ColorToken::SurfaceVariant,
border_radius: 10
)
}
}
Text (
"MOTION PRESETS",
height: 18,
font_size: 9,
text_color: ColorToken::OnSurfaceVariant,
paragraph: ParagraphStyle::label().with_align(TextAlign::Start)
)
Row (
id: "icon_gallery_motion",
height: 54,
justify: JustifyContent::SpaceEvenly,
align: AlignItems::Center,
column_gap: 4
) {
Icon (
path: ICON_HEART,
color: ThemedColor::Token(ColorToken::Error),
size: Dimension::Px(Fixed::from_int(34)),
grow: 1.0,
height: 52
) [
beat(),
]
Icon (
path: ICON_CIRCLE,
color: ThemedColor::Token(ColorToken::Primary),
size: Dimension::Px(Fixed::from_int(34)),
grow: 1.0,
height: 52
) [
breathe(),
]
Icon (
path: ICON_STAR,
color: ThemedColor::Token(ColorToken::Success),
size: Dimension::Px(Fixed::from_int(34)),
grow: 1.0,
height: 52
) [
bounce(),
]
Icon (
path: ICON_PLAY,
color: ThemedColor::Token(ColorToken::Primary),
size: Dimension::Px(Fixed::from_int(34)),
grow: 1.0,
height: 52
) [
beat(),
]
Icon (
path: ICON_PLUS,
color: ThemedColor::Token(ColorToken::OnSurface),
size: Dimension::Px(Fixed::from_int(34)),
grow: 1.0,
height: 52
) [
bounce(),
]
}
}
};
}
#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
B: Surface,
F: RendererFactory<B>,
{
app.add_system(IconScale::system());
app.add_plugin(StdInstantClockPlugin);
app.compose(parent, build_widgets);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Viewport;
use crate::ui::Children;
use crate::ui::render_system::update_layout;
use crate::ui::{ComputedRect, IdMap, UiScope};
#[test]
fn build_widgets_smoke() {
let mut world = World::new();
world.insert_resource(IdMap::new());
let parent = WidgetBuilder::new(&mut world).id();
let mut cx = UiScope::new(&mut world, parent);
build_widgets(&mut cx);
assert!(
world
.get::<Children>(parent)
.is_some_and(|c| !c.0.is_empty()),
);
}
#[test]
fn icons_table_has_twenty_entries() {
assert_eq!(icons().len(), 20);
}
#[test]
fn icon_grid_stays_inside_phone_shell() {
let mut world = World::new();
world.insert_resource(IdMap::new());
let parent = WidgetBuilder::new(&mut world).id();
let mut cx = UiScope::new(&mut world, parent);
build_widgets(&mut cx);
drop(cx);
update_layout(&mut world, parent, &Viewport::new(320, 568, Fixed::ONE));
let rect = |id| {
world
.get::<ComputedRect>(world.find_by_id(id).unwrap())
.unwrap()
.0
};
let shell = rect("icon_gallery_shell");
for id in ["icon_gallery_grid", "icon_gallery_motion"] {
let child = rect(id);
assert!(child.x >= shell.x);
assert!(child.x + child.w <= shell.x + shell.w);
}
}
}