diff --git a/Cargo.lock b/Cargo.lock index 7d5d46b..daa5a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3129,6 +3129,7 @@ dependencies = [ "lazy_static", "neko_derive", "pretty_assertions", + "rand", "regex", "thiserror 2.0.17", ] diff --git a/Cargo.toml b/Cargo.toml index 8f48723..1f337a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ thiserror = "2" [dev-dependencies] bevy = { version = "0.17", default-features = true } pretty_assertions = "1.4" +rand = "0.9.2" [features] default = ["hot-reload"] diff --git a/assets/animated.neko_ui b/assets/animated.neko_ui new file mode 100644 index 0000000..113ce4e --- /dev/null +++ b/assets/animated.neko_ui @@ -0,0 +1,107 @@ +var random-color = #ffffff; +var random-num = 20; + +var paddings = 5; + +style p { + color: #000000; +} + +style p +bouncy-text { + font-size: $random-num; +} + +style div +rainbow { + background-color: $random-color; +} + + +def inner { + layout div { + class rainbow; + padding: $paddings; + output; + } +} + +def outer { + layout div { + padding: $paddings; + background-color: #cccccc; + + with inner { + output; + } + } +} + + +def item { + layout outer { + with p { + class bouncy-text; + text: "Hi!"; + } + output; + } +} + +def row { + layout div { + flex-direction: row; + column-gap: 5; + + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + with item {} + + output; + } +} + +layout div { + width: 100%; + align-items: center; + justify-content: center; + + flex-direction: column; + row-gap: 5; + + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} +} diff --git a/assets/board.neko_ui b/assets/board.neko_ui new file mode 100644 index 0000000..afa6927 --- /dev/null +++ b/assets/board.neko_ui @@ -0,0 +1,133 @@ +var cell-size = 20; +var gap = 1; + +style div +box { + background-color: #cccccc; +} +style div +box +hovered { + background-color: #999999; +} +style div +box +pressed { + background-color: #666666; +} + +style div +box +active { + background-color: #181818ff; +} +style div +box +active +hovered { + background-color: #464646ff; +} +style div +box +active +pressed { + background-color: #5c5c5cff; +} + +def cell { + layout div { + class interactable; + class box; + width: $cell-size; + height: $cell-size; + + output; + } +} + +def row { + layout div { + flex-direction: row; + column-gap: $gap; + + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + with cell {} + + output; + } +} + +layout div { + width: 100%; + align-items: center; + justify-content: center; + + flex-direction: column; + row-gap: $gap; + + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} + with row {} +} diff --git a/assets/interactions.neko_ui b/assets/interactions.neko_ui new file mode 100644 index 0000000..91caec1 --- /dev/null +++ b/assets/interactions.neko_ui @@ -0,0 +1,129 @@ +// global variables that are controlled dynamically. +var width = 500; +var color = #000000; + +var text-color = #614a31; + +style div +outer-menu { + background-color: #D5A96E; + border-color: #5B4A31; + border-thickness: 4px; + border-radius: 12px; +} + +style div +inner-menu { + background-color: #d9ad72; + border-color: #c1935b; + border-thickness: 4px; + border-radius: 12px; +} + +style p { + color: #000000; +} + +style p +h1 { + font-size: 32; + color: $color; +} + +def h1 { + var text = "Text"; + + layout div { + flex-direction: column; + + with p { + class h1; + text: $text; + } + + with div { + height: 2px; + width: 100%; + + background-color: #614a31; + border-radius: 4px; + } + + output; + } +} + + +style div +button { + background-color: #ab691fff; +} + +style div +button +hovered { + background-color: #945710ff; +} + +style div +button +pressed { + background-color: #8f7a65ff; +} + +def button { + var text = "Click Me"; + + layout div { + class interactable; + class button; + + padding: 10; + border-radius: 2; + + with p { + text: $text; + color: #ffffff; + } + + output; + } +} + + +layout div { + class outer-menu; + + flex-direction: column; + align-items: center; + + width: $width; + height: 250px; + + margin: auto; + padding: 8px; + row-gap: 8px; + + with h1 { + text: "Message"; + } + + with div { + class inner-menu; + + flex-grow: 1; + flex-direction: column; + width: 100%; + padding: 8px; + justify-content: space-between; + + with div { + flex-direction: column; + + with p { + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec pharetra malesuada tortor, sit amet consequat libero posuere ac."; + } + } + + with div { + padding: 5; + column-gap: 10; + justify-content: end; + + with button { text: "Yes"; } + with button { text: "No"; } + } + } +} diff --git a/assets/stress.neko_ui b/assets/stress.neko_ui new file mode 100644 index 0000000..6e7d341 --- /dev/null +++ b/assets/stress.neko_ui @@ -0,0 +1,97 @@ + + +def item { + var color = #aaaaaa; + var value = ""; + + layout div { + padding: 10; + background-color: $color; + border-radius: 4; + + with p { + text: $value; + } + + output; + } +} + +def row { + var c = #aaaaaa; + + layout div { + flex-direction: row; + column-gap: 10; + + with item { color: $c; value: "0"; } + with item { color: $c; value: "1"; } + with item { color: $c; value: "2"; } + with item { color: $c; value: "3"; } + with item { color: $c; value: "4"; } + with item { color: $c; value: "5"; } + with item { color: $c; value: "6"; } + with item { color: $c; value: "7"; } + with item { color: $c; value: "8"; } + with item { color: $c; value: "9"; } + with item { color: $c; value: "10"; } + with item { color: $c; value: "11"; } + with item { color: $c; value: "12"; } + with item { color: $c; value: "13"; } + with item { color: $c; value: "14"; } + with item { color: $c; value: "15"; } + with item { color: $c; value: "16"; } + with item { color: $c; value: "17"; } + with item { color: $c; value: "18"; } + with item { color: $c; value: "19"; } + with item { color: $c; value: "20"; } + with item { color: $c; value: "21"; } + with item { color: $c; value: "22"; } + with item { color: $c; value: "23"; } + with item { color: $c; value: "24"; } + with item { color: $c; value: "25"; } + with item { color: $c; value: "26"; } + with item { color: $c; value: "27"; } + with item { color: $c; value: "28"; } + with item { color: $c; value: "29"; } + with item { color: $c; value: "30"; } + with item { color: $c; value: "31"; } + with item { color: $c; value: "32"; } + with item { color: $c; value: "33"; } + with item { color: $c; value: "34"; } + with item { color: $c; value: "35"; } + with item { color: $c; value: "36"; } + + output; + } +} + + +layout div { + justify-content: start; + align-items: start; + padding: 5; + flex-direction: column; + row-gap: 10; + + with row { c: #ffaaaa; } + with row { c: #ffffaa; } + with row { c: #aaffaa; } + with row { c: #aaffff; } + with row { c: #aaaaff; } + with row { c: #ffaaff; } + with row { c: #ffaaaa; } + with row { c: #ffffaa; } + with row { c: #aaffaa; } + with row { c: #aaffff; } + with row { c: #aaaaff; } + with row { c: #ffaaff; } + with row { c: #ffaaaa; } + with row { c: #ffffaa; } + with row { c: #aaffaa; } + with row { c: #aaffff; } + with row { c: #aaaaff; } + with row { c: #ffaaff; } + with row { c: #ffaaaa; } + with row { c: #ffffaa; } +} \ No newline at end of file diff --git a/assets/test.neko_ui b/assets/test.neko_ui new file mode 100644 index 0000000..86be947 --- /dev/null +++ b/assets/test.neko_ui @@ -0,0 +1,52 @@ +var p = 20; +var text-color = #000000; +var text-size = 20; + +style p { + color: $text-color; + font-size: $text-size; +} + + +def inner { + var color = #aaaaaa; + + layout div { + padding: $p; + background-color: $color; + + output; + } +} + +def outer { + var outer_color = #aaaaaa; + var inner_color = #aaaaaa; + + var color = $outer_color; + + layout div { + padding: $p; + background-color: $color; + + with inner { + color: $inner_color; + + output; + } + } +} + +layout div { + align-items: start; + padding: $p; + + with outer { + outer_color: #ffaaaa; + inner_color: #aaffaa; + + with p { + text: "Hi!"; + } + } +} \ No newline at end of file diff --git a/examples/animated.rs b/examples/animated.rs new file mode 100644 index 0000000..ed1c428 --- /dev/null +++ b/examples/animated.rs @@ -0,0 +1,184 @@ +use std::collections::VecDeque; + +use bevy::color::palettes::css::WHITE; +use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; +use bevy::prelude::*; +use neko_maid::components::NekoUITree; +use neko_maid::parse::value::PropertyValue; + +#[derive(Resource, Clone)] +pub struct FpsSettings { + pub startup_visibility: Visibility, +} +impl Default for FpsSettings { + fn default() -> Self { + Self { + startup_visibility: Visibility::Hidden, + } + } +} + +/// Marker to find the container entity so we can show/hide the FPS counter +#[derive(Component)] +struct FpsRoot; + +/// Marker to find the text entity so we can update it +#[derive(Component)] +struct FpsText; + +#[derive(Resource, Default)] +pub struct FpsHistory { + last_update: f64, + values: VecDeque, + sum: f64, +} +impl FpsHistory { + pub fn push(&mut self, value: f64) { + self.sum += value; + self.values.push_back(value); + + while self.values.len() > 10 { + let Some(v) = self.values.pop_front() else { + break; + }; + self.sum -= v; + } + } + + pub fn mean(&self) -> Option { + if self.values.is_empty() { + return None; + } + Some(self.sum / self.values.len() as f64) + } +} + +fn setup_fps_counter(settings: Res, mut commands: Commands) { + let font = TextFont { + font_size: 20.0, + ..Default::default() + }; + let color = TextColor(WHITE.into()); + + commands.spawn(( + FpsRoot, + Node { + position_type: PositionType::Absolute, + right: Val::Percent(1.), + top: Val::Percent(1.), + bottom: Val::Auto, + left: Val::Auto, + padding: UiRect::all(Val::Px(4.0)), + ..Default::default() + }, + settings.startup_visibility, + ZIndex(i32::MAX), + BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.25)), + children![ + (Text("FPS:".to_string()), font.clone(), color.clone()), + (FpsText, Text("N/A".to_string()), font, color), + ], + )); +} + +fn fps_text_update_system( + diagnostics: Res, + time: Res