//! Demonstrates how to forward ECS changes to UI. use std::time::Duration; use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; use bevy_rand::prelude::*; use haalka::prelude::*; use rand::prelude::{IteratorRandom, Rng}; fn main() { App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { position: WindowPosition::Centered(MonitorSelection::Primary), ..default() }), ..default() }), HaalkaPlugin, EntropyPlugin::::default(), )) .add_systems(Startup, (ui_root, setup)) .add_systems(Update, (sync_timer, dot_spawner, dot_despawner)) .run(); } enum ColorCategory { Blue, Green, Red, Yellow, } const WIDTH: f32 = 1280.; // default window const HEIGHT: f32 = 720.; // default window const BOX_SIZE: f32 = HEIGHT / 2.; const FONT_SIZE: f32 = 30.; #[allow(dead_code)] fn box_(category: ColorCategory) -> El { El::::new() .width(Val::Px(BOX_SIZE)) .height(Val::Px(BOX_SIZE)) .background_color(BackgroundColor(match category { ColorCategory::Blue => BLUE, ColorCategory::Green => GREEN, ColorCategory::Red => RED, ColorCategory::Yellow => YELLOW, })) .align(Align::center()) // .child(El::::new().text(text(&category.to_string()))) } fn text(string: &str) -> Text { Text::from_section( string, TextStyle { font_size: FONT_SIZE, ..default() }, ) } fn labeled_element(label: impl Element, element: impl Element) -> impl Element { Row::::new() .with_style(|mut style| style.column_gap = Val::Px(10.)) .item(label) .item(element) } fn labeled_count(label: impl Element, count_signal: impl Signal + Send + 'static) -> impl Element { labeled_element(label, { El::::new().text_signal(count_signal.map(|count| text(&count.to_string()))) }) } fn text_labeled_element(label: &str, element: impl Element) -> impl Element { labeled_element(El::::new().text(text(&format!("{}: ", label))), element) } fn text_labeled_count(label: &str, count_signal: impl Signal + Send + 'static) -> impl Element { text_labeled_element(label, { El::::new().text_signal(count_signal.map(|count| text(&count.to_string()))) }) } fn category_count(category: ColorCategory, count: impl Signal + Send + 'static) -> impl Element { labeled_count( { El::::new() .width(Val::Px(30.)) .height(Val::Px(30.)) .background_color(BackgroundColor(match category { ColorCategory::Blue => BLUE, ColorCategory::Green => GREEN, ColorCategory::Red => RED, ColorCategory::Yellow => YELLOW, })) .align(Align::center()) // .child(El::::new().text(text(&category.to_string()))) }, count, ) } // like serde fn incrde_button(value: Mutable, incr: f32) -> impl Element { let hovered = Mutable::new(false); let f = move || { let new = (*value.lock_ref() + incr).max(0.); *value.lock_mut() = new; }; El::::new() .width(Val::Px(45.0)) .align_content(Align::center()) .background_color_signal( hovered .signal() .map_bool(|| Color::hsl(300., 0.75, 0.85), || Color::hsl(300., 0.75, 0.75)) .map(BackgroundColor), ) .hovered_sync(hovered) .on_pressing_with_sleep_throttle(f, Duration::from_millis(50)) .child(El::::new().text(text(if incr.is_sign_positive() { "+" } else { "-" }))) } fn rate_element(rate: Mutable) -> impl Element { Row::::new() .with_style(|mut style| style.column_gap = Val::Px(15.0)) .item(El::::new().text_signal(rate.signal().map(|rate| text(&format!("{:.1}", rate))))) .item(incrde_button(rate.clone(), 0.1)) .item(incrde_button(rate, -0.1)) } struct MutableTimer { timer: Timer, rate: Mutable, } fn close(a: f32, b: f32) -> bool { (a - b).abs() < 0.000001 } impl MutableTimer { fn from(rate: Mutable) -> Self { Self { timer: Timer::from_seconds(1. / rate.get(), TimerMode::Repeating), rate, } } fn sync(&mut self) { let rate = self.rate.get(); if rate > 0. { self.timer.unpause(); let new = 1. / rate; let cur = self.timer.duration().as_secs_f32(); if !close(new, cur) { self.timer.set_duration(Duration::from_secs_f32(new)); } } else { self.timer.pause(); } } } #[derive(Resource)] struct Spawner(MutableTimer); #[derive(Resource)] struct Despawner(MutableTimer); #[derive(Resource)] struct Counts { blue: Mutable, green: Mutable, red: Mutable, yellow: Mutable, } const STARTING_SPAWN_RATE: f32 = 1.5; const STARTING_DESPAWN_RATE: f32 = 1.; fn ui_root(world: &mut World) { let spawn_rate = Mutable::new(STARTING_SPAWN_RATE); let despawn_rate = Mutable::new(STARTING_DESPAWN_RATE); let blue_count = Mutable::new(0); let green_count = Mutable::new(0); let red_count = Mutable::new(0); let yellow_count = Mutable::new(0); world.insert_resource(Spawner(MutableTimer::from(spawn_rate.clone()))); world.insert_resource(Despawner(MutableTimer::from(despawn_rate.clone()))); world.insert_resource(Counts { blue: blue_count.clone(), green: green_count.clone(), red: red_count.clone(), yellow: yellow_count.clone(), }); let counts = MutableVec::new_with_values(vec![ blue_count.clone(), green_count.clone(), red_count.clone(), yellow_count.clone(), ]); El::::new() .width(Val::Percent(100.)) .height(Val::Percent(100.)) .child( Row::::new() .with_style(|mut style| style.column_gap = Val::Px(50.)) .item( El::::new().width(Val::Px(HEIGHT)).height(Val::Px(HEIGHT)), // can't put non ui nodes on top of ui nodes; yes u can https://discord.com/channels/691052431525675048/743663673393938453/1192729978744352858 // Column::::new() // .with_z_index(|z_index| *z_index = ZIndex::Global(1)) // .item(Row::::new().item(box_(Category::A)).item(box_(Category::B))) // .item(Row::::new().item(box_(Category::C)).item(box_(Category::D))) ) .item( Column::::new() .with_style(|mut style| { style.row_gap = Val::Px(50.); style.padding.left = Val::Px(50.); }) .item( Row::::new() .item( Column::::new() .align_content(Align::new().left()) .with_style(|mut style| style.row_gap = Val::Px(10.)) .item(category_count(ColorCategory::Blue, blue_count.signal())) .item(category_count(ColorCategory::Green, green_count.signal())) .item(category_count(ColorCategory::Red, red_count.signal())) .item(category_count(ColorCategory::Yellow, yellow_count.signal())), ) .item( text_labeled_count("total", { counts .signal_vec_cloned() .map_signal(|count| count.signal()) .to_signal_map(|counts| counts.iter().sum()) .dedupe() }) .align(Align::new().right()) .update_raw_el(|raw_el| { raw_el.with_component::