haalka 0.7.1

ergonomic reactive Bevy UI library powered by FRP signals
Documentation
//! Simple calculator. Spurred by <https://discord.com/channels/691052431525675048/885021580353237032/1263661461364932639>.

mod utils;
use utils::*;

use bevy::prelude::*;
use calc::*;
use haalka::prelude::*;
use rust_decimal::prelude::*;

fn main() {
    App::new()
        .add_plugins(examples_plugin)
        .add_systems(
            Startup,
            (
                |world: &mut World| {
                    ui_root().spawn(world);
                },
                camera,
            ),
        )
        .insert_resource(Output(String::new()))
        .insert_resource(Error)
        .run();
}

const BLUE: Color = Color::srgb(91. / 255., 206. / 255., 250. / 255.);
const PINK: Color = Color::srgb(245. / 255., 169. / 255., 184. / 255.);
const FONT_SIZE: f32 = 50.0;
const WIDTH: f32 = 500.;
const BUTTON_SIZE: f32 = WIDTH / 5.;
const GAP: f32 = BUTTON_SIZE / 5.;
const HEIGHT: f32 = BUTTON_SIZE * 5. + GAP * 6.;

#[derive(Resource, Clone)]
struct Error;

#[derive(Resource, Clone, Deref, DerefMut)]
struct Output(String);

fn textable_element(text_signal: impl Signal<Item = impl Into<String> + 'static> + 'static) -> El<Node> {
    El::<Node>::new()
        .with_node(|mut node| node.border = UiRect::all(Val::Px(2.0)))
        .border_color(BorderColor::all(Color::WHITE))
        .child(
            El::<Text>::new()
                .text_font(TextFont::from_font_size(FONT_SIZE))
                .text_color(TextColor(Color::WHITE))
                .text_signal(text_signal.map_in(Text::new).map_in(Some)),
        )
}

#[rustfmt::skip]
fn buttons() -> [&'static str; 16] {
    [
        "7", "8", "9", "/",
        "4", "5", "6", "*",
        "1", "2", "3", "-",
        "0", ".", "=", "+",
    ]
}

fn button(symbol: &'static str) -> El<Node> {
    textable_element(signal::always(symbol))
        .with_node(|mut node| {
            node.width = Val::Px(BUTTON_SIZE);
            node.height = Val::Px(BUTTON_SIZE);
        })
        .align_content(Align::center())
}

fn input_button(symbol: &'static str) -> impl Element {
    let lazy_entity = LazyEntity::new();
    button(symbol)
        .insert((Pickable::default(), Hoverable))
        .lazy_entity(lazy_entity.clone())
        .cursor(CursorIcon::System(SystemCursorIcon::Pointer))
        .background_color_signal(
            signal::from_entity(lazy_entity)
                .has_component::<Hovered>()
                .dedupe()
                .map_bool_in(|| BLUE, || PINK)
                .map_in(BackgroundColor)
                .map_in(Some),
        )
        .on_click(move |In(_), mut output: ResMut<Output>, mut commands: Commands| {
            if symbol == "=" {
                if let Ok(result) = Context::<f64>::default().evaluate(&output)
                    && let Some(result) = Decimal::from_f64((result * 100.).round() / 100.)
                {
                    **output = result.normalize().to_string();
                    return;
                }
                commands.insert_resource(Error);
            } else {
                **output += symbol;
            }
        })
}

fn display() -> impl Element {
    textable_element(signal::from_resource_changed::<Output>().map_in(deref_cloned))
        .with_builder(|builder| {
            builder.component_signal(
                signal::from_resource_option::<Error>()
                    .map_in_ref(Option::is_some)
                    .dedupe()
                    .map_true_in(|| Outline::new(Val::Px(4.0), Val::ZERO, bevy::color::palettes::basic::RED.into())),
            )
        })
        .with_node(|mut node| {
            node.width = Val::Px(BUTTON_SIZE * 3. + GAP * 2.);
            node.height = Val::Px(BUTTON_SIZE);
            node.padding = UiRect::all(Val::Px(GAP));
            node.overflow = Overflow::clip();
        })
        .background_color(BackgroundColor(BLUE))
        .align_content(Align::new().right().center_y())
}

fn clear_button() -> impl Element {
    let lazy_entity = LazyEntity::new();
    let output_empty = signal::from_resource_changed::<Output>()
        .map_in(deref_cloned)
        .map_in_ref(String::is_empty);
    let hovered = signal::from_entity(lazy_entity.clone())
        .has_component::<Hovered>()
        .dedupe();
    button("c")
        .insert((Pickable::default(), Hoverable))
        .lazy_entity(lazy_entity.clone())
        .background_color_signal(
            signal::zip!(output_empty.clone(), hovered)
                .map_in(|(output_empty, hovered)| {
                    if output_empty {
                        BLUE
                    } else if hovered {
                        bevy::color::palettes::basic::RED.into()
                    } else {
                        PINK
                    }
                })
                .dedupe()
                .map_in(BackgroundColor)
                .map_in(Some),
        )
        .cursor_disableable_signal(CursorIcon::System(SystemCursorIcon::Pointer), output_empty)
        .on_click(|In(_), mut output: ResMut<Output>| output.0.clear())
}

fn ui_root() -> impl Element {
    let error_clearer = signal::from_resource_changed::<Output>()
        .map(|In(_), mut commands: Commands| commands.remove_resource::<Error>())
        .task();
    El::<Node>::new()
        .with_builder(|builder| builder.hold_tasks([error_clearer]))
        .with_node(|mut node| {
            node.width = Val::Percent(100.);
            node.height = Val::Percent(100.);
        })
        .insert(Pickable::default())
        .cursor(CursorIcon::default())
        .align_content(Align::center())
        .child(
            Column::<Node>::new()
                .background_color(BackgroundColor(PINK))
                .align(Align::center())
                .with_node(|mut node| {
                    node.height = Val::Px(HEIGHT);
                    node.width = Val::Px(WIDTH);
                    node.row_gap = Val::Px(GAP);
                    node.padding = UiRect::all(Val::Px(GAP));
                })
                .item(
                    Row::<Node>::new()
                        .align(Align::center())
                        .with_node(|mut node| node.column_gap = Val::Px(GAP))
                        .item(clear_button())
                        .item(display()),
                )
                .item(
                    Row::<Node>::new()
                        .multiline()
                        .align_content(Align::center())
                        .with_node(|mut node| {
                            node.row_gap = Val::Px(GAP);
                            node.column_gap = Val::Px(GAP);
                        })
                        .items(buttons().into_iter().map(input_button)),
                ),
        )
}

fn camera(mut commands: Commands) {
    commands.spawn(Camera2d);
}