extern crate alloc;
use crate::input::event::scroll::{ScrollAxis, ScrollConfig, ScrollOffset};
use crate::prelude::*;
use crate::ui::widgets::{Button, ParagraphStyle, Text};
const ROW_H: i32 = 32;
#[derive(Clone)]
struct Fruit {
name: &'static str,
color: ColorToken,
}
const PALETTE: [Fruit; 6] = [
Fruit {
name: "Apple",
color: ColorToken::Error,
},
Fruit {
name: "Lime",
color: ColorToken::Success,
},
Fruit {
name: "Plum",
color: ColorToken::Primary,
},
Fruit {
name: "Mango",
color: ColorToken::Secondary,
},
Fruit {
name: "Berry",
color: ColorToken::Tertiary,
},
Fruit {
name: "Pear",
color: ColorToken::SurfaceVariant,
},
];
#[compose]
pub fn build_widgets() {
let fruits = Signal::new(alloc::vec![
PALETTE[0].clone(),
PALETTE[1].clone(),
PALETTE[2].clone(),
]);
let (dec, inc, label) = (fruits.clone(), fruits.clone(), fruits.clone());
let rows = fruits.clone();
let content = fruits.clone();
ui! {
Column (
grow: 1.0,
align: AlignItems::Center,
justify: JustifyContent::Center,
padding: Padding::all(20),
row_gap: 10
) {
Text (
text: ${ alloc::format!("{} FRUITS", label.with(| f | f.len())) },
width: Dimension::percent(100),
max_width: 260,
height: 36,
font_size: 18,
text_color: ColorToken::OnSurface,
paragraph: ParagraphStyle::label()
)
Row (
width: Dimension::percent(100),
max_width: 180,
height: 40,
column_gap: 10
) {
Button (
text_color: ColorToken::OnPrimary,
grow: 1.0,
height: 40,
border_radius: 10,
normal_color: ColorToken::Error,
pressed_color: ColorToken::SurfaceVariant
) [
Text::label("−"),
] on Tap {
dec.update(|f| {
f.pop();
});
}
Button (
text_color: ColorToken::OnPrimary,
grow: 1.0,
height: 40,
border_radius: 10,
normal_color: ColorToken::Primary,
pressed_color: ColorToken::Secondary
) [
Text::label("+"),
] on Tap {
inc.update(|f| {
let next = PALETTE[f.len() % PALETTE.len()].clone();
f.push(next);
});
}
}
Column (
id: "state_list_scroll",
bg_color: ColorToken::SurfaceVariant,
grow: 1.0,
width: Dimension::percent(100),
max_width: 260,
max_height: 180,
border_radius: 12,
padding: Padding::all(6),
row_gap: 4
) [
ScrollOffset {
x: Fixed::ZERO,
y: Fixed::ZERO,
},
ScrollConfig {
direction: ScrollAxis::Vertical,
elastic: true,
content_height: Fixed::from_int(3 * ROW_H),
content_width: Fixed::ZERO,
},
] {
walk ${ rows.get() } with fruit {
View (
bg_color: fruit.color,
text_color: ColorToken::OnPrimary,
width: Dimension::percent(100),
height: ROW_H,
border_radius: 8
) [
Text::label(fruit.name),
]
}
}
}
};
let scroll = World::find_by_id(cx.world_mut(), "state_list_scroll")
.expect("scroll container id registered");
crate::core::reactive::with_world_scope(cx.world_mut(), || {
crate::core::reactive::effect_with_widget(scroll, move || {
let n = content.with(|f| f.len()) as i32;
crate::core::reactive::with_world(|w| {
if let Some(cfg) = w.get_mut::<ScrollConfig>(scroll) {
cfg.content_height = Fixed::from_int(n * ROW_H);
}
});
});
});
}
#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
B: Surface,
F: RendererFactory<B>,
{
use crate::app::plugins::StdInstantClockPlugin;
app.add_plugin(StdInstantClockPlugin);
app.compose(parent, build_widgets);
}
pub const DEMO_SIZE: crate::gallery::DemoSize = crate::gallery::DemoSize::at_most(320, 320);
#[cfg(test)]
mod tests {
use super::*;
use crate::core::reactive::flush_signal_dirty;
use crate::input::event::GestureHandler;
use crate::input::event::gesture::GestureEvent;
use crate::ui::Children;
use crate::ui::IdMap;
use crate::ui::UiScope;
fn row_count(world: &World, list: Entity) -> usize {
world.get::<Children>(list).map(|c| c.0.len()).unwrap_or(0)
}
fn tap(world: &mut World, e: Entity) {
GestureHandler::trigger(
world,
e,
&GestureEvent::Tap {
x: Fixed::ZERO,
y: Fixed::ZERO,
target: e,
},
);
flush_signal_dirty(world);
}
#[test]
fn list_grows_and_shrinks_by_signal() {
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);
let col = world.get::<Children>(parent).unwrap().0[0];
let list = world.get::<Children>(col).unwrap().0[2];
let row_ctrl = world.get::<Children>(col).unwrap().0[1];
let dec = world.get::<Children>(row_ctrl).unwrap().0[0];
let inc = world.get::<Children>(row_ctrl).unwrap().0[1];
assert_eq!(
world.get::<Children>(col).unwrap().0.len(),
3,
"column has label, button-row, list"
);
assert_eq!(
world.get::<Children>(row_ctrl).unwrap().0.len(),
2,
"button row has - and + buttons"
);
assert_eq!(row_count(&world, list), 3, "starts with 3 rows");
let content_h = |w: &World| {
w.get::<crate::input::event::scroll::ScrollConfig>(list)
.unwrap()
.content_height
};
assert_eq!(
content_h(&world),
Fixed::from_int(3 * ROW_H),
"scroll content height tracks 3 rows"
);
let first_before = world.get::<Children>(list).unwrap().0[0];
tap(&mut world, inc);
assert_eq!(row_count(&world, list), 4, "tap + appends one row");
let first_after = world.get::<Children>(list).unwrap().0[0];
assert_eq!(first_before, first_after, "surviving row keeps its entity");
assert_eq!(
content_h(&world),
Fixed::from_int(4 * ROW_H),
"scroll content height grows with rows"
);
tap(&mut world, dec);
tap(&mut world, dec);
assert_eq!(row_count(&world, list), 2, "two taps - drop two tail rows");
assert_eq!(
world.get::<Children>(list).unwrap().0[0],
first_before,
"row 0 survives shrink"
);
assert_eq!(
content_h(&world),
Fixed::from_int(2 * ROW_H),
"scroll content height shrinks with rows"
);
}
}