Skip to content

Commit e976e73

Browse files
committed
Finished core parsing and spawning functionality.
Signed-off-by: TheDudeFromCI <thedudefromci@gmail.com>
1 parent e1f90d1 commit e976e73

31 files changed

+5001
-3205
lines changed

Cargo.lock

Lines changed: 2222 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ bevy = { version = "0.17", default-features = false, features = [
1111
"bevy_ui_render",
1212
"bevy_text",
1313
"bevy_asset",
14+
"bevy_log",
1415
] }
15-
common_macros = "0.1.1"
16-
lazy_static = "1.5.0"
17-
thiserror = "2.0.17"
16+
lazy_static = "1.5"
17+
regex = "1.12"
18+
thiserror = "2"
1819

1920
[dev-dependencies]
20-
pretty_assertions = "1.4.1"
21+
bevy = { version = "0.17", default-features = true }
22+
pretty_assertions = "1.4"
23+
24+
[features]
25+
default = ["hot-reload"]
26+
hot-reload = ["bevy/file_watcher"]

assets/example.neko_ui

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,12 @@
1-
import "global_theme";
2-
3-
var sky: #7777ff;
4-
5-
style div +pressed {
6-
background_color: red;
7-
}
8-
9-
style div +hovered !disabled {
10-
with p {
11-
text-size: 16;
12-
}
1+
style div +outer-menu {
2+
background-color: #D5A96E;
3+
border-color: #5B4A31;
4+
border-thickness: 4px;
135
}
146

157
layout div {
16-
+outer-menu
17-
18-
background_color: $sky;
19-
anchor: top-left;
20-
21-
with div {
22-
// Comment
8+
class outer-menu;
239

24-
anchor: top;
25-
width: 150px;
26-
height: 100px;
27-
}
10+
width: 400px;
11+
height: 600px;
2812
}

example/menu.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use bevy::prelude::*;
2+
use neko_maid::components::NekoUITree;
3+
4+
fn main() {
5+
App::new()
6+
.add_plugins(DefaultPlugins)
7+
.add_plugins(neko_maid::NekoMaidPlugin)
8+
.add_systems(Startup, setup)
9+
.run();
10+
}
11+
12+
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
13+
commands.spawn(Camera2d);
14+
15+
let handle = asset_server.load("example.neko_ui");
16+
commands.spawn(NekoUITree::new(handle));
17+
}

