extern crate alloc;
#[cfg(feature = "std")]
use crate::anim::ease;
#[cfg(feature = "std")]
use crate::app::{App, RendererFactory};
use crate::components::{LazyList, LazyListBinder, LazyListPool};
use crate::components::{ProgressBar, Slider, Switch, TabBar, TabContent, Text};
use crate::ecs::{Entity, World};
use crate::event::scroll::{ScrollAxis, ScrollConfig, ScrollOffset};
#[cfg(feature = "std")]
use crate::event::sim::{SimAction, SimTimeline, sim_timeline_system};
#[cfg(feature = "std")]
use crate::plugins::{FpsSummaryPlugin, InputFeedbackPlugin, StdInstantClockPlugin};
use crate::prelude::*;
#[cfg(feature = "std")]
use crate::surface::Surface;
#[cfg(feature = "std")]
use crate::types::DimPoint;
use crate::widget::dirty::Dirty;
use crate::widget::theme::{self, ColorToken};
use crate::widget::{Children, OffscreenRender, Theme};
use alloc::format;
#[cfg(feature = "std")]
use alloc::vec;
use alloc::vec::Vec;
const POOL_SIZE: usize = 12;
const ITEM_COUNT: u32 = 50;
pub const DEFAULT_VIEW: (u16, u16) = (512, 512);
pub const ACCENT: ColorToken = ColorToken::custom("accent");
struct FormSlider;
struct FormProgress;
pub struct ThemeCycleIndex(pub u8);
struct DemoSize {
w: i32,
tabbar_h: i32,
page_h: i32,
row_h: i32,
scale: i32,
}
impl DemoSize {
fn for_viewport(view_w: u16, view_h: u16) -> Self {
let w = (view_w as i32).max(1);
let h = (view_h as i32).max(1);
let scale = (w.min(h) / 128).max(1);
let tabbar_h = 14 * scale;
Self {
w,
tabbar_h,
page_h: h - tabbar_h,
row_h: 12 * scale,
scale,
}
}
}
pub fn dark_with_accent() -> Theme {
Theme::dark().with(ACCENT, Color::rgb(255, 200, 60))
}
pub fn light_with_accent() -> Theme {
Theme::light().with(ACCENT, Color::rgb(220, 60, 90))
}
pub fn custom_theme() -> Theme {
Theme::dark().with_many([
(ColorToken::Primary, Color::rgb(255, 105, 180)),
(ColorToken::OnPrimary, Color::rgb(20, 20, 30)),
(ColorToken::Success, Color::rgb(255, 200, 60)),
(ColorToken::Surface, Color::rgb(38, 28, 50)),
(ColorToken::SurfaceVariant, Color::rgb(70, 50, 90)),
(ColorToken::OnSurface, Color::rgb(245, 235, 255)),
(ColorToken::OnSurfaceVariant, Color::rgb(180, 150, 200)),
(ACCENT, Color::rgb(140, 200, 220)),
])
}
fn row_binder(world: &mut World, entity: Entity, index: u32) {
let label = format!("Row {index}");
if let Some(t) = world.get_mut::<Text>(entity) {
t.0 = label.into_bytes();
} else {
world.insert(entity, Text(label.into_bytes()));
}
}
#[mirui_macros::system]
pub fn slider_to_progress_system(world: &mut World) {
let sliders: Vec<Entity> = world.query::<FormSlider>().collect();
let mut value = None;
for e in sliders {
if let Some(s) = world.get::<Slider>(e) {
value = Some(s.value.to_f32() / 100.0);
}
}
let Some(v) = value else { return };
let bars: Vec<Entity> = world.query::<FormProgress>().collect();
for e in bars {
if let Some(pb) = world.get_mut::<ProgressBar>(e)
&& (pb.value - v).abs() > 0.001
{
pb.value = v;
world.insert(e, Dirty);
}
}
}
mirui_macros::timer!(Cycle, every: 3_000, |world, entity| {
let next = world
.get::<ThemeCycleIndex>(entity)
.map(|i| (i.0 + 1) % 3)
.unwrap_or(0);
world.insert(entity, ThemeCycleIndex(next));
let theme = match next {
0 => dark_with_accent(),
1 => light_with_accent(),
_ => custom_theme(),
};
theme::set_theme(world, theme);
});
pub fn build_widgets(world: &mut World, parent: Entity, view_w: u16, view_h: u16) {
let DemoSize {
w: w_,
tabbar_h: tabbar_h_,
page_h: page_h_,
row_h: row_h_,
scale: scale_,
} = DemoSize::for_viewport(view_w, view_h);
let tabs = ui! {
:(
parent: parent
world: world
:)
TabBar (
bg_color: ColorToken::SurfaceVariant,
width: w_,
height: tabbar_h_,
count: 3,
indicator_height: Fixed::from_int(2 * scale_)
) {
View (
text: "List",
text_color: ColorToken::OnSurface,
grow: 1.0,
align: AlignItems::Center,
justify: JustifyContent::Center
)
View (
text: "Form",
text_color: ColorToken::OnSurface,
grow: 1.0,
align: AlignItems::Center,
justify: JustifyContent::Center
)
View (
text: "Thm",
text_color: ColorToken::OnSurface,
grow: 1.0,
align: AlignItems::Center,
justify: JustifyContent::Center
)
}
};
let list = ui! {
:(
parent: parent
world: world
:)
LazyList (
bg_color: ColorToken::Surface,
position: Position::Absolute,
left: 0,
top: tabbar_h_,
width: w_,
height: page_h_,
item_count: ITEM_COUNT,
item_height: Fixed::from_int(row_h_),
pool_size: POOL_SIZE as u8
) [
TabContent {
tab_bar: tabs,
index: 0,
},
LazyListBinder { bind: row_binder },
ScrollOffset {
x: Fixed::ZERO,
y: Fixed::ZERO,
},
ScrollConfig {
direction: ScrollAxis::Vertical,
elastic: false,
content_height: Fixed::from_int(row_h_ * ITEM_COUNT as i32),
content_width: Fixed::ZERO,
},
] {
walk 0..POOL_SIZE with _i {
Row (
bg_color: ColorToken::SurfaceVariant,
text_color: ColorToken::OnSurface,
position: Position::Absolute,
left: 0,
top: 0,
width: w_,
height: row_h_
)
}
}
};
let pool: Vec<Entity> = world
.get::<Children>(list)
.map(|c| c.0.clone())
.unwrap_or_default();
world.insert(list, LazyListPool::new(pool));
ui! {
:(
parent: parent
world: world
:)
Column (
bg_color: ColorToken::Surface,
position: Position::Absolute,
left: 0,
top: tabbar_h_,
width: w_,
height: page_h_,
padding: Padding::all(10 * scale_)
) [
TabContent {
tab_bar: tabs,
index: 1,
},
] {
Row (
height: 28 * scale_,
align: AlignItems::Center
) {
View (text: "Enable", text_color: ColorToken::OnSurface, grow: 1.0)
Switch (width: 40 * scale_, height: 20 * scale_) [
OffscreenRender::default(),
]
}
View (
height: 14 * scale_,
padding: Padding {
top: Dimension::px(6 * scale_),
..Default::default()
}
) {
Slider (
width: 108 * scale_,
height: 14 * scale_,
min: Fixed::ZERO,
max: Fixed::from_int(100)
) [
FormSlider,
]
}
View (
height: 10 * scale_,
padding: Padding {
top: Dimension::px(8 * scale_),
..Default::default()
}
) {
ProgressBar (
width: 108 * scale_,
height: 8 * scale_,
border_radius: 4 * scale_ as u32
) [
FormProgress,
]
}
}
};
ui! {
:(
parent: parent
world: world
:)
Column (
bg_color: ColorToken::Surface,
position: Position::Absolute,
left: 0,
top: tabbar_h_,
width: w_,
height: page_h_,
padding: Padding::all(12 * scale_),
align: AlignItems::Center
) [
TabContent {
tab_bar: tabs,
index: 2,
},
] {
View (text: "Primary", text_color: ColorToken::OnSurface, height: 14 * scale_)
View (
width: 80 * scale_,
height: 18 * scale_,
bg_color: ColorToken::Primary,
border_radius: 4 * scale_ as u32
)
View (
text: "accent (custom)",
text_color: ColorToken::OnSurfaceVariant,
height: 12 * scale_,
padding: Padding {
top: Dimension::px(8 * scale_),
..Default::default()
}
)
View (
width: 80 * scale_,
height: 18 * scale_,
bg_color: ACCENT,
border_radius: 4 * scale_ as u32
)
}
};
}
#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
B: Surface,
F: RendererFactory<B>,
{
let info = app.backend.display_info();
app.add_plugin(InputFeedbackPlugin::default());
app.add_plugin(StdInstantClockPlugin);
app.add_plugin(FpsSummaryPlugin::default());
app.with_offscreen_pool_budget(512 * 1024);
app.add_system(sim_timeline_system::system());
app.add_system(slider_to_progress_system::system());
app.world.insert_resource(dark_with_accent());
let cycle_e = Cycle::install(&mut app.world);
app.world.insert(cycle_e, ThemeCycleIndex(0));
build_widgets(&mut app.world, parent, info.width, info.height);
let tabs_kids: Vec<Entity> = {
let q: Vec<Entity> = app.world.query::<TabBar>().collect();
let tab_bar_e = *q.first().expect("TabBar must be installed");
app.world
.get::<Children>(tab_bar_e)
.map(|c| c.0.clone())
.unwrap_or_default()
};
if tabs_kids.len() < 3 {
return;
}
let (tab_list, tab_form, tab_theme) = (tabs_kids[0], tabs_kids[1], tabs_kids[2]);
let switches: Vec<Entity> = app.world.query::<Switch>().collect();
let switch_e = *switches.first().expect("form Switch must be installed");
let sliders: Vec<Entity> = app.world.query::<Slider>().collect();
let slider_e = *sliders.first().expect("form Slider must be installed");
let lists: Vec<Entity> = app.world.query::<LazyList>().collect();
let list_e = *lists.first().expect("List LazyList must be installed");
if std::env::var("MIRUI_SIM_OFF").ok().as_deref() == Some("1") {
return;
}
app.world.insert_resource(
SimTimeline::new(vec![
SimAction::wait(800),
SimAction::tap(DimPoint::CENTER).on(tab_form),
SimAction::wait(800),
SimAction::tap(DimPoint::CENTER).on(switch_e),
SimAction::wait(800),
SimAction::drag(
DimPoint::percent(10, 50),
DimPoint::percent(90, 50),
600,
ease::ease_in_out_cubic,
)
.on(slider_e),
SimAction::wait(800),
SimAction::tap(DimPoint::CENTER).on(switch_e),
SimAction::wait(1500),
SimAction::tap(DimPoint::CENTER).on(tab_theme),
SimAction::wait(6500),
SimAction::tap(DimPoint::CENTER).on(tab_list),
SimAction::wait(800),
SimAction::drag(
DimPoint::percent(50, 80),
DimPoint::percent(50, 20),
100,
ease::linear,
)
.on(list_e),
SimAction::wait(800),
SimAction::drag(
DimPoint::percent(50, 20),
DimPoint::percent(50, 80),
100,
ease::linear,
)
.on(list_e),
SimAction::wait(800),
])
.looping(true),
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::IdMap;
use crate::widget::builder::WidgetBuilder;
#[test]
fn build_widgets_smoke() {
let mut world = World::new();
world.insert_resource(IdMap::new());
let parent = WidgetBuilder::new(&mut world).id();
build_widgets(&mut world, parent, DEFAULT_VIEW.0, DEFAULT_VIEW.1);
assert!(
world
.get::<Children>(parent)
.is_some_and(|c| !c.0.is_empty()),
);
}
}