src/asset.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! The NekoMaid style asset, and asset loader for NekoMaid ui files.
2+
3+
use std::time::Instant;
4+
5+
use bevy::asset::io::Reader;
6+
use bevy::asset::{AssetLoader, LoadContext, LoadDirectError};
7+
use bevy::prelude::*;
8+
9+
use crate::native::NATIVE_WIDGETS;
10+
use crate::parse::module::Module;
11+
use crate::parse::{NekoMaidParseError, NekoMaidParser};
12+
13+
/// A NekoMaid UI asset.
14+
#[derive(Debug, Asset, TypePath, Deref)]
15+
pub struct NekoMaidUI(Module);
16+
17+
/// The asset loader for NekoMaid ui files.
18+
#[derive(Debug, Default)]
19+
pub struct NekoMaidAssetLoader;
20+
impl AssetLoader for NekoMaidAssetLoader {
21+
type Asset = NekoMaidUI;
22+
type Settings = ();
23+
type Error = NekoMaidAssetLoaderError;
24+
25+
async fn load(
26+
&self,
27+
reader: &mut dyn Reader,
28+
_: &Self::Settings,
29+
load_context: &mut LoadContext<'_>,
30+
) -> Result<Self::Asset, Self::Error> {
31+
let now = Instant::now();
32+
33+
let mut bytes = Vec::new();
34+
reader.read_to_end(&mut bytes).await?;
35+
36+
let text_file = String::from_utf8(bytes)?;
37+
let mut parser = NekoMaidParser::tokenize(&text_file)?;
38+
39+
for native in NATIVE_WIDGETS.iter() {
40+
parser.register_native_widget(native.clone());
41+
}
42+
43+
for import in parser.predict_imports().clone() {
44+
let path = load_context.asset_path();
45+
let Ok(module_path) = path.resolve(&format!("../{}.neko_ui", import)) else {
46+
continue;
47+
};
48+
49+
let asset = load_context
50+
.loader()
51+
.immediate()
52+
.load::<NekoMaidUI>(&module_path)
53+
.await?;
54+
55+
let module = asset.get().0.clone();
56+
parser.add_module(import.clone(), module);
57+
}
58+
59+
let module = parser.finish()?;
60+
61+
let elapsed = now.elapsed().as_millis();
62+
debug!(
63+
"Loaded NekoMaid UI asset {} in {} ms.",
64+
load_context.path().display(),
65+
elapsed,
66+
);
67+
68+
Ok(NekoMaidUI(module))
69+
}
70+
71+
fn extensions(&self) -> &[&str] {
72+
&["neko_ui"]
73+
}
74+
}
75+
76+
/// Errors that can occur while loading a NekoMaid asset.
77+
#[derive(Debug, thiserror::Error)]
78+
pub enum NekoMaidAssetLoaderError {
79+
/// An I/O error occurred while loading the asset.
80+
#[error("I/O error: {0}")]
81+
IO(#[from] std::io::Error),
82+
83+
/// The asset contained invalid UTF-8.
84+
#[error("Invalid UTF-8: {0}")]
85+
InvalidUtf8(#[from] std::string::FromUtf8Error),
86+
87+
/// An error occurred while parsing the asset.
88+
#[error("Syntax error: {0}")]
89+
FailedToParse(#[from] NekoMaidParseError),
90+
91+
/// An error occurred while loading a dependency.
92+
#[error("{0}")]
93+
FailedToLoadDependency(#[from] LoadDirectError),
94+
}

src/components.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! Components used for the NekoMaid plugin.
2+
3+
use bevy::prelude::*;
4+
5+
use crate::asset::NekoMaidUI;
6+
7+
/// A component representing the root of a NekoMaid UI tree.
8+
#[derive(Debug, Component)]
9+
#[require(Node)]
10+
pub struct NekoUITree {
11+
/// The NekoMaid UI asset associated with this tree.
12+
asset: Handle<NekoMaidUI>,
13+
14+
/// Whether the tree needs to be re-spawned.
15+
dirty: bool,
16+
}
17+
18+
impl NekoUITree {
19+
/// Creates a new NekoUITree with the given asset handle.
20+
pub fn new(asset: Handle<NekoMaidUI>) -> Self {
21+
Self { asset, dirty: true }
22+
}
23+
24+
/// Returns a reference to the asset handle of this tree.
25+
pub fn asset(&self) -> &Handle<NekoMaidUI> {
26+
&self.asset
27+
}
28+
29+
/// Marks the tree as dirty, indicating that it needs to be re-spawned.
30+
pub fn mark_dirty(&mut self) {
31+
self.dirty = true;
32+
}
33+
34+
/// Clears the dirty flag.
35+
pub fn clear_dirty(&mut self) {
36+
self.dirty = false;
37+
}
38+
39+
/// Returns whether the tree is dirty.
40+
pub fn is_dirty(&self) -> bool {
41+
self.dirty
42+
}
43+
}

src/lib.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,39 @@
44

55
use bevy::prelude::*;
66

7+
use crate::asset::{NekoMaidAssetLoader, NekoMaidUI};
8+
9+
pub mod asset;
10+
pub mod components;
11+
pub mod native;
712
pub mod parse;
8-
pub mod vm;
13+
mod systems;
914

1015
/// A Bevy UI plugin: NekoMaid
1116
///
1217
/// This plugin provides core functionality for the NekoMaid framework,
1318
/// including UI components and systems, assets, and high-level widgets.
1419
pub struct NekoMaidPlugin;
1520
impl Plugin for NekoMaidPlugin {
16-
fn build(&self, _: &mut App) {}
21+
fn build(&self, app_: &mut App) {
22+
app_.init_asset::<NekoMaidUI>()
23+
.init_asset_loader::<NekoMaidAssetLoader>()
24+
.add_systems(
25+
Update,
26+
(
27+
systems::spawn_tree.in_set(NekoMaidSystems::UpdateTree),
28+
systems::update_tree.in_set(NekoMaidSystems::AssetListener),
29+
),
30+
);
31+
}
32+
}
33+
34+
/// System sets used by the NekoMaid plugin.
35+
#[derive(Debug, SystemSet, Clone, Copy, PartialEq, Eq, Hash)]
36+
pub enum NekoMaidSystems {
37+
/// System for spawning UI trees.
38+
UpdateTree,
39+
40+
/// System for listening for asset changes.
41+
AssetListener,
1742
}

src/native.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! Defines the native widgets available in NekoMaid UI.
2+
3+
use bevy::platform::collections::HashMap;
4+
use bevy::prelude::*;
5+
use lazy_static::lazy_static;
6+
7+
use crate::parse::element::NekoElement;
8+
use crate::parse::property::PropertyValue;
9+
use crate::parse::widget::NativeWidget;
10+
11+
lazy_static! {
12+
/// The list of native widgets available in NekoMaid UI.
13+
pub static ref NATIVE_WIDGETS: Vec<NativeWidget> = vec![NativeWidget {
14+
name: String::from("div"),
15+
properties: {
16+
let mut m = HashMap::new();
17+
m.insert("width".into(), "auto".into());
18+
m.insert("height".into(), "auto".into());
19+
m.insert("background-color".into(), Color::NONE.into());
20+
21+
m.insert("border-color".into(), Color::NONE.into());
22+
m.insert("border-color-top".into(), Color::NONE.into());
23+
m.insert("border-color-left".into(), Color::NONE.into());
24+
m.insert("border-color-right".into(), Color::NONE.into());
25+
m.insert("border-color-bottom".into(), Color::NONE.into());
26+
27+
m.insert("border-thickness".into(), Color::NONE.into());
28+
m.insert("border-thickness-top".into(), Color::NONE.into());
29+
m.insert("border-thickness-left".into(), Color::NONE.into());
30+
m.insert("border-thickness-right".into(), Color::NONE.into());
31+
m.insert("border-thickness-bottom".into(), Color::NONE.into());
32+
m
33+
},
34+
spawn_func: spawn_div,
35+
}];
36+
}
37+
38+
/// Spawns a `div` native widget.
39+
fn spawn_div(commands: &mut Commands, parent: Entity, element: &NekoElement) -> Entity {
40+
commands
41+
.spawn((
42+
ChildOf(parent),
43+
node_bundle(element),
44+
background_color_bundle(element),
45+
border_color_bundle(element),
46+
))
47+
.id()
48+
}
49+
50+
/// Build [`Node`] bundle
51+
fn node_bundle(element: &NekoElement) -> Node {
52+
let width = element.get_property("width").map_or(Val::Auto, as_val);
53+
let height = element.get_property("height").map_or(Val::Auto, as_val);
54+
55+
let border_thickness = element
56+
.get_property("border-thickness")
57+
.map_or(Val::Auto, as_val);
58+
let border_thickness_top = element
59+
.get_property("border-thickness-top")
60+
.map_or(border_thickness, as_val);
61+
let border_thickness_left = element
62+
.get_property("border-thickness-left")
63+
.map_or(border_thickness, as_val);
64+
let border_thickness_right = element
65+
.get_property("border-thickness-right")
66+
.map_or(border_thickness, as_val);
67+
let border_thickness_bottom = element
68+
.get_property("border-thickness-bottom")
69+
.map_or(border_thickness, as_val);
70+
71+
Node {
72+
width,
73+
height,
74+
border: UiRect {
75+
top: border_thickness_top,
76+
left: border_thickness_left,
77+
right: border_thickness_right,
78+
bottom: border_thickness_bottom,
79+
},
80+
..default()
81+
}
82+
}
83+
84+
/// Build [`BorderColor`] bundle
85+
fn border_color_bundle(element: &NekoElement) -> BorderColor {
86+
let border_color = element
87+
.get_property("border-color")
88+
.map_or(Color::NONE, as_color);
89+
let border_color_top = element
90+
.get_property("border-color-top")
91+
.map_or(border_color, as_color);
92+
let border_color_left = element
93+
.get_property("border-color-left")
94+
.map_or(border_color, as_color);
95+
let border_color_right = element
96+
.get_property("border-color-right")
97+
.map_or(border_color, as_color);
98+
let border_color_bottom = element
99+
.get_property("border-color-bottom")
100+
.map_or(border_color, as_color);
101+
102+
BorderColor {
103+
top: border_color_top,
104+
left: border_color_left,
105+
right: border_color_right,
106+
bottom: border_color_bottom,
107+
}
108+
}
109+
110+
/// Build [`BackgroundColor`] bundle
111+
fn background_color_bundle(element: &NekoElement) -> BackgroundColor {
112+
let bg_color = element
113+
.get_property("background-color")
114+
.map_or(Color::NONE, as_color);
115+
116+
BackgroundColor(bg_color)
117+
}
118+
119+
/// Converts a [`PropertyValue`] to a Bevy UI [`Val`].
120+
fn as_val(property: &PropertyValue) -> Val {
121+
match property {
122+
PropertyValue::String(s) if s == "auto" => Val::Auto,
123+
PropertyValue::Pixels(n) => Val::Px(*n as f32),
124+
PropertyValue::Percent(n) => Val::Percent(*n as f32),
125+
_ => {
126+
warn_once!("Failed to convert PropertyValue {:?} to Val", property);
127+
Val::Auto
128+
}
129+
}
130+
}
131+
132+
/// Converts a [`PropertyValue`] to a Bevy UI [`Color`].
133+
fn as_color(property: &PropertyValue) -> Color {
134+
match property {
135+
PropertyValue::Color(c) => *c,
136+
_ => {
137+
warn_once!("Failed to convert PropertyValue {:?} to Color", property);
138+
Color::NONE
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)