├── .gitignore ├── src └── lib.rs ├── crates ├── core │ ├── syntax │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── ui │ ├── syntax │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── README.md └── ecs │ ├── syntax │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── Cargo.toml │ ├── src │ └── lib.rs │ └── README.md ├── examples ├── core_code.rs ├── ecs_picking.rs ├── ecs_simple_spawn.rs ├── ui_bundle.rs └── entity_macro.rs ├── LICENSE-MIT ├── Cargo.toml ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use mevy_core::*; 2 | pub use mevy_ecs::*; 3 | 4 | #[cfg(feature = "ui")] 5 | pub use mevy_ui::*; 6 | -------------------------------------------------------------------------------- /crates/core/syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_core_syntax" 3 | version = "0.2.4" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "token handling for mevy_core!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["proc","bevy"] 11 | 12 | [dependencies.deki] 13 | workspace = true 14 | features = ["proc"] 15 | -------------------------------------------------------------------------------- /examples/core_code.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use mevy::*; 3 | 4 | pub fn main() { 5 | println!{"{:#?}",code!{BoxShadow(vec![ShadowStyle{ 6 | // use #... is replaced with Color, meaning you can e.g. use methods 7 | color: #FF1265.mix(&#F93ECA,0.4).with_alpha(0.2), 8 | x_offset: 100px, 9 | y_offset: 50%, 10 | spread_radius: 3.1vh, 11 | blur_radius: 40.23vmax, 12 | }])}}; 13 | } 14 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_core" 3 | version = "0.1.1" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "core bevy macros for mevy!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["macro","bevy"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | mevy_core_syntax.workspace = true 17 | 18 | [dependencies.deki] 19 | workspace = true 20 | features = ["proc"] 21 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use mevy_core_syntax::code as code_syntax; 2 | use proc_macro::TokenStream as CompilerTokens; 3 | 4 | // Macro \\ 5 | 6 | /// Replaces the following patterns: 7 | /// - `#rgb`/`#rgba`/`#rrggbb`/`#rrggbbaa` -> `Color` 8 | /// - `0px`/`0%`/`0vw`/`0vh`/`0vmin`/`0vmax`/`@`(auto) -> `Val` 9 | /// - `[>0px]`/`[>0px 0px]`/`[>0px 0px 0px]`/`[>0px 0px 0px 0px]` -> UiRect 10 | #[proc_macro] 11 | pub fn code (stream:CompilerTokens) -> CompilerTokens { 12 | code_syntax(stream.into()).into() 13 | } 14 | 15 | 16 | // EOF \\ 17 | -------------------------------------------------------------------------------- /crates/ui/syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_ui_syntax" 3 | version = "0.4.2" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "token handling for mevy_ui!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["proc","bevy"] 11 | 12 | [dependencies] 13 | mevy_core_syntax.workspace = true 14 | 15 | [dependencies.deki] 16 | workspace = true 17 | features = ["proc"] 18 | 19 | [features] 20 | default = [] 21 | "0.15" = [] 22 | "0.16" = [] 23 | "0.16-rc" = ["0.16"] 24 | "0.17" = [] 25 | -------------------------------------------------------------------------------- /crates/ecs/syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_ecs_syntax" 3 | version = "0.2.4" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "token handling for mevy_ecs!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["proc","bevy"] 11 | 12 | [dependencies] 13 | mevy_core_syntax.workspace = true 14 | 15 | [dependencies.deki] 16 | workspace = true 17 | features = ["proc"] 18 | 19 | [features] 20 | default = [] 21 | "0.15" = [] 22 | "0.16" = [] 23 | "0.16-rc" = ["0.16"] 24 | "0.17" = [] 25 | -------------------------------------------------------------------------------- /crates/ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_ui" 3 | version = "0.3.1" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "bevy_ui macro, that adds a CSS-like syntax!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["macro","bevy","bevy_ui"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | mevy_ui_syntax.workspace = true 17 | 18 | [dependencies.deki] 19 | workspace = true 20 | features = ["proc"] 21 | 22 | [features] 23 | default = [] 24 | "0.15" = ["mevy_ui_syntax/0.15"] 25 | "0.16-rc" = ["mevy_ui_syntax/0.16-rc"] 26 | "0.16" = ["mevy_ui_syntax/0.16"] 27 | "0.17" = ["mevy_ui_syntax/0.17"] 28 | -------------------------------------------------------------------------------- /crates/ecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy_ecs" 3 | version = "0.2.4" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "bevy_ecs macros, simplifying child spawning and picking!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["macro","bevy","bevy_ecs"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | mevy_ecs_syntax.workspace = true 17 | 18 | [dependencies.deki] 19 | workspace = true 20 | features = ["proc"] 21 | 22 | [features] 23 | default = ["experimental"] 24 | experimental = [] 25 | "0.15" = ["mevy_ecs_syntax/0.15"] 26 | "0.16" = ["mevy_ecs_syntax/0.16"] 27 | "0.16-rc" = ["mevy_ecs_syntax/0.16-rc"] 28 | "0.17" = ["mevy_ecs_syntax/0.17"] 29 | -------------------------------------------------------------------------------- /examples/ecs_picking.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use mevy::*; 3 | 4 | pub fn main() { 5 | let mut app = App::new(); 6 | app.add_plugins(DefaultPlugins) 7 | .add_systems(Startup,startup); 8 | app.run(); 9 | } 10 | 11 | fn startup(mut world:Commands){ 12 | entity!{Camera2d::default()} 13 | 14 | entity!{ 15 | ui!(( 16 | size: 80px 50px; 17 | background: gray; 18 | border_radius: 8px; 19 | justify_self: center; 20 | align_self: center; 21 | )); 22 | > Pointer { 23 | this.despawn(); 24 | }; 25 | > Pointer { 26 | this.insert(ui!(( 27 | background: red; 28 | border_radius: 16px; 29 | ))); 30 | }; 31 | > Pointer { 32 | this.insert(ui!(( 33 | background: gray; 34 | border_radius: 8px; 35 | ))); 36 | }; 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Dekirisu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/ecs_simple_spawn.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use bevy::prelude::*; 3 | use mevy::*; 4 | 5 | pub fn main() { 6 | let mut app = App::new(); 7 | app.add_plugins(DefaultPlugins) 8 | .add_systems(Startup,startup); 9 | app.run(); 10 | } 11 | 12 | fn startup(mut world:Commands){ 13 | entity!{Camera2d} 14 | entity!{ 15 | BackgroundColor(#ff0000); 16 | BorderColor::all(#00ffff); 17 | Node{ width:80px, height:80px, margin:[>16px], !}; 18 | .observe(destroy_on::(e1)); 19 | .observe(destroy_on::(named_child)); 20 | [ // auto-called 'e1' 21 | Node{ width:20%, height:20%, !}; 22 | BackgroundColor(#00ff00); 23 | ] 24 | [ 25 | Node{ width:20%, height:20%, !}; 26 | BackgroundColor(#00aaff); 27 | .observe(destroy_self_on::); 28 | ] 29 | [named_child][ 30 | Node{ width:20%, height:20%, !}; 31 | BackgroundColor(#00ffaa); 32 | Visibility!; 33 | ] 34 | } 35 | } 36 | 37 | fn destroy_on (entity:Entity) -> impl Fn(Trigger>,Commands) { 38 | move|_,mut world|{if let Ok(mut ecmd) = world.get_entity(entity){ 39 | ecmd.despawn(); 40 | }} 41 | } 42 | 43 | fn destroy_self_on (trigger:Trigger>,mut cmd:Commands) { 44 | cmd.entity(trigger.observer()).despawn(); 45 | } 46 | -------------------------------------------------------------------------------- /examples/ui_bundle.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use mevy::*; 3 | 4 | pub fn main() { 5 | let mut app = App::new(); 6 | app.add_plugins(DefaultPlugins) 7 | .add_systems(Startup,startup); 8 | app.run(); 9 | } 10 | 11 | fn startup(mut cmd:Commands){ 12 | cmd.spawn(Camera2d::default()); 13 | cmd.spawn(ui!(( 14 | padding: 24px; 15 | column_gap: 24px; 16 | box_shadow: _ _ 0px 0px; 17 | ))) 18 | .with_children(|p|{ 19 | p.spawn(neat_box()) 20 | .observe(|trigger:Trigger>,mut query:Query<(&mut Node,&mut BoxShadow)>|{ 21 | let (mut node,mut box_shadow) = query.get_mut(trigger.observer()).unwrap(); 22 | bigger_border_h(&mut node, &mut box_shadow); 23 | }); 24 | p.spawn(same_neat_box()); 25 | }) 26 | ; 27 | } 28 | 29 | // Bundles \\ 30 | 31 | ui!{bigger_border_h{ 32 | border: 40px _; 33 | box_shadow: _ _ 0px 0px; 34 | }} 35 | 36 | ui!{neat_box( 37 | size: 100px 100px; 38 | border: 5px #ff0000; 39 | box_shadow: 10% 10% 3px 8px #ffaa44; 40 | background: #ffffff; 41 | border_radius: 6px; 42 | neat_outline; 43 | )?} 44 | 45 | ui!{neat_outline( 46 | outline: 3px 1px #00ff00; 47 | )} 48 | 49 | fn same_neat_box() -> impl Bundle {code!{{( 50 | Node{ 51 | width: 100px, 52 | height: 100px, 53 | border: [>5px], 54 | ..default() 55 | }, 56 | BoxShadow(vec![ShadowStyle{ 57 | color: #ffaa44, 58 | x_offset: 10%, 59 | y_offset: 10%, 60 | blur_radius: 3px, 61 | spread_radius: 8px, 62 | }]), 63 | BackgroundColor(#ffffff), 64 | BorderColor::all(#ff0000), 65 | BorderRadius::all(6px), 66 | neat_outline() 67 | )}}} 68 | 69 | // EOF \\ 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mevy" 3 | version = "0.3.1" 4 | edition = "2024" 5 | 6 | authors = ["Dekirisu "] 7 | license = "MIT OR Apache-2.0" 8 | description = "A growing set of macros, which add witchcraft into bevy!" 9 | repository = "https://github.com/dekirisu/mevy/" 10 | keywords = ["macro","bevy"] 11 | 12 | 13 | [dependencies] 14 | mevy_core.workspace = true 15 | mevy_ui.workspace = true 16 | mevy_ecs.workspace = true 17 | # Optionals 18 | mevy_ui.optional = true 19 | 20 | 21 | [features] 22 | default = ["ui"] 23 | experimental = ["mevy_ecs/experimental"] 24 | ui = ["dep:mevy_ui"] 25 | "0.15" = ["mevy_ecs/0.15","mevy_ui?/0.15"] 26 | "0.16-rc" = ["mevy_ecs/0.16-rc","mevy_ui?/0.16-rc"] 27 | "0.16" = ["mevy_ecs/0.16","mevy_ui?/0.16"] 28 | "0.17" = ["mevy_ecs/0.17","mevy_ui?/0.17"] 29 | 30 | [workspace.dependencies.mevy_core] 31 | path = "crates/core" 32 | version = "0.1.1" 33 | 34 | [workspace.dependencies.mevy_core_syntax] 35 | path = "crates/core/syntax" 36 | version = "0.2.3" 37 | 38 | [workspace.dependencies.mevy_ui] 39 | path = "crates/ui" 40 | version = "0.3.1" 41 | default-features = false 42 | 43 | [workspace.dependencies.mevy_ui_syntax] 44 | path = "crates/ui/syntax" 45 | version = "0.4.2" 46 | default-features = false 47 | 48 | [workspace.dependencies.mevy_ecs] 49 | path = "crates/ecs" 50 | version = "0.2.4" 51 | default-features = false 52 | 53 | [workspace.dependencies.mevy_ecs_syntax] 54 | path = "crates/ecs/syntax" 55 | version = "0.2.4" 56 | default-features = false 57 | 58 | 59 | [workspace.dependencies] 60 | deki = "0.3.0" 61 | 62 | [workspace] 63 | members = [ 64 | "crates/*", 65 | "crates/*/syntax", 66 | ] 67 | 68 | [dev-dependencies.bevy] 69 | version = "0.17.0" 70 | default-features = false 71 | features = [ 72 | "bevy_window", 73 | "bevy_winit", 74 | "bevy_ui", 75 | "bevy_picking", 76 | "x11" 77 | ] 78 | 79 | [dev-dependencies.mevy] 80 | path = "" 81 | features = ["0.17"] 82 | -------------------------------------------------------------------------------- /crates/ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | use deki::*; 2 | use mevy_ui_syntax::*; 3 | use proc_macro::TokenStream as CompilerTokens; 4 | 5 | // Macro \\ 6 | 7 | /// Provides CSS-like syntax to either edit or create bevy_ui components. 8 | /// 9 | /// ## Available fields 10 | /// To see a full list of built-in fields, see readme of mevy_ui, here are some examples: 11 | /// ```rust 12 | /// cmd.spawn(ui!(( 13 | /// size: 50px 50px; 14 | /// background: #ff0000; 15 | /// box_shadow: 0px 0px 5px 5px #ff0000; 16 | /// border: 5px #00ff00; 17 | /// ))); 18 | /// ``` 19 | /// The same, but 'slim': 20 | /// ```rust 21 | /// cmd.spawn(ui!( w:50 h:50 bg:#f00 shadow:0+0+5+5#f00 border:5#0f0 )); 22 | /// ``` 23 | /// 24 | /// ## Possible Modes 25 | /// Depending on the delimiters & if there is a name defined, the function of this macro differs: 26 | /// - 'Slim' Inline Tuple Mode | `ui!{ w:8 h:8 l:2 t:2 }` 27 | /// - returns a tuple of mentioned UI components 28 | /// - Inline Tuple Mode | `ui!{( width: 1px; )}`: 29 | /// - returns a tuple of mentioned UI components 30 | /// - Function Tuple Mode | `ui!{func_name( width: 1px; )}`: 31 | /// - defines a function that returns a tuple of mentioned UI components 32 | /// - these can then be used as fields like this: `ui!{( func_name; )}` 33 | /// - Function Edit Mode | `ui!{func_name{ width: 2px; }}` 34 | /// - defines a function, that edits mentioned UI components 35 | /// - the parameters of this function = the needed mutable components 36 | /// - using `_` will keep the original values: e.g. `border: _ #ff0000;` 37 | /// 38 | /// ## Custom Fields 39 | /// Every function that returns any `Bundle` (even if it is just -> `impl Bundle`) can be used 40 | /// as custom field. 41 | #[proc_macro] 42 | pub fn ui (tok:CompilerTokens) -> CompilerTokens { 43 | let tok: TokenStream = tok.into(); 44 | let mut iter = tok.peek_iter(); 45 | 46 | match iter.peek().unwrap() { 47 | TokenTree::Ident(ident) if ident.to_string().chars().next().unwrap().is_lowercase() => { 48 | let ident = iter.next().unwrap().unwrap_ident(); 49 | match iter.peek().unwrap() { 50 | TokenTree::Group(g) if g.delimiter().is_parenthesis() => { 51 | let g = iter.next().unwrap().unwrap_group(); 52 | let bundle = bundle(g.stream().peek_iter(),iter.next()); 53 | qt!{pub fn #ident () -> impl Bundle {#bundle}} 54 | } 55 | TokenTree::Group(g) if g.delimiter().is_brace() => { 56 | let g = iter.next().unwrap().unwrap_group(); 57 | let prep = UiPrep::from_stream(g.stream(),false,|a,_|a.is_punct(';')); 58 | let (expected,edits) = prep.get_edits(); 59 | let attr = expected.into_iter() 60 | .map(|(v,t)|qt!(#v: &mut #t)) 61 | .collect::>(); 62 | qt!{pub fn #ident (#(#attr),*) {#edits}} 63 | } 64 | _ => { 65 | let aa = qt![#ident #(#iter)*]; 66 | bundle_slim(aa.peek_iter(),None) 67 | } 68 | } 69 | } 70 | 71 | TokenTree::Group(_) => { 72 | kill!{*Some(TokenTree::Group(group)) = iter.next()} 73 | match group.delimiter() { 74 | Delimiter::Parenthesis => bundle(group.stream().peek_iter(),iter.next()), 75 | Delimiter::Bracket => bundle_slim(group.stream().peek_iter(),iter.next()), 76 | _ => todo!{} 77 | } 78 | } 79 | 80 | _ => bundle_slim(iter,None), 81 | 82 | }.into() 83 | } 84 | 85 | 86 | // EOF \\ 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 |

9 | 10 |

11 | 12 |

[!IMPORTANT] 20 | > This crate is meant to provide macros only - no additional bevy plugins, resources, components or systems 21 | 22 | ## Setup 23 | Multiple bevy versions are supported and managed by features: 24 | ```toml 25 | # bevy 0.17 26 | mevy = {version="0.3",features=["0.17"]} 27 | 28 | # bevy 0.16 29 | mevy = {version="0.3",features=["0.16"]} 30 | 31 | # bevy 0.15 32 | mevy = {version="0.3",features=["0.15"]} 33 | ``` 34 | 35 | Then just `use` all of it: 36 | ```rust 37 | use bevy::prelude::*; 38 | use mevy::*; 39 | ``` 40 | 41 | ## Simpler Hierarchy Spawning 42 | Spawn children just by stating `[]` - the 'names' are just variables containing their `Entity` 43 | - those variables can be used anywhere in the macro - even 'before' 44 | - [read more](crates/ecs/README.md) or see [this example](examples/ecs_simple_spawn.rs). 45 | 46 | ```rust 47 | entity!{ 48 | // pass a mut World, Commands, ... variable 49 | SpecificChild(optional_child_name); // insert component 50 | .observe(..); // use method 51 | > Pointer{..}; // quick observe (e.g. 'on click') 52 | // component/bundle; 53 | // .method(..); 54 | [optional_child_name][ 55 | // component; 56 | // .method(..); 57 | ] 58 | } 59 | ``` 60 | 61 | Modify entities in a 'quick and dirty' way: 62 | - [read more](crates/ecs/README.md) or see [this example](examples/entity_macro.rs). 63 | 64 | ```rust 65 | entity!{ 66 | // select every entity with this Component 67 | // > select all children of those 68 | // >> infinitely chain those selectors 69 | .despawn(); // despawn all of the last selected 70 | } 71 | ``` 72 | 73 | ## CSS-like notation for bevy_ui 74 | Using `ui!((..))` (inner round braces) will return a tuple of **mentioned components** only. 75 | - read about **available fields**, custom fields & notation in [this readme](crates/ui/README.md) 76 | - see [this example](examples/ui_bundle.rs). 77 | ```rust 78 | // Slim Mode 79 | c.spawn(ui!( 80 | w:100 h:100 bg:#fff round:6 border:5#f00 81 | shadow:10%10%3+8#fa4 neat_outline 82 | )); 83 | 84 | // CSS-Like Mode (does the same) 85 | c.spawn(ui!(( 86 | size: 100px 100px; 87 | border: 5px #ff0000; 88 | box_shadow: 10% 10% 3px 8px #ffaa44; 89 | background: #ffffff; 90 | border_radius: 6px; 91 | neat_outline; 92 | )?)); 93 | //^ optional ? (or any token): hovering shows the returned tuple (if LSP used) 94 | 95 | /// function as custom fields or p refabs 96 | fn neat_outline() -> Outline {ui!(( 97 | outline: 3px 1px #00ff00; 98 | ))} 99 | ``` 100 | 101 | ## Code Replacement Macro 102 | Using the `code!{}` macro simplifies constructing: 103 | - `Color` by writing `#rgb`/`#rgba`/`#rrggbb`/`#rrggbbaa` 104 | - `Val` by writing `0px`/`0%`/`0vw`/`0vh`/`0vmin`/`0vmax`/`@`(auto) 105 | - `UiRect` by writing `[>0px]`/`[>0px 0px]`/`[>0px 0px 0px]`/`[>0px 0px 0px 0px]` (css-like) 106 | 107 | So you can do fun things like: 108 | ```rust 109 | let shadow = code!{BoxShadow{ 110 | // use #... is replaced with Color, meaning you can e.g. use methods 111 | color: #FF1265.mix(&#F93ECA,0.4).with_alpha(0.2), 112 | x_offset: 100px, 113 | y_offset: 50%, 114 | spread_radius: 3.1vh, 115 | blur_radius: 40.23vmax, 116 | }}}; 117 | let color = code!{#FF0000}; 118 | // or multiple things in the macro 119 | code!{ 120 | let color2 = #00FF00; 121 | let color3 = #6600AA; 122 | } 123 | println!{"{color2:?}"} 124 | ``` 125 | 126 | ## Design 127 | Crates are separated into: 128 | - `crate/*/syntax`: token handling, meant to be reusable 129 | - `crate/*`: actual macros, based on that 'syntax' 130 | 131 | > [!NOTE] 132 | > **Only relevant if you dig deeper into this crate:** The versions of those are not hard linked, since the macros can keep (or gain) features, even if the the syntax api has changed. So if one of those is `0.2.x` and the other `0.5.x` at some point, don't worry. 133 | -------------------------------------------------------------------------------- /crates/ecs/src/lib.rs: -------------------------------------------------------------------------------- 1 | use mevy_ecs_syntax::*; 2 | use proc_macro::TokenStream as Cokens; 3 | use TokenStream as Tokens; 4 | use deki::*; 5 | 6 | #[proc_macro] 7 | pub fn entity(stream:Cokens) -> Cokens { 8 | world_spawn_syntax(stream.into()).into() 9 | } 10 | 11 | #[proc_macro] 12 | pub fn modify(stream:Cokens) -> Cokens { 13 | let stream: Tokens = stream.into(); 14 | world_spawn_syntax(qt!(<|> #stream)).into() 15 | } 16 | 17 | 18 | // Experimental \\ 19 | // Kinda My Sandbox! 20 | 21 | // Resource \| 22 | 23 | /// Get Resource 24 | /// - required: mutable `world: World|DeferredWorld` 25 | /// - get: `let time = gere![Time].unwrap();` 26 | /// - get_mut: `let mut time = gere![mut Time].unwrap();` 27 | #[cfg(feature="experimental")] 28 | #[proc_macro] 29 | pub fn gere (item:Cokens) -> Cokens { 30 | let stream: Tokens = item.into(); 31 | let mut stream = stream.peek_iter(); 32 | let get = stream.next_if(|a|a.is_string("mut")) 33 | .map(|_|qt![get_resource_mut]) 34 | .unwrap_or(qt![get_resource]); 35 | let mut res = stream.collect::>(); 36 | let mut wrap = qt![]; 37 | if res.last().unwrap().is_punct('!'){ 38 | res.pop(); 39 | wrap = qt![.unwrap()]; 40 | } 41 | qt!{world.#get::<#(#res)*>() #wrap}.into() 42 | } 43 | 44 | /// Quickly Edit Resource (if available) 45 | /// - required: mutable `world: World|DeferredWorld` 46 | /// - usage: `gere![Struct.field = 100];` 47 | #[cfg(feature="experimental")] 48 | #[proc_macro] 49 | pub fn edre (item:Cokens) -> Cokens { 50 | let stream: Tokens = item.into(); 51 | let mut stream = stream.peek_iter(); 52 | let comp = stream.next().unwrap(); 53 | let left = Tokens::from_iter(stream); 54 | qt!{if let Some(mut data) = world.get_resource_mut::<#comp>() { 55 | data #left; 56 | }}.into() 57 | } 58 | 59 | 60 | // Component \| 61 | 62 | /// Get Component 63 | /// - required: mutable `world: World|DeferredWorld` 64 | /// - required: `me: Entity` 65 | /// - get: `let time = geco![Time].unwrap();` 66 | /// - get_mut: `let mut time = geco![mut Time].unwrap();` 67 | /// - get_cloned: `let time = geco![Time*].unwrap();` 68 | /// - has_component?: `if geco![Time?] {}` 69 | #[cfg(feature="experimental")] 70 | #[proc_macro] 71 | pub fn geco (item:Cokens) -> Cokens { 72 | let stream: Tokens = item.into(); 73 | let mut stream = stream.as_vec(); 74 | let last = match stream.last().cloned() { 75 | Some(TokenTree::Punct(p)) if p.as_char() != '>' => { 76 | stream.pop(); 77 | p.as_char() 78 | } 79 | _ => '-' 80 | }; 81 | let post = match last { 82 | '?' => qt![.is_some()], 83 | '*' => qt![.cloned()], 84 | _ => qt![] 85 | }; 86 | let get = match stream.get(0) { 87 | Some(t) if t.is_string("mut") => { 88 | stream.remove(0); 89 | qt![get_mut] 90 | } 91 | _ => qt![get] 92 | }; 93 | let enty = match stream.last().cloned() { 94 | Some(TokenTree::Group(g)) => { 95 | stream.pop(); 96 | g.stream() 97 | }, 98 | _ => qt![me] 99 | }; 100 | let comp = Tokens::from_iter(stream.into_iter()); 101 | qt!{world.#get::<#comp>(#enty)#post}.into() 102 | } 103 | 104 | /// Quickly Edit Components (if available) 105 | /// - required: mutable `world: World|DeferredWorld` 106 | /// - usage: `geco![Struct.field = 100];` 107 | #[cfg(feature="experimental")] 108 | #[proc_macro] 109 | pub fn edco (item:Cokens) -> Cokens { 110 | let stream: Tokens = item.into(); 111 | let mut stream = stream.peek_iter(); 112 | let is_deref = stream.next_if(|a|a.is_punct('*')).is_some(); 113 | let comp = stream.next().unwrap(); 114 | let rest = Tokens::from_iter(stream); 115 | let data = if is_deref {qt![*data]} else {qt![data]}; 116 | qt!{if let Some(mut data) = world.get_mut::<#comp>(me) { 117 | #data #rest; 118 | }}.into() 119 | } 120 | 121 | // Spawners \| 122 | 123 | /// "D(eferred) En(tity)" 124 | /// Alternative `entity!` for `world: DeferredWorld` 125 | /// - `den![..]`: spawn a `me: Entity` 126 | /// - `den![&..]`: edit a `me: Entity` 127 | /// - `den![*..]: edit a `world: EntityCommands` 128 | /// - `den![#Marker|..]: edit all Entities with `Marker` component 129 | #[proc_macro] 130 | pub fn den(stream:Cokens) -> Cokens { 131 | let stream: Tokens = stream.into(); 132 | let mut iter = stream.peek_iter(); 133 | let dir = en_translate(qt![-],&mut iter); 134 | let stream = Tokens::from_iter(iter); 135 | mevy_ecs_syntax::world_spawn_syntax(qt!(#dir #stream)).into() 136 | } 137 | 138 | /// "W(orld) En(tity)" 139 | /// Alternative `entity!` for `world: World` 140 | /// - `wen![..]`: spawn a `me: Entity` 141 | /// - `wen![&..]`: edit a `me: Entity` 142 | /// - `wen![*..]: edit a `world: EntityWorldMut` 143 | /// - `wen![#Marker|..]: edit all Entities with `Marker` component 144 | #[proc_macro] 145 | pub fn wen(stream:Cokens) -> Cokens { 146 | let stream: Tokens = stream.into(); 147 | let mut iter = stream.peek_iter(); 148 | let dir = en_translate(qt![+],&mut iter); 149 | let stream = Tokens::from_iter(iter); 150 | mevy_ecs_syntax::world_spawn_syntax(qt!(#dir #stream)).into() 151 | } 152 | 153 | /// "C(ommand) En(tity)" 154 | /// Alternative `entity!` for `world: Commands` 155 | /// - `cen![..]`: spawn a `me: Entity` 156 | /// - `cen![&..]`: edit a `me: Entity` 157 | /// - `cen![*..]: edit a `world: EntityWorldMut` 158 | /// - `cen![#Marker|..]: edit all Entities with `Marker` component 159 | #[proc_macro] 160 | pub fn cen(stream:Cokens) -> Cokens { 161 | let stream: Tokens = stream.into(); 162 | let mut iter = stream.peek_iter(); 163 | let dir = en_translate(qt![],&mut iter); 164 | let stream = Tokens::from_iter(iter); 165 | mevy_ecs_syntax::world_spawn_syntax(qt!(#dir #stream)).into() 166 | } 167 | 168 | 169 | // Spawners Helper \| 170 | 171 | fn en_translate(left:Tokens,iter:&mut PeekIter) -> Tokens {match iter.peek_punct(){ 172 | '#' => { 173 | let me = iter.collect_til_punct('|'); 174 | iter.next(); 175 | qt![<#left|#(#me)*>] 176 | } 177 | '*' => { 178 | iter.next(); 179 | let me = if iter.peek_punct() != ':' {qt!()} else { 180 | iter.next(); 181 | let this = iter.next().unwrap(); 182 | qt!{#this} 183 | }; 184 | qt!{<#left*#me>} 185 | } 186 | '&' => { 187 | iter.next(); 188 | let me = if iter.peek_punct() != ':' {qt!()} else { 189 | iter.next(); 190 | let this = iter.next().unwrap(); 191 | qt!{#this} 192 | }; 193 | qt!{<#left|#me>} 194 | } 195 | _ => if left.is_empty() {qt!{}} else {qt!{<#left>}} 196 | }} 197 | 198 | 199 | // EOF \\ 200 | -------------------------------------------------------------------------------- /examples/entity_macro.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(path_statements)] 3 | use bevy::{ecs::{relationship::RelatedSpawnerCommands, world::DeferredWorld}, prelude::*}; 4 | use mevy::*; 5 | pub fn main() {} 6 | 7 | // Commands: Spawning \\ 8 | 9 | fn spawn_with_commands( mut commands: Commands ){ 10 | entity!{ 11 | 12 | Transform!; 13 | [spawned][Transform!] 14 | } 15 | } 16 | 17 | /// same as [spawn_with_commands] 18 | fn spawn_with_named_commands( mut world: Commands ){ 19 | entity!{ 20 | // implies 21 | Transform!; 22 | [spawned][Transform!] 23 | >} 24 | //^ using '>' (pointing outside) at the end will leak the entities, e.g. here: 25 | me; // <- the root entity 26 | spawned; // <- the child named 'spawned' 27 | 28 | let _me = entity!( 29 | Transform! 30 | <); 31 | //^ using '<' (pointing inside) at the end will return the root entity 32 | } 33 | 34 | // Commands: Modify w/ Provided Entity \\ 35 | 36 | fn modify_with_commands( mut commands: Commands, entity: Entity ){ 37 | entity!{ 38 | 39 | Transform!; 40 | [spawned][Transform!] 41 | } 42 | } 43 | 44 | fn modify_with_commands_redir( mut commands: Commands, entity: Entity ){ 45 | entity!{ 46 | 47 | // select first child, if available 48 | // select all children of selected 49 | Visibility::Hidden; // hide all of them 50 | .despawn(); // despawn all of them 51 | } 52 | } 53 | 54 | /// same as [modify_with_commands] 55 | fn modify_with_named_commands( mut world: Commands, entity: Entity ){ 56 | entity!{ 57 | // empty before '|' implies a [Commands] names 'world' 58 | <|entity> 59 | Transform!; 60 | [spawned][Transform!] 61 | } 62 | } 63 | 64 | /// same as [modify_with_commands] 65 | fn modify_with_named_entity( mut commands: Commands, me: Entity ){ 66 | entity!{ 67 | // empty after '|' implies an [Entity] names 'me' 68 | // NOTE: You have to type the '|', otherwise it's like [spawn_with_commands] 69 | 70 | Transform!; 71 | [spawned][Transform!] 72 | } 73 | } 74 | 75 | /// same as [modify_with_commands] 76 | fn modify_with_named_commands_entity( mut world: Commands, me: Entity ){ 77 | entity!{ 78 | // empty before '|' implies a [Commands] names 'world' 79 | // empty after '|' implies an [Entity] names 'me' 80 | <|> 81 | Transform!; 82 | [spawned][Transform!] 83 | } 84 | } 85 | 86 | // Commands: Modify w/ Provided Resource \\ 87 | 88 | #[derive(Resource,Component)] 89 | struct MultiEntities(Vec); 90 | 91 | #[derive(Resource,Component)] 92 | struct AnEntity(Entity); 93 | 94 | impl AnEntity { 95 | fn get(&self) -> Option {Some(self.0)} 96 | } 97 | 98 | fn modify_with_commands_resources( mut cmd: Commands ){ 99 | entity!{ 100 | // using '@' (SAFE MODE) tries to get an entity from a resource: 101 | // - use a path that returns: Option 102 | // - does nothing if resource not available 103 | // - does nothing if return value is 'None' 104 | 105 | Transform!; 106 | [spawned][Transform!] 107 | } 108 | // (SAFE MODE): 'spawned' entities CAN NOT be leaked here 109 | } 110 | 111 | fn modify_with_commands_resources_forced( mut cmd: Commands ){ 112 | entity!{ 113 | // using '@!' (RISKY MODE) to get an entity from a resource: 114 | // - use a path that returns: Entity 115 | // - panics if resource not available 116 | // - panics if return value is 'None' 117 | 118 | Transform!; 119 | [spawned][Transform!] 120 | >} 121 | // (RISKY MODE): 'spawned' entities CAN be leaked here 122 | } 123 | 124 | fn modify_with_commands_resources_iter( mut cmd: Commands ){ 125 | entity!{ 126 | // using '@*' will target entities in a iterator of a resource: 127 | // - use a path that returns an Iterator over [Entity]s 128 | 129 | Transform!; 130 | [spawned][Transform!] 131 | } 132 | } 133 | 134 | // Commands: Modify w/ Provided Component \\ 135 | 136 | fn modify_with_commands_marker( mut cmd: Commands ){ 137 | entity!{ 138 | // using '#' without a trailing path will target EVERY entity With 139 | 140 | Transform!; 141 | [spawned][Transform!] 142 | } 143 | } 144 | 145 | fn modify_with_commands_component( mut cmd: Commands ){ 146 | entity!{ 147 | // using '#' (SAFE MODE) will target EVERY component in the world: 148 | // - use a path that returns: Option 149 | 150 | Transform!; 151 | [spawned][Transform!] 152 | } 153 | } 154 | 155 | fn modify_with_commands_component_forced( mut cmd: Commands ){ 156 | entity!{ 157 | // using '#!' (RISKY MODE) will target THE ONLY component in the world: 158 | // - use a path that returns: Option 159 | // - panics if no entity has this component 160 | // - panics if more than one entity has this component 161 | 162 | Transform!; 163 | [spawned][Transform!] 164 | } 165 | // (RISKY MODE): 'spawned' entities are available here 166 | } 167 | 168 | fn modify_with_commands_multi( mut cmd: Commands ){ 169 | entity!{ 170 | // using '#*' will target entities in a iterator of a component: 171 | // - use a path that returns an Iterator over [Entity]s 172 | 173 | Transform!; 174 | [spawned][Transform!] 175 | } 176 | } 177 | 178 | // Other 'World Mutators' \\ 179 | // Only the first part of <..|..> changes, everything else is as described above 180 | 181 | fn modify_with_entity_commands( mut ecmd: EntityCommands ){ 182 | entity!{ 183 | <*ecmd> 184 | Transform!; 185 | [spawned][Transform!] 186 | } 187 | } 188 | 189 | fn modify_with_child_builder<'a>( mut cbuild: RelatedSpawnerCommands<'a,ChildOf> ){ 190 | entity!{ 191 | <^cbuild> 192 | Transform!; 193 | [spawned][Transform!] 194 | } 195 | } 196 | 197 | fn modify_with_deferred_world( mut dworld: DeferredWorld, me: Entity ){ 198 | entity!{ 199 | <-dworld|> 200 | Transform!; 201 | [spawned][Transform!] 202 | } 203 | } 204 | 205 | fn modify_with_world( mut world: World, me: Entity ){ 206 | entity!{ 207 | <+world|> 208 | Transform!; 209 | [spawned][Transform!] 210 | } 211 | } 212 | 213 | // Other 'World Mutators' Implied Name \\ 214 | 215 | fn modify_with_entity_commands_implied_this( mut this: EntityCommands ){ 216 | entity!{<> 217 | Transform!; 218 | [spawned][Transform!] 219 | } 220 | } 221 | 222 | fn modify_with_entity_commands_implied( mut world: EntityCommands ){ 223 | entity!{<*> 224 | Transform!; 225 | [spawned][Transform!] 226 | } 227 | } 228 | 229 | fn modify_with_child_builder_implied<'a>( mut world: RelatedSpawnerCommands<'a,ChildOf> ){ 230 | entity!{<^> 231 | Transform!; 232 | [spawned][Transform!] 233 | } 234 | } 235 | 236 | fn modify_with_deferred_world_implied( mut world: DeferredWorld, me: Entity ){ 237 | entity!{<-|> 238 | Transform!; 239 | [spawned][Transform!] 240 | } 241 | } 242 | 243 | fn modify_with_world_implied( mut world: World, me: Entity ){ 244 | entity!{<+|> 245 | Transform!; 246 | [spawned][Transform!] 247 | } 248 | } 249 | 250 | fn modify_with_entity_world_implied(mut world: EntityWorldMut ){ 251 | entity!{<+*|> 252 | Transform!; 253 | [spawned][Transform!] 254 | } 255 | } 256 | 257 | // EOF \\ 258 | -------------------------------------------------------------------------------- /crates/ui/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 |

9 | 10 | > [!NOTE] 11 | > This crate is part of [mevy](https://github.com/dekirisu/mevy) (tl;dr: more neat macros) so take a look! 🦆 12 | 13 | ## Setup 14 | Multiple bevy versions are supported and managed by features: 15 | ```toml 16 | # bevy 0.17 17 | mevy_ui = {version="0.3",features=["0.17"]} 18 | 19 | # bevy 0.16 20 | mevy_ui = {version="0.3",features=["0.16"]} 21 | 22 | # bevy 0.15 23 | mevy_ui = {version="0.3",features=["0.15"]} 24 | ``` 25 | 26 | ## ⭐ The Star of the Crate: CSS-like Notation 27 | The macro `ui!()` has multiple modes, that are invoked by (1.) the type of delimiters and (2.) if a name is provided: 28 | 29 | ### 'Slim' Tuple Inline Mode 30 | Short syntax, inspired by TailwindCSS! 31 | ```rust 32 | c.spawn(ui!( 33 | w:100 h:100 radius:6 bg:#fff border:5#f00 34 | shadow:10%+10%+3px+8px#fa4 35 | )?)); 36 | ``` 37 | 38 | ### Tuple Inline Mode 39 | `ui!((..))` (inner round braces) will return a tuple of **mentioned components** only. See [this example](../../examples/ui_bundle.rs). 40 | ```rust 41 | c.spawn(ui!(( 42 | size: 100px 100px; 43 | border: 5px #ff0000; 44 | box_shadow: 10% 10% 3px 8px #ffaa44; 45 | background: #ffffff; 46 | border_radius: 6px; 47 | neat_outline; 48 | )?)); 49 | //^ optional ? (or any token): hovering shows the returned tuple (if LSP used) 50 | 51 | /// function as custom fields or p refabs 52 | fn neat_outline() -> Outline {ui!(( 53 | outline: 3px 1px #00ff00; 54 | ))} 55 | ``` 56 | The order of the components are based on 'first mention' and consistent: 57 | ```rust 58 | let (shadow,node) = ui!(( 59 | box_shadow: 10% 10% 3px 8px #ffaa44; 60 | size: 100px 100px; 61 | )); 62 | ``` 63 | ### Tuple Function Mode 64 | If a name is defined before the inner braces, a function will be defined returning an `impl Bundle` of its content: 65 | ```rust 66 | ui!{neat_box( 67 | size: 100px 100px; 68 | background: #ffffff; 69 | )} 70 | 71 | // is the same as: 72 | pub fn neat_box() -> impl Bundle {ui!(( 73 | size: 100px 100px; 74 | background: #ffffff; 75 | ))} 76 | ``` 77 | 78 | ### Edit Function Mode 79 | Defining a name and then using curly brackets `{}` instead, will define a function that mutates existing components: 80 | - The parameters of this function are `&mut ..` of the components to be mutated 81 | - Custom fields will be `insert`ed, an `EntityCommands` will be then added to the parameters 82 | - Using `_` instead of a calue will keep ste already existing one: 83 | - e.g. `box_shadow: _ _ 10px` will only affect the blur radius, since x and y are 'kept' and spread & color not mentioned 84 | ```rust 85 | ui!{into_red_glow{ 86 | border: _ #ffffff; 87 | background: #ff0000; 88 | // no value mentined, so only color affected: 89 | box_shadow: #ff0000 90 | }} 91 | 92 | // does the same as: 93 | pub fn into_red_glow( 94 | border_color: &mut BorderColor, 95 | background_color: &mut BackgroundColor, 96 | box_shadow: &mut BoxShadow 97 | ){ 98 | border_color.0 = Srgba::hex("#ffffff").unwrap().into(); 99 | background_color.0 = Srgba::hex("#ff0000").unwrap().into(); 100 | box_shadow.color = Srgba::hex("#ff0000").unwrap().into(); 101 | } 102 | ``` 103 | 104 | ### Neat Notation 105 | Inside the macro there you can write those things: 106 | - `0px` = `Val::Px(0.)` 107 | - `0%` = `Val::Percent(0.)` 108 | - `0vh` = `Val::Vh(0.)` 109 | - `0vw` = `Val::Vw(0.)` 110 | - `0vmin` = `Val::VMin(0.)` 111 | - `0vmax` = `Val::VMax(0.)` 112 | - `#ff0000` = `Color::Srgba(Srgba::hex("#ff0000").unwrap())` 113 | - checked by the compiler (and LSP), throwing a custom error if not valid! 114 | - `red`|`RED` = `bevy::color::palettes::css::RED` 115 | 116 | ### Custom Fields 117 | Currently, There are 2 ways to add those: 118 | - a `fn` that returns something that `impl Bundle` 119 | - the `new` method of a struct that `impl Bundle` 120 | ```rust 121 | c.spawn(ui!(( 122 | just_glow: #ff0000; 123 | // targets Outline::new(..) 124 | Outline: 5px, 2px, #ff0000; 125 | ))); 126 | 127 | fn just_glow(color:Color) -> BoxShadow { 128 | BoxShadow { 129 | spread_radius: Val::Px(5.0), 130 | blur_radius: Val::Px(5.0), 131 | color, 132 | ..default() 133 | } 134 | } 135 | ``` 136 | 137 | ### Edge Selection (CSS-like) 138 | ```rust 139 | border: 5px; // 5px to all edges 140 | border: 5px 2px; // 5px vertical, 2px horizontal 141 | border: 5px 2px 8px; // 5px top, 2px horizontal, 8px bottom 142 | border: 5px 2px 4px 1px // (clockwise) top right bottom left 143 | ``` 144 | 145 | ### Corner Selection (CSS-like) 146 | ```rust 147 | border-radius: 5px; // 5px to all corners 148 | border-radius: 5px 0px; // 5px top-left/right, 0px bottom-left/right 149 | border-radius: 5px 2px 8px; // 5px top-left, 2px top-right, 8px bottom 150 | border-radius: 5px 2px 4px 1px // (clockwise) top-left top-right bottom-right bottom-left 151 | ``` 152 | 153 | ### Limitations 154 | At the moment, you can only use variables in custom fields. It's planned to work for built-in fields soon™. 155 | 156 | 157 | ### Built-In Fields 158 | Here's a list of all available out of the box fields, `Val` order same as in bevy 159 | - numbers can be written as int or float 160 | - enum variants can also be written in snake case 161 | ```rust 162 | ui!(( 163 | 164 | // Position Type 165 | position_type: absolute|relative; 166 | absolute; // shortcut 167 | relative; // ^ 168 | 169 | // Visibility 170 | hidden; 171 | visible; 172 | inherit; 173 | 174 | // Transform - be aware how it affects UI! 175 | scale: 1.0 1.2; // x y 176 | scale: 1.0; // x & y 177 | rotation: 1.41; // radian 178 | rotation: 45deg; // degree 179 | 180 | // Positions 181 | l|left: 1px; 182 | r|right: 1px; 183 | t|top: 1px; 184 | b|bottom: 1px; 185 | x: 1px; // left & right 186 | y: 1px; // top & bottom 187 | xy: 1px; // x & y 188 | z|z_index: 10; 189 | zg|z_global: 10; 190 | 191 | // Size 192 | w|width: 1px; 193 | h|height: 1px; 194 | size: 1px 1px; // width height 195 | min_w|min_width: 1px; 196 | min_h|min_height: 1px; 197 | max_w|max_width: 1px; 198 | max_h|max_height: 1px; 199 | aspect_ratio: 1; 200 | 201 | // Margin 202 | m|margin: 1px 1px 1px 1px; // see 'Edge Selection' 203 | mt|margin_top: 1px; 204 | mb|margin_bottom: 1px; 205 | ml|margin_left: 1px; 206 | mr|margin_right: 1px; 207 | mx|margin_x: 1px; // ml & mr 208 | my|margin_y: 1px; // mt & mb 209 | 210 | // Padding 211 | p|padding: 1px 1px 1px 1px; // see 'Edge Selection' 212 | pt|padding_top: 1px; 213 | pb|padding_bottom: 1px; 214 | pl|padding_left: 1px; 215 | pr|padding_right: 1px; 216 | px|padding_x: 1px; // pl & pr 217 | py|padding_y: 1px; // pt & pb 218 | 219 | // Box Styling 220 | bg|background: red; 221 | shadow|box_shadow: 1px 1px 1px 1px #ff0000; 222 | border: 1px 1px 1px 1px #ff0000; // see 'Edge Selection' 223 | border_color: #ff0000; 224 | outline: 1px 1px #ff0000; 225 | round|border_radius: 1px 1%; 226 | 227 | // Text Styling 228 | text|text_size|font_size: 20; 229 | text|justify_text: left|center|right|justified; 230 | text|line_break: word_boundary|any_character|word_or_character|no_wrap; 231 | leading|line_height: 1.2; 232 | color|font_color: #ff0000; 233 | text_shadow: 2 2 #000; // x y color 234 | 235 | // Images 236 | img|image: $an_image; // an_image: Handle 237 | img|image: $(path.to.image); 238 | img|image|image_color: #000; 239 | img|image: flip_x|flip_y; 240 | 241 | // Align Self 242 | justify_self: auto|start|end|center|baseline|stretch; 243 | align_self: ^|flex_start|flex_end; 244 | 245 | // Handle Children 246 | display: flex|grid|block|none; 247 | justify_items: default|start|end|center|baseline|stretch; 248 | align_items: ^|flex_start|flex_end; 249 | justify_content: default|start|end|flex_start|flex_end|center|stretch|space_between|space_evenly|space_around; 250 | align_content: ^; 251 | gap_y|row_gap: 1px; 252 | gap_x|column_gap: 1px; 253 | gap: 1px; // gap_x & gap_y 254 | overflow: visible|clip|hidden|scroll; // set x and y 255 | overflow: clip clip; // or separately 256 | overflow_clip_margin: content_box|padding_box|border_box 5; // number optional 257 | 258 | // Flex 259 | flex|flex_direction: row|column|row_reverse|column_reverse; 260 | flex_basis: 1px; 261 | flex_grow: 1; 262 | flex_shrink: 1; 263 | flex_wrap: no_wrap|wrap|wrap_reverse; 264 | 265 | // Grid 266 | grid_auto_flow: row|column|row_dense|column_dense; 267 | grid_row: span|start|end 10; 268 | grid_row: start_span|start_end|end_span 8 10; 269 | grid_column: span|start|end 10; 270 | grid_column: start_span|start_end|end_span 8 10; 271 | grid_auto_rows: 1px 3% 10fr min_content; // any number of `GridTrack`s 272 | grid_auto_columns: 1px 3% 10fr; // ^ 273 | grid_template_rows: 10:1px 10:3%; // ^, but {repetition}: before each Track 274 | grid_template_columns: 10:1px 10:3%; // ^ 275 | 276 | // Separate Components 277 | interaction; // adds `Interaction` component 278 | cursor_position; // adds `RelativeCursorPosition` component 279 | focus|focus_policy: pass 280 | scroll|scroll_position: 1px 1px; 281 | 282 | )) 283 | ``` 284 | 285 | -------------------------------------------------------------------------------- /crates/ecs/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 |

9 | 10 | These are ONLY proc-macros (no additional traits, structs, fns etc.). 11 | This crate is part of [mevy](https://github.com/dekirisu/mevy) (tl;dr: more neat macros) so take a look! 🦆 12 | 13 | ## Setup 14 | Multiple bevy versions are supported and managed by features: 15 | ```toml 16 | # bevy 0.17 17 | mevy_ecs = {version="0.2",features=["0.17"]} 18 | 19 | # bevy 0.16 20 | mevy_ecs = {version="0.2",features=["0.16"]} 21 | 22 | # bevy 0.15 23 | mevy_ecs = {version="0.2",features=["0.15"]} 24 | ``` 25 | 26 | # Rough Overview 27 | ```rust 28 | entity!{ 29 | <..> // World/Entity Selection 30 | Bundle::new(..); // Insert Bundle to Selected 31 | .observe(..); // Use a method 32 | {..} // Free code block w/ 'this: EntityCommands' 33 | > Pointer{..} // Quick Observe w/ 'this: EntityCommands' 34 | >> Pointer{..} // Quick Observe w/ 'world: World', 'entity: Entity' 35 | [ // Spawn a Child 36 | Bundle::new(..); // Insert Bundle to Child 37 | .observe(..); // Use method on Childs EnitityCommands/EntityWorldMut 38 | {ancestors[0]} // ancestors: Vec 39 | // first = parent, last = 'selected root' 40 | ] 41 | [named_child][ // Spawn a Child with a name 42 | // .. 43 | ] 44 | } 45 | ``` 46 | 47 | # Alternative Syntax 48 | ```rust 49 | entity!{ 50 | <..> 51 | 52 | Bundle!; // = Bundle::default(); 53 | Bundle{a:3,!}; // = Bundle{a:3,..default()}; 54 | Bundle: 3; // = Bundle::new(3); 55 | bundle_fn: 3; // = bundle_fn(3); 56 | macro!: 3, 4; // = macro!{3,4}; 57 | .method: 5; // = .macro(5); 58 | 59 | // any plain Hex-Code = [Color] 60 | BorderColor(#ff0000); 61 | 62 | // css like [Val]s 63 | Node{ 64 | left: 10px; 65 | top: 5%; 66 | width: 3vw; 67 | height: 6vmax; 68 | !}; 69 | 70 | } 71 | ``` 72 | 73 | # World/Entity Selection 74 | The first entry of the macro determines which world access is used and which entity is aimed. 75 | ```rust 76 | entity!{ 77 | 78 | // SPAWN an entity using this [Commands] 79 | // (no entry) assumes a 'world: Commands' 80 | 81 | // MODIFY an entity: pass a Commands | Entity 82 | <|enty> // assumes a 'world: Commands' 83 | // assumes a 'me: Entity' 84 | <|> // assumes both 85 | 86 | <+world> // SPAWN using this [World] 87 | <+world|..> // MODIFY using this [World] 88 | <+> <+|..> // assumes a 'world: World' 89 | 90 | <-world> // SPAWN using this [DeferredWorld] 91 | <-world|..> // MODIFY using this [DeferredWorld] 92 | <-> <-|..> // assumes a 'world: DeferredWorld' 93 | 94 | <*this> // MODIFY using this [EntityCommands] 95 | <*> // assumes a 'world: EntityCommands' 96 | <> // assumes a 'this: EntityCommands' 97 | 98 | <^this> // MODIFY the parent of a [ChildBuilder] 99 | <^> // assumes a 'world: ChildBuilder' 100 | 101 | <+*this> // MODIFY using this [EntityWorldMut] 102 | <+*> // assumes a 'world: EntityWorldMut' 103 | 104 | <|#Comp> // target EVERY entity with this component 105 | <|#Comp.get()> // ..or an [Option] of the component 106 | <|#*Comp.all()> // ..or any iterator over [Entity]s 107 | 108 | <|!#Comp> // target THE ONLY (.single()) entity, enables 'leaking' 109 | <|!#Comp.0> // ..or an [Entity] on the component 110 | 111 | <|@Comp.get()> // target an [Option] on a resource 112 | <|@*Comp.all()> // ..or any iterator over [Entity]s 113 | 114 | <|!@Comp.0> // target an [Entity] on a resource, enables 'leaking' 115 | 116 | } 117 | ``` 118 | 119 | # Redirection 120 | After the initial selection of entities, you can redirect it to entities of a component. 121 | This expects: `.>` or `.!` (mind the '!'). 122 | ```rust 123 | entity!{ 124 | // select: every Entity with [Marker] 125 | // > select: first child, if available 126 | // >> select all children 127 | Visibility::Hidden; // hide all of them 128 | .despawn(); // despawn all of them 129 | } 130 | ``` 131 | 132 | # Leaking / Returning 133 | If the selector can 'leak' entities, you can use one of those symbold a the END of the macro: 134 | - `>` 'leak': every spawned entity is available in this scope 135 | - `<` 'return': returns the root entity 136 | 137 | ```rust 138 | let enty = entity!{ 139 | Bundle::new(5); 140 | <}; 141 | 142 | entity!{ 143 | Bundle::new(5); 144 | [] 145 | [named][] 146 | >} 147 | me; // the spawned entity 148 | e1; // the unnamed child entity 149 | named // the named child entity 150 | 151 | entity!{ 152 | <|!#Comp> 153 | [named][] 154 | >} 155 | named // resource/component selectors only leak children 156 | ``` 157 | 158 | ## Quick Observe 159 | A simpler way to use triggers on self, basically means goated event control: 160 | ```rust 161 | spawn!{ 162 | // Using '>' 163 | > Pointer { 164 | // Provided variables: 165 | // 'this' = EnitityCommands 166 | // 'event' = e.g. &Pointer 167 | this.despawn(); 168 | } 169 | // Using '>>' 170 | >> Pointer { 171 | // Provided variables: 172 | // 'world' = mut World 173 | // 'entity' = Entity 174 | // 'event' = e.g. &Pointer 175 | } 176 | } 177 | ``` 178 | 179 | ```rust 180 | fn startup(mut world: Commands){ 181 | spawn!{Camera2d::default()} 182 | } 183 | ``` 184 | 185 | # Child Names 186 | The 'Child Names' are variables containing the child entity. 187 | - Inside the macro they can be used anywhere, even 'before' you wrote them 188 | - If none is provided - one will be generated: `e{number}`: `e0`, `e1`, ... 189 | ```rust 190 | spawn!{ 191 | Component{ entity: named_child }; 192 | [named_child][ 193 | // components & methods 194 | ] 195 | } 196 | ``` 197 | 198 | This is 'token based', which means it preserves LSP greatness. 199 | ```rust 200 | spawn!{ 201 | // typing . will fire LSPs autocomplete 202 | .obs // would suggest 'observer' 203 | } 204 | ``` 205 | 206 | Easy way to address `ancestors` through a provided array, created within a macro call: 207 | - first entry: the direct parent 208 | - last entry: root entity of the current macro call 209 | ```rust 210 | spawn!{ 211 | [[[[[[ 212 | SpecificEntity(ancestors[3]); 213 | ]]]]]] 214 | } 215 | ``` 216 | 217 | ## Example 218 | Combining the things you could write thing like this: 219 | ```rust 220 | fn startup(mut world: Commands){ 221 | entity!{ 222 | Name: "Root"; 223 | Node{ padding:10px, !}; 224 | BackgroundColor(#ff0000); 225 | Marker; 226 | [nice_text][ 227 | Name: "Some Name"; 228 | Text: "Hello World"; 229 | > Pointer {this.despawn();}; 230 | ] 231 | } 232 | } 233 | ``` 234 | 235 | And modify it by a marker: 236 | ```rust 237 | fn update(mut world:Commands){ 238 | entity!{ 239 | <|#Marker> 240 | BackgroundColor(#00ff00); 241 | } 242 | } 243 | ``` 244 | 245 | ## Synergies with [mevy](https://github.com/dekirisu/mevy) 246 | Using `mevy_ui` macro `ui!{}`, it's a bit like html/css: 247 | ```rust 248 | spawn!{ 249 | ui!(( 250 | size: 5px; 251 | box_shadow: 1px 2px 3px 4px #ff0000; 252 | background: cyan; 253 | )); 254 | [inner_box][ui!(( 255 | size: 80%; 256 | background: green; 257 | ))] 258 | } 259 | ``` 260 | 261 | # Experimental 262 | Macros I use but are very bare-bone and might change a lot quckly. 263 | 264 | ## Alternative Macros 265 | Macros are split into the base accessors: 266 | - `cen![..]`: C(ommand) En(tity) 267 | - `den![..]`: D(eferredWorld) En(tity) 268 | - `wen![..]`: W(orld) En(tity) 269 | 270 | The Entity accessors change to: 271 | - `cen![..]`: spawn a `me: Entity` 272 | - `cen![&..]`: edit a `me: Entity` 273 | - `cen![*..]: edit a `world: EntityCommands` 274 | - `cen![#Marker|..]: edit all Entities with `Marker` component 275 | 276 | ## Get Resource 277 | Get Resource 278 | - required: mutable `world: World|DeferredWorld` 279 | - ref: `let time = gere![Time].unwrap();` 280 | - mut: `let mut time = gere![mut Time].unwrap();` 281 | 282 | ## Edit Resource 283 | Quickly Edit Resource (if available) 284 | - required: mutable `world: World|DeferredWorld` 285 | - usage: `gere![Struct.field = 100];` 286 | 287 | ## Get Component 288 | Get Component 289 | - required: mutable `world: World|DeferredWorld` 290 | - required: `me: Entity` 291 | - ref: `let time = geco![Time].unwrap();` 292 | - mut: `let mut time = geco![mut Time].unwrap();` 293 | - cloned: `let time = geco![Time*].unwrap();` 294 | - has?: `if geco![Time?] {}` 295 | 296 | ## Edit Component 297 | Quickly Edit Components (if available) 298 | - required: mutable `world: World|DeferredWorld` 299 | - usage: `geco![Struct.field = 100];` 300 | 301 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /crates/core/syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | use deki::*; 2 | use std::iter::zip; 3 | use syn::LitFloat; 4 | 5 | 6 | // Neat Ui Block \\ 7 | 8 | /// simple in-code replacement of: 9 | /// - `#hexcode` => bevy color 10 | /// - `0px`, `3%` ... => bevy ui Vals 11 | /// - `[>0px 2px]` css-like notation => bevy UiRect 12 | pub fn code (stream:TokenStream) -> TokenStream { 13 | code_helper(stream,false) 14 | } 15 | 16 | fn code_helper(stream:TokenStream,in_group:bool) -> TokenStream { 17 | let mut list = stream.peek_iter(); 18 | let mut out = qt!{}; 19 | loop{match code_next(&mut list,in_group) { 20 | Step::Shift(stream) => out.extend(stream), 21 | Step::Base(tree) => match tree { 22 | TokenTree::Group(group) => out.extend([ 23 | TokenTree::Group(Group::new(group.delimiter(),code_helper(group.stream(),true))) 24 | ]), 25 | _ => out.extend([tree]), 26 | } 27 | _ => break 28 | }} 29 | out 30 | } 31 | 32 | fn code_next (iter:&mut PeekIter,in_group:bool) -> Step { 33 | exit!{next = iter.next()} 34 | match next.try_val_variant(true) { 35 | Check::Some(stream) => return Step::Shift(qt!{Val::#stream}), 36 | Check::Maybe(num) => if let Some(stream) = iter.seek_val_variant(num) { 37 | let stream = stream.with_span(next.span()); 38 | return Step::Shift(qt!{Val::#stream}); 39 | } _ => {} 40 | } 41 | 42 | if next.is_punct('!') && iter.peek().nay() { 43 | let default = "default".ident_span(next.span()); 44 | return Step::Shift(match in_group { 45 | true => qt!{..Default::#default()}, 46 | false => qt!{::#default()} 47 | }); 48 | } 49 | 50 | if next.is_punct('!') && iter.peek_punct() == ';' { 51 | let default = "default".ident_span(next.span()); 52 | return Step::Shift(qt!{::#default()}); 53 | } 54 | 55 | if next.is_punct('@'){ 56 | let auto = "Auto".ident_span(next.span()); 57 | return Step::Shift(qt!{Val::#auto}); 58 | } 59 | 60 | if let TokenTree::Group(group) = &next { 61 | let mut iter = group.stream().peek_iter(); 62 | if iter.next_if(|a|a.is_punct('>')).yay() { 63 | let [rect,_] = iter.seek_rect_like(); 64 | match rect { 65 | Step::Shift([t,r,b,l]) => { 66 | sandwich!{ 67 | let ^0 = stringify!{^0}.ident_span(l.span()); 68 | #left #right #top #bottom 69 | } 70 | let tok = qt!{UiRect{ 71 | #top: Val::#t, 72 | #right: Val::#r, 73 | #bottom: Val::#b, 74 | #left: Val::#l, 75 | }}; 76 | return Step::Shift(tok); 77 | } _ => {} 78 | } 79 | } 80 | } 81 | 82 | if next.is_punct('#') { 83 | if let Some(out) = iter.next_hex_color() { 84 | return Step::Shift(out.0); 85 | } 86 | } 87 | 88 | Step::Base(next) 89 | } 90 | 91 | 92 | // Peek Iter \\ 93 | 94 | #[ext(pub trait UiPeekIter)] 95 | impl PeekIter { 96 | 97 | fn seek_val_variant(&mut self,num:f32) -> Option { 98 | exit!{if self.peek_punct() != '%'} 99 | self.next(); 100 | Some(qt!{Percent(#num)}) 101 | } 102 | 103 | /// get hex color by next token, custom error or none if no token is available 104 | fn next_hex_color(&mut self) -> Option<(TokenStream,Span)> { 105 | if let Some(out) = self.seek_hex_color() { 106 | Some(out) 107 | } 108 | else { 109 | exit!{hexy = self.next()} 110 | Some((qt!{compile_error!{"Invalid hex string!"}}.with_span(hexy.span()),hexy.span())) 111 | } 112 | } 113 | 114 | fn seek_hex_color(&mut self) -> Option<(TokenStream,Span)> { 115 | exit!{hex = self.peek().try_hex_color()} 116 | let tree = self.next().unwrap(); 117 | let hex = format!("#{hex}"); 118 | let hex = qt!{#hex}.with_span(tree.span()); 119 | let fnc = "hex".ident_span(tree.span()); 120 | Some((qt!{Color::Srgba(Srgba::#fnc(#hex).unwrap())},tree.span())) 121 | } 122 | 123 | /// get a rect by valid upcoming tokens - or a default one 124 | fn seek_rect_like(&mut self) -> [Step<(Option,Literal),[TokenStream;4]>;2] { 125 | let mut rect = vec![]; 126 | let mut last = Step::None; 127 | for _ in 0..4 { match self.next_valvar(){ 128 | Step::Shift(v) => {rect.push(v);}, 129 | Step::Base(b) => { 130 | last = Step::Base(b); 131 | break; 132 | }, 133 | Step::None => hold!{if self.peek().nay()} 134 | }} 135 | match rect.len() { 136 | 0 => [last,Step::None], 137 | _ => [Step::Shift(rect.into_rect_like(false,qt!{},|v|v.with_span(Span::call_site()))),last], 138 | } 139 | } 140 | 141 | /// # Returns 142 | /// - None: not a Val::_: Iterator hasn't progressed 143 | /// - Base: not a Val::_, but a number lit: Iterator has progressed 144 | /// - Shift: a Val::_/ 145 | fn next_valvar(&mut self) -> Step<(Option,Literal),TokenStream> { 146 | let (sign,punct) = match self.peek_punct() { 147 | '-' => (false,self.next().map(|t|t.unwrap_punct())), 148 | '+' => {self.next();(true,None)}, 149 | _ => (true,None) 150 | }; 151 | match self.peek().try_val_variant(sign) { 152 | Check::Maybe(m) => { 153 | let base = self.next(); 154 | match self.seek_val_variant(m){ 155 | None => Step::Base((punct,base.unwrap().unwrap_literal())), 156 | Some(v) => Step::Shift(v.with_span(base.span())) 157 | } 158 | } 159 | Check::Some(v) => {self.next();Step::Shift(v)} 160 | Check::None => if self.peek_punct() == '@' { 161 | let span = self.next().unwrap().span(); 162 | Step::Shift(qt!{Auto}.with_span(span)) 163 | } 164 | else {Step::None} 165 | } 166 | } 167 | } 168 | 169 | 170 | // Token Handling \\ 171 | 172 | fn hex_check(text:&str) -> bool { 173 | text.to_lowercase().chars().filter(|a|match a { 174 | '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'| 175 | '8'|'9'|'a'|'b'|'c'|'d'|'e'|'f' => false, 176 | _ => true 177 | }).next().is_none() 178 | } 179 | 180 | #[ext(pub trait UiTokenTree)] 181 | impl TokenTree { 182 | fn try_val_variant(&self,sign:bool) -> Check { 183 | exit!{*TokenTree::Literal(lit) = self} 184 | lit.try_val_variant(sign) 185 | } 186 | fn try_hex_color(&self) -> Option { 187 | let mut t = self.to_string(); 188 | match t.len() { 189 | 6|8 => {} 190 | 5 => { 191 | let vec = t.chars().collect::>(); 192 | let [r,g,b,a0,a1] = vec.try_into().unwrap(); 193 | t = format!["{r}{r}{g}{g}{b}{b}{a0}{a1}"]; 194 | } 195 | 3|4 => t = zip(t.chars(),t.chars()) 196 | .map(|(a,b)|String::from_iter([a,b])) 197 | .collect(), 198 | _ => exit!{}, 199 | } 200 | hex_check(&t).then_some(t) 201 | } 202 | } 203 | 204 | impl UiTokenTree for Option<&TokenTree> {sandwich!{ 205 | fn ^0 ^1 { 206 | exit!{tree = self} 207 | tree.^0 ^2 208 | } 209 | #try_val_variant^(&self,sign:bool) -> Check^(sign) 210 | #try_hex_color^(&self) -> Option^() 211 | }} 212 | 213 | #[ext(pub trait UiLiteral)] 214 | impl Literal { 215 | fn try_val_variant(&self,sign:bool) -> Check { 216 | exit!{if !self.is_numeric()} 217 | let lit: LitFloat = self.clone().into(); 218 | kill!{num = lit.base10_parse::()} 219 | let num = if sign {num} else {-num}; 220 | let val = match lit.suffix() { 221 | "px" => "Px", 222 | "vw" => "Vw", 223 | "vh" => "Vh", 224 | "vmin" => "VMin", 225 | "vmax" => "VMax", 226 | "" => return Check::Maybe(num), 227 | _ => return Check::None 228 | }; 229 | let val = val.ident(); 230 | Check::Some(qt!{#val(#num)}.with_span(lit.span())) 231 | } 232 | } 233 | 234 | #[ext(pub trait UiVec)] 235 | impl Vec { 236 | 237 | /// orders, removes & clones entries to fit a css-like rect 238 | /// - corner_align: use corner order logic (e.g. like css border-radius) 239 | /// - output: [top, right, bottom, left] 240 | fn into_rect_like( 241 | mut self, 242 | corner_align: bool, 243 | default: T, 244 | cloned_edit: fn(T)->T 245 | ) -> [T;4] { 246 | match self.len() { 247 | 0 => [default.clone(),default.clone(),default.clone(),default], 248 | 1 => { 249 | let a = self.pop().unwrap(); 250 | let a0 = cloned_edit(a.clone()); 251 | [a0.clone(),a0.clone(),a0,a] 252 | } 253 | 2 => unsafe { 254 | let [v,h] = self.try_into().unwrap_unchecked(); 255 | let v0 = cloned_edit(v.clone()); 256 | let h0 = cloned_edit(h.clone()); 257 | if corner_align {[v0,v,h0,h]} else {[v0,h0,v,h]} 258 | } 259 | 3 => unsafe { 260 | let [t,h,b] = self.try_into().unwrap_unchecked(); 261 | if corner_align { 262 | let b0 = cloned_edit(b.clone()); 263 | [t,h,b0,b] 264 | } 265 | else { 266 | let h0 = cloned_edit(h.clone()); 267 | [t,h0,b,h] 268 | } 269 | } 270 | _ => unsafe { 271 | self.set_len(4); 272 | self.try_into().unwrap_unchecked() 273 | } 274 | } 275 | } 276 | } 277 | 278 | 279 | // EOF \\ 280 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 Dekirisu 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /crates/ecs/syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | use deki::*; 2 | 3 | // \\ 4 | 5 | pub enum WorldEntry { 6 | EntityCommands{ 7 | entry: TokenStream, 8 | }, 9 | Commands{ 10 | entry: TokenStream, 11 | entity: Option, 12 | }, 13 | DeferredWorld{ 14 | entry: TokenStream, 15 | entity: Option, 16 | }, 17 | World{ 18 | entry: TokenStream, 19 | entity: Option, 20 | }, 21 | EntityWorldMut{ 22 | entry: TokenStream, 23 | }, 24 | ChildBuilder{ 25 | entry: TokenStream, 26 | }, 27 | } 28 | 29 | impl WorldEntry { 30 | 31 | /// (.., Component-Redirect?) 32 | pub fn from_tokens(streams:&Option>) -> Self { 33 | if streams.is_none(){ 34 | return Self::Commands{entry:qt!(world),entity:None}; 35 | } 36 | let streams = streams.as_ref().unwrap(); 37 | let entity = streams.get(1).map(|a|if a.is_empty(){qt!{me}} else {a.clone()}); 38 | let out = match streams.get(0) { 39 | None => Self::EntityCommands{entry:qt!{this}}, 40 | Some(a) if a.is_empty() => Self::Commands{entry:qt!{world},entity}, 41 | Some(a) => { 42 | let mut a_iter = a.clone().peek_iter(); 43 | let first = a_iter.peek().cloned().unwrap(); 44 | match first { 45 | TokenTree::Punct(pnc) => { 46 | a_iter.next(); 47 | let mut entry = TokenStream::from_iter(a_iter); 48 | if entry.is_empty() {entry = qt!{world};}; 49 | match pnc.as_char() { 50 | '*' => Self::EntityCommands{entry}, 51 | '-' => Self::DeferredWorld{entry,entity}, 52 | '+' => { 53 | let mut a_iter = entry.peek_iter(); 54 | match a_iter.peek_punct() { 55 | '*' => { 56 | a_iter.next(); 57 | let mut entry = TokenStream::from_iter(a_iter); 58 | if entry.is_empty() {entry = qt!{world};}; 59 | Self::EntityWorldMut{entry} 60 | } 61 | _ => Self::World{entry:TokenStream::from_iter(a_iter),entity}, 62 | } 63 | } 64 | '^' => Self::ChildBuilder{entry}, 65 | _ => Self::Commands{entry,entity} 66 | } 67 | } 68 | _ => Self::Commands{entry:a.clone(),entity} 69 | } 70 | } 71 | }; 72 | out 73 | } 74 | 75 | pub fn get_entity(&self) -> Option {match self { 76 | Self::EntityCommands{entry} 77 | | Self::EntityWorldMut{entry} 78 | => Some(qt!{#entry.id()}), 79 | Self::Commands{entry:_,entity} 80 | | Self::DeferredWorld {entry:_,entity} 81 | | Self::World {entry:_,entity} 82 | => entity.clone(), 83 | Self::ChildBuilder{entry} => Some({ 84 | #[cfg(any(feature="0.16",feature="0.17"))] 85 | qt!{#entry.target_entity()} 86 | #[cfg(feature="0.15")] 87 | qt!{#entry.parent_entity()} 88 | #[cfg(not(feature="0.17"))] 89 | #[cfg(not(feature="0.16"))] 90 | #[cfg(not(feature="0.15"))] 91 | compile_error_no_version() 92 | }), 93 | }} 94 | 95 | pub fn has_entity(&self) -> bool {self.get_entity().is_some()} 96 | 97 | pub fn init_entity(&self) -> TokenStream { 98 | self.get_entity().map(|e|qt!{let me = #e;}) 99 | .unwrap_or_else(||{ 100 | let entry = self.get_entry(); 101 | qt!(let me = #entry.spawn_empty().id();) 102 | }) 103 | } 104 | 105 | pub fn get_entry(&self) -> TokenStream {match self { 106 | Self::EntityCommands { entry } 107 | | Self::DeferredWorld { entry, entity:_ } 108 | => qt!{#entry.commands()}, 109 | Self::Commands { entry, entity:_ } 110 | | Self::World { entry, entity:_ } 111 | => qt!{#entry}, 112 | Self::EntityWorldMut { entry } 113 | => qt!{unsafe{#entry.world_mut()}}, 114 | Self::ChildBuilder{entry} => { 115 | #[cfg(any(feature="0.16",feature="0.17"))] 116 | qt!{#entry.commands_mut()} 117 | #[cfg(feature="0.15")] 118 | qt!{#entry} 119 | #[cfg(not(feature="0.17"))] 120 | #[cfg(not(feature="0.16"))] 121 | #[cfg(not(feature="0.15"))] 122 | compile_error_no_version() 123 | } 124 | }} 125 | 126 | pub fn init_entry(&self) -> TokenStream{ match self { 127 | Self::EntityCommands { entry } 128 | | Self::DeferredWorld { entry, entity:_ } 129 | => qt!{let mut world = #entry.commands();}, 130 | Self::EntityWorldMut { entry } 131 | => qt!{let world = unsafe{#entry.world_mut()};}, 132 | Self::ChildBuilder { entry } => { 133 | #[cfg(any(feature="0.16",feature="0.17"))] 134 | qt!{let mut world = #entry.commands_mut();} 135 | #[cfg(feature="0.15")] 136 | qt!{} 137 | #[cfg(not(feature="0.17"))] 138 | #[cfg(not(feature="0.16"))] 139 | #[cfg(not(feature="0.15"))] 140 | compile_error_no_version() 141 | }, 142 | _ => qt!{} 143 | }} 144 | 145 | pub fn use_entry(&self) -> TokenStream{ match self { 146 | Self::EntityCommands { entry:_ } 147 | | Self::DeferredWorld { entry:_, entity:_ } 148 | | Self::EntityWorldMut { entry:_ } 149 | => qt!{world}, 150 | Self::ChildBuilder { entry:_ } => { 151 | #[cfg(any(feature="0.16",feature="0.17"))] 152 | qt!{world} 153 | #[cfg(feature="0.15")] 154 | {self.get_entry()} 155 | #[cfg(not(feature="0.17"))] 156 | #[cfg(not(feature="0.16"))] 157 | #[cfg(not(feature="0.15"))] 158 | compile_error_no_version() 159 | }, 160 | _ => self.get_entry() 161 | }} 162 | 163 | pub fn init(&self) -> TokenStream { 164 | let mut out = self.init_entity(); 165 | out.extend(self.init_entry()); 166 | out 167 | } 168 | 169 | pub fn world_wrap(&self,inner:TokenStream) -> TokenStream {match self { 170 | Self::Commands{entry:_,entity:_} 171 | | Self::EntityCommands{entry:_} 172 | | Self::DeferredWorld {entry:_,entity:_} => { 173 | let world = self.use_entry(); 174 | qt!{#world.queue(move|world:&mut World|{#inner});} 175 | }, 176 | Self::World{entry:_,entity:_} | Self::EntityWorldMut{entry:_} => qt!{ 177 | #inner 178 | }, 179 | Self::ChildBuilder{entry:_} => { 180 | #[allow(unused_variables)] 181 | let world = self.use_entry(); 182 | #[cfg(any(feature="0.16",feature="0.17"))] 183 | qt!{#world.queue(move|world:&mut World|{#inner});} 184 | #[cfg(feature="0.15")] 185 | qt!{#world.enqueue_command(move|world:&mut World|{#inner});} 186 | #[cfg(not(feature="0.17"))] 187 | #[cfg(not(feature="0.16"))] 188 | #[cfg(not(feature="0.15"))] 189 | compile_error_no_version() 190 | }, 191 | }} 192 | 193 | } 194 | 195 | /// (.., Component-Redirect?) 196 | pub fn check_angled(iter:&mut PeekIter) -> Option> { 197 | exit!{if iter.peek_punct() != '<'} 198 | iter.next(); 199 | let mut streams = vec![qt!{}]; 200 | for token in iter { 201 | hold!{if token.is_punct('>')} 202 | if token.is_punct('|') { 203 | streams.push(qt!{}); 204 | continue 205 | } 206 | streams.last_mut().unwrap().extend([token]); 207 | } 208 | exit!{>if (streams.len()==1 && streams[0].is_empty()) Some(vec![])} 209 | Some(streams) 210 | } 211 | 212 | //\\ 213 | 214 | fn peek_split_punct_once(mut iter:PeekIter,punct:char) -> [TokenStream;2] { 215 | let mut out = [qt!{},qt!{}]; 216 | let mut i = 0; 217 | while let Some(token) = iter.next() { 218 | if token.is_punct(punct){ 219 | match iter.peek_punct() != punct { 220 | true => if i == 0 { 221 | i += 1; 222 | continue 223 | }, 224 | false => if let Some(t) = iter.next() { 225 | out[i.min(1)].extend(match punct { 226 | ':' => qt!{::}, 227 | _ => qt!(#t #t) 228 | }); 229 | continue 230 | } 231 | } 232 | } 233 | out[i.min(1)].extend(token.into_token_stream()); 234 | } 235 | out 236 | } 237 | 238 | /// bool: is forced? 239 | fn world_entity_init(vec:Option<&TokenStream>) -> Option<(TokenStream,bool)> { 240 | exit!{vec = vec} 241 | let mut iter = vec.clone().peek_iter(); 242 | exit!{first = iter.next()} 243 | exit!{*TokenTree::Punct(punct) = first} 244 | match punct.as_char() { 245 | 246 | '@' => { 247 | let punct = iter.peek_punct(); 248 | if punct == '!' || punct == '*' {iter.next();} 249 | let [typi,path] = peek_split_punct_once(iter,'.'); 250 | match punct { 251 | 252 | '!' => Some((qt!{let me = world.resource::<#typi>().#path;},true)), 253 | 254 | '*' => Some((qt!{ 255 | let Some(data) = world.get_resource::<#typi>() else {return}; 256 | for me in data .#path.collect::>() 257 | },false)), 258 | 259 | 'n' => Some((qt!{ 260 | let Some(data) = world.get_resource::<#typi>() else {return}; 261 | #[allow(for_loops_over_fallibles)] 262 | for me in data. #path 263 | },false)), 264 | 265 | _ => None 266 | } 267 | } 268 | 269 | '#' => { 270 | let punct = iter.peek_punct(); 271 | if punct == '!' || punct == '*' {iter.next();} 272 | let [typi,path] = peek_split_punct_once(iter,'.'); 273 | match punct { 274 | 275 | '!' => Some((match path.is_empty(){ 276 | true => qt!{ 277 | let mut query = world.query_filtered::>(); 278 | let me = query.single(world).unwrap(); 279 | }, 280 | false => qt!{ 281 | let mut query = world.query::<&#typi>(); 282 | let me = query.single(world).unwrap().#path; 283 | } 284 | },true)), 285 | 286 | '*' => Some((qt!{ 287 | let mut query = world.query::<&#typi>(); 288 | for me in query.iter(world) 289 | .map(|data|data .#path.collect::>()) 290 | .flatten().collect::>() 291 | },false)), 292 | 293 | 'n' => Some((match path.is_empty(){ 294 | true => qt!{ 295 | let mut query = world.query_filtered::>(); 296 | for me in query.iter(world).collect::>() 297 | }, 298 | false => qt!{ 299 | let mut query = world.query::<&#typi>(); 300 | for me in query.iter(world).filter_map(|data|data .#path).collect::>() 301 | } 302 | },false)), 303 | 304 | _ => None 305 | } 306 | } 307 | 308 | _ => None 309 | } 310 | } 311 | 312 | /// (_,is_forced?) 313 | fn query_to_redirect(query:TokenStream) -> TokenStream { 314 | let mut vec = query.into_iter().collect::>(); 315 | let mut post = qt!{.collect::>()}; 316 | 317 | let is_enty = vec.last().is_some_and(|a|a.is_punct('!')); 318 | let is_opti = vec.last().is_some_and(|a|a.is_punct('?')); 319 | 320 | if is_enty || is_opti { 321 | vec.pop(); 322 | post = qt!{}; 323 | } 324 | 325 | let iter = TokenStream::from_iter(vec).peek_iter(); 326 | let [typi,path] = peek_split_punct_once(iter,'.'); 327 | if is_enty {qt!{ 328 | let Some(data) = world.get::<#typi>(me) else {continue}; 329 | let me = data. #path; 330 | }} 331 | else if is_opti {qt!{ 332 | let Some(data) = world.get::<#typi>(me) else {continue}; 333 | let Some(me) = data. #path else {continue}; 334 | }} 335 | else {qt!{ 336 | let Some(data) = world.get::<#typi>(me) else {continue}; 337 | #[allow(for_loops_over_fallibles)] 338 | for me in data. #path #post 339 | }} 340 | } 341 | 342 | // World Spawning \\ 343 | 344 | pub fn world_spawn_syntax(stream:TokenStream) -> TokenStream { 345 | let mut idx = 0; 346 | let mut spawn = qt!(); 347 | let mut parenting = qt!(); 348 | let mut mutato = vec![]; 349 | 350 | let mut iter = stream.peek_iter(); 351 | let angled = check_angled(&mut iter); 352 | 353 | let mut chain = vec![]; 354 | while let Some(mut query) = check_angled(&mut iter) { 355 | chain.push(query_to_redirect(query.swap_remove(0))); 356 | } 357 | chain.reverse(); 358 | 359 | // NOTE: Mind the clone, try to dodge? 360 | let mut vec = iter.collect::>(); 361 | let [leak,retu,ccmd] = vec.last().map(|t| 362 | [t.is_punct('>'),t.is_punct('<'),t.is_punct('@')] 363 | ).unwrap_or_default(); 364 | if leak || retu || ccmd {vec.pop();} 365 | 366 | let stream = TokenStream::from_iter(vec); 367 | 368 | let mut is_forced = true; 369 | let mut on_current = qt!{}; 370 | let mut on_world = qt!{}; 371 | let mut on_world_block = qt!{}; 372 | 373 | let wentry = WorldEntry::from_tokens(&angled); 374 | let mut exec_on_world = match wentry { 375 | WorldEntry::World{entry:_,entity:_} 376 | | WorldEntry::ChildBuilder{entry:_} 377 | | WorldEntry::EntityWorldMut { entry:_ } 378 | => true, 379 | _ => false 380 | }; 381 | 382 | if let Some((winit,forced)) = world_entity_init(angled.as_ref().map(|a|a.get(1)).flatten()) { 383 | on_world.extend(winit); 384 | is_forced = forced; 385 | exec_on_world = true; 386 | } else { 387 | on_current.extend(wentry.init_entity()); 388 | on_world.extend(qt!{ 389 | #[allow(for_loops_over_fallibles)] 390 | for _ in Some(()) 391 | }); 392 | } 393 | 394 | if !chain.is_empty() { 395 | is_forced = false; 396 | exec_on_world = true; 397 | } 398 | 399 | let spawn_on_world = !is_forced && exec_on_world; 400 | on_current.extend(wentry.init_entry()); 401 | 402 | world_spawn_syntax_recursive( 403 | stream,Span::call_site(),Some("me".ident()),vec![], 404 | &mut idx,&mut spawn,&mut parenting,&mut mutato, 405 | if spawn_on_world {qt!{world}} else {wentry.use_entry()}, 406 | if exec_on_world {qt!{world}} else {wentry.use_entry()}, 407 | if exec_on_world {qt!{entity_mut}} else {qt!{entity}} 408 | ); 409 | 410 | if spawn_on_world {on_world_block.extend(spawn)} 411 | else {on_current.extend(spawn)} 412 | 413 | if chain.is_empty() { 414 | on_world_block.extend(qt!(#parenting #(#mutato)*)); 415 | } else { 416 | let mut chain = chain.into_iter(); 417 | let header = chain.next().unwrap(); 418 | let mut block = qt!{#header{ #parenting #(#mutato)* }}; 419 | for header in chain { 420 | block = qt!{#header{#block}}; 421 | } 422 | on_world_block.extend(block); 423 | } 424 | 425 | if exec_on_world{ 426 | on_world.extend(qt!{{#on_world_block}}); 427 | let wrap = wentry.world_wrap(on_world); 428 | on_current.extend(wrap); 429 | } else { 430 | on_current.extend(on_world_block); 431 | } 432 | 433 | match [leak,retu,ccmd] { 434 | [true, _, _] => on_current, 435 | [_, true, _] => qt!{{#on_current me}}, 436 | [_, _, true] => qt!{|mut world:Commands|{#on_current}}, 437 | [ _ , _ , _] => qt!{{#on_current}} 438 | } 439 | 440 | } 441 | 442 | // Recursion \\ 443 | 444 | fn world_spawn_syntax_recursive( 445 | stream: TokenStream, 446 | span: Span, 447 | custom_name: Option, 448 | mut ancestors: Vec, 449 | idx: &mut usize, 450 | spawn: &mut TokenStream, 451 | parenting: &mut TokenStream, 452 | mutato: &mut Vec, 453 | spawn_token: TokenStream, 454 | world_token: TokenStream, 455 | entity_mut: TokenStream 456 | ){ 457 | let iter = stream.peek_iter(); 458 | let split = iter.split_punct(';').into_iter(); 459 | 460 | // prepare ancestors 461 | let ancestors_tokens = if ancestors.is_empty(){ 462 | qt!{} 463 | } else { 464 | let ancestors_rev = ancestors.iter().rev(); 465 | qt!{let ancestors = [#(#ancestors_rev),*];} 466 | }; 467 | 468 | // handle naming & hierarchy 469 | let name = custom_name.unwrap_or(format!("e{idx}").ident_span(span)); 470 | let name_tmp = name.to_string().ident(); 471 | 472 | if let Some(parent) = ancestors.last() { 473 | #[cfg(any(feature="0.16",feature="0.17"))] 474 | parenting.extend(qt!{ 475 | #world_token.#entity_mut(#name_tmp).insert(ChildOf(#parent)); 476 | }); 477 | #[cfg(feature="0.15")] 478 | parenting.extend(qt!{ 479 | #world_token.#entity_mut(#name_tmp).set_parent(#parent); 480 | }); 481 | #[cfg(not(feature="0.17"))] 482 | #[cfg(not(feature="0.16"))] 483 | #[cfg(not(feature="0.15"))] 484 | parenting.extend(compile_error_no_version()); 485 | 486 | spawn.extend(qt!( 487 | let mut #name = #spawn_token.spawn_empty().id(); 488 | )) 489 | } 490 | 491 | *idx += 1; 492 | ancestors.push(name_tmp); 493 | 494 | // 495 | let mut commands = qt!(); 496 | let mut group_name = None; 497 | for row in split { 498 | let mut iter = row.peek_iter(); 499 | next!{first = iter.peek()} 500 | use mevy_core_syntax::code as mecode; 501 | match first { 502 | 503 | // TokenTree::Ident(ident) if ident.to_string().as_str() == "if" => { 504 | // commands.extend(qt!(#(#iter)*)); 505 | // } 506 | // 507 | // TokenTree::Ident(ident) if ident.to_string().as_str() == "match" => { 508 | // commands.extend(qt!(#(#iter)*)); 509 | // } 510 | 511 | TokenTree::Ident(ident) if ident.to_string().as_str() == "try" => { 512 | iter.next(); 513 | commands.extend(qt!( 514 | if let Some(bundle) = #(#iter)* { 515 | this.insert(bundle); 516 | } 517 | )); 518 | } 519 | 520 | TokenTree::Ident(ident) => { 521 | next!{first = ident.to_string().chars().next()} 522 | let [func,mut attr] = peek_split_punct_once(iter,':'); 523 | 524 | let tokens = if first.is_uppercase() { 525 | mecode(if attr.is_empty() {func} else {qt!{<#func>::new(#attr)}}) 526 | } else if !attr.is_empty() { 527 | let is_macro = func.clone().into_iter().last().map(|p|p.is_punct('!')).unwrap_or_default(); 528 | if !is_macro {attr = mecode(attr)} 529 | qt!{#func(#attr)} 530 | } else { 531 | let mut iter = func.peek_iter(); 532 | let ident = iter.next().unwrap(); 533 | let is_macro = first.is_lowercase() && iter.peek_punct() == '!'; 534 | let mut tokens = TokenStream::from_iter(iter); 535 | if !is_macro { tokens = mecode(tokens); } 536 | qt!{#ident #tokens} 537 | }; 538 | 539 | commands.extend(qt!(this.insert(#tokens);)); 540 | } 541 | 542 | TokenTree::Group(g) if g.delimiter().is_parenthesis() => { 543 | let mut tokens = TokenStream::from_iter(iter); 544 | tokens = mecode(tokens); 545 | commands.extend(qt!(this.insert(#tokens);)); 546 | } 547 | 548 | TokenTree::Group(g) if g.delimiter().is_brace() => for group in iter { 549 | next!{*TokenTree::Group(group) = group} 550 | commands.extend(group.into_token_stream()); 551 | } 552 | 553 | TokenTree::Group(g) if g.delimiter().is_bracket() => for group in iter { 554 | next!{*TokenTree::Group(group) = group} 555 | let mut check = group.stream().into_iter(); 556 | if let (Some(TokenTree::Ident(n)),None) = (check.next(),check.next()) { 557 | group_name = Some(n); 558 | continue 559 | } 560 | 561 | if !commands.is_empty() { 562 | let world_token = world_token.clone(); 563 | mutato.push(qt!( 564 | #ancestors_tokens 565 | let mut this = #world_token.#entity_mut(#name); 566 | #commands 567 | )); 568 | commands = qt!(); 569 | } 570 | 571 | world_spawn_syntax_recursive( 572 | group.stream(), group.span_open(), group_name.take(), 573 | ancestors.clone(), idx, spawn, parenting, mutato, 574 | spawn_token.clone(), world_token.clone(), entity_mut.clone() 575 | ); 576 | } 577 | 578 | TokenTree::Punct(p) if p.as_char() == '@' => { 579 | iter.next(); 580 | let mut strm = iter.collect::>(); 581 | let attr = match strm.last() { 582 | Some(TokenTree::Group(_)) => { 583 | let b = strm.pop().unwrap().unwrap_group().stream(); 584 | qt![,#b] 585 | } 586 | _ => qt![] 587 | }; 588 | commands.extend(qt!(#(#strm)*(&mut this #attr);)); 589 | } 590 | 591 | TokenTree::Punct(p) if p.as_char() == '.' => { 592 | let [func,attr] = peek_split_punct_once(iter,':'); 593 | let tokens = if !attr.is_empty() {qt!{#func(#attr)}} else {func}; 594 | let tokens = mevy_core_syntax::code(tokens); 595 | commands.extend(qt!(this #tokens;)); 596 | } 597 | 598 | TokenTree::Punct(p) if p.as_char() == '>' => { 599 | let span_entity = p.span(); 600 | iter.next(); 601 | let span_world = iter.next_if(|p|p.is_punct('>')).map(|p|p.span()); 602 | let mut event = iter.collect::>(); 603 | next!{action = event.pop()} 604 | match action { 605 | TokenTree::Group(group) if group.delimiter().is_brace() => { 606 | let trigger = "trigger".ident_span(span_entity); 607 | let let_event = "event".ident_span(span_entity); 608 | 609 | #[cfg(feature="0.17")] 610 | let trigger_entity = qt!{trigger.event_target()}; 611 | #[cfg(feature="0.16")] 612 | let trigger_entity = qt!{trigger.target()}; 613 | #[cfg(feature="0.15")] 614 | let trigger_entity = qt!{trigger.entity()}; 615 | #[cfg(not(feature="0.17"))] 616 | #[cfg(not(feature="0.15"))] 617 | #[cfg(not(feature="0.16"))] 618 | let trigger_entity = compile_error_no_version(); 619 | 620 | #[cfg(feature="0.17")] 621 | let triname = qt!{On}; 622 | #[cfg(not(feature="0.17"))] 623 | let triname = qt!{Trigger}; 624 | 625 | commands.extend(match span_world { 626 | None => { 627 | let this = "this".ident_span(span_entity); 628 | qt!(this.observe(move|#trigger:#triname<#(#event)*>,mut world: Commands|{ 629 | #[allow(unused_variables)] 630 | let mut #this = world.entity(#trigger_entity); 631 | #[allow(unused_variables)] 632 | let #let_event = trigger.event(); 633 | #group 634 | });) 635 | }, 636 | Some(span_world) => { 637 | let entity = "entity".ident_span(span_entity); 638 | let world = "world".ident_span(span_world); 639 | qt!(this.observe(move|#trigger:#triname<#(#event)*>,mut world: Commands|{ 640 | #[allow(unused_variables)] 641 | let #entity = #trigger_entity; 642 | #[allow(unused_variables)] 643 | let #let_event = trigger.event().clone(); 644 | world.queue(move|#world:&mut World|#group); 645 | });) 646 | } 647 | }); 648 | } 649 | _ => {} 650 | } 651 | } 652 | 653 | _ => {} 654 | } 655 | } 656 | 657 | if !commands.is_empty() { 658 | let entity_mut = entity_mut.clone(); 659 | let world_token = world_token.clone(); 660 | mutato.push(qt!( 661 | #ancestors_tokens 662 | let mut this = #world_token.#entity_mut(#name); 663 | #commands 664 | )); 665 | } 666 | } 667 | 668 | 669 | fn compile_error_no_version() -> TokenStream { 670 | qt!{compile_error!{"Mevy: Missing bevy version!: Specify it in Cargo.toml! e.g. feature=[\"0.15\"])"}} 671 | } 672 | 673 | // EOF \\ 674 | -------------------------------------------------------------------------------- /crates/ui/syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | use deki::syn::LitInt; 2 | pub use deki::*; 3 | pub use mevy_core_syntax::*; 4 | use std::{f32::consts::PI, iter::zip}; 5 | use syn::LitFloat; 6 | 7 | // CSS -> Bundle \\ 8 | 9 | pub fn bundle(iter:PeekIter,after:Option) -> TokenStream { 10 | UiPrep::from_iter(iter,false,|a,_|a.is_punct(';')).get_bundle(after) 11 | } 12 | 13 | pub fn bundle_slim(iter:PeekIter,after:Option) -> TokenStream { 14 | UiPrep::from_iter(iter,true,|a,b|{ 15 | let a_punct = a.is_punct(':') || a.is_punct('#') || a.is_punct('$'); 16 | !a_punct && b.map(|b|b.is_any_ident()).unwrap_or_default() 17 | }).get_bundle(after) 18 | } 19 | 20 | #[ext(pub trait TreeIterExt2)] 21 | impl PeekIter { 22 | 23 | /// splits Tokens into multiple [TokenStream]s by a char delimiter. 24 | /// - doesn't include empty parts. 25 | fn split_by_filter(mut self,push_first:bool,func:fn(&TokenTree,Option<&TokenTree>) -> bool) -> Vec { 26 | let mut out = vec![]; 27 | let mut curr = vec![]; 28 | while let Some(tree) = self.next() { 29 | if push_first {curr.push(tree.clone());} 30 | if func(&tree,self.peek()) { 31 | if !curr.is_empty(){ 32 | out.push(TokenStream::from_iter(std::mem::take(&mut curr))); 33 | } 34 | continue 35 | } 36 | if !push_first {curr.push(tree);} 37 | } 38 | if !curr.is_empty(){ 39 | out.push(TokenStream::from_iter(curr)); 40 | } 41 | out 42 | } 43 | } 44 | 45 | 46 | 47 | #[derive(Default)] 48 | pub struct UiPrep { 49 | /// (variable,is_builtin) 50 | pub variables: StackMap, 51 | pub defaults: StackMap, 52 | pub assign: TokenStream 53 | } 54 | 55 | impl UiPrep { 56 | pub fn from_stream(stream:TokenStream,push_first:bool,func:fn(&TokenTree,Option<&TokenTree>)->bool) -> Self { 57 | Self::from_iter(stream.peek_iter(),push_first,func) 58 | } 59 | pub fn from_iter(mut iter:PeekIter,push_first:bool,func:fn(&TokenTree,Option<&TokenTree>)->bool) -> Self { 60 | let mut out = Self::default(); 61 | 62 | if let Some(TokenTree::Literal(_)) = iter.peek() { 63 | kill!{a = iter.next()} 64 | let var = "txt".ident(); 65 | out.assign.extend(qt!{let #var = Text::new(#a);}); 66 | *out.variables.entry(var.to_string()) = true; 67 | } 68 | 69 | for stream in iter.split_by_filter(push_first,func) { 70 | let mut iter = stream.peek_iter(); 71 | let field = iter.next().unwrap().unwrap_ident(); 72 | let yuim = ui_style_sheet(field.clone().into(),&mut iter); 73 | 74 | if yuim.is_empty() { 75 | let varnm = field.to_string().chars() 76 | .map(|c|if c.is_alphanumeric() {c} else {'_'}) 77 | .collect::().to_case(Case::Snake).ident(); 78 | let is_class = field.to_string().chars().next().unwrap().is_uppercase(); 79 | let attr = code(TokenStream::from_iter(iter)); 80 | let func = match is_class { 81 | true => qt!{#field::new}, 82 | false => qt!{#field} 83 | }; 84 | out.assign.extend(qt!{let #varnm = #func(#attr);}); 85 | *out.variables.entry(varnm.to_string()) = false; 86 | continue 87 | } 88 | 89 | for (key,yuis) in yuim.into_iter() { 90 | let var = key.to_case(Case::Snake).ident(); 91 | out.defaults.entry(key.to_string()); 92 | *out.variables.entry(key.to_case(Case::Snake)) = true; 93 | for UiEntry { typ:_, fields, value, extra:_ } in yuis { 94 | out.assign.extend(qt!{#var #fields = #value;}); 95 | } 96 | next!{if key == "Node"} 97 | let typ = key.ident_span(field.span()); 98 | out.assign.extend(qt!({type O = #typ;})); 99 | } 100 | } 101 | out 102 | } 103 | 104 | pub fn get_bundle(&self,after:Option) -> TokenStream { 105 | let after = after.map(|a|a.span()).unwrap_or(Span::call_site()); 106 | 107 | let defaults = TokenStream::from_iter(self.defaults.keys.iter().map(|s|{ 108 | let (var,typ) = (s.to_case(Case::Snake).ident(),s.ident()); 109 | match s.as_str() { 110 | "BoxShadow" => { 111 | #[cfg(any(feature="0.16",feature="0.17"))] 112 | qt!{let mut #var = BoxShadow(vec![ShadowStyle::default()]);} 113 | #[cfg(feature="0.15")] 114 | qt!{let mut #var = #typ::default();} 115 | #[cfg(not(feature="0.15"))] 116 | #[cfg(not(feature="0.16"))] 117 | #[cfg(not(feature="0.17"))] 118 | compile_error_no_version() 119 | } 120 | _ => qt!{let mut #var = #typ::default();} 121 | } 122 | })); 123 | let bundle = self.variables.keys.iter().map(|s|s.ident()); 124 | let out = "bundle".ident_span(after); 125 | let assign = self.assign.clone(); 126 | 127 | qt!{{ 128 | #defaults 129 | #assign 130 | let #out = (#(#bundle),*); 131 | bundle 132 | }} 133 | } 134 | 135 | pub fn get_edits(&self) -> (Vec<(Ident,Ident)>,TokenStream) { 136 | let mut expected = vec![]; 137 | let mut add = qt!(); 138 | for (var,builtin) in self.variables.iter() { 139 | let var = var.ident(); 140 | if *builtin { 141 | let vart = var.to_case(Case::Pascal); 142 | expected.push((var,vart)); 143 | } 144 | else {add.extend(qt!(ecmd.insert(#var);));} 145 | } 146 | if !add.is_empty() { 147 | expected.push(("ecmd".ident(),"EntityCommands".ident())); 148 | } 149 | let assign = self.assign.clone(); 150 | 151 | (expected,qt!({ 152 | #assign 153 | #add 154 | })) 155 | } 156 | 157 | } 158 | 159 | //\\ 160 | 161 | #[ext(pub trait Identasdf)] 162 | impl TokenTree { 163 | fn resolve_alias(self) -> Ident { 164 | macro_rules! masch {($($main:literal: $($alias:literal)*;)*)=>{match self.to_string().as_str(){ 165 | $($($alias)|* => $main.ident_span(self.span()),)* 166 | _ => if let TokenTree::Ident(id) = self {id} else {panic!{"waduhek?"}} 167 | }}} 168 | masch!{ 169 | 170 | // margin \| 171 | "margin": "m"; 172 | "margin_x": "mx"; 173 | "margin_y": "my"; 174 | "margin_left": "ml"; 175 | "margin_right": "mr"; 176 | "margin_top": "mt"; 177 | "margin_bottom": "mb"; 178 | 179 | // padding \| 180 | "padding": "p"; 181 | "padding_x": "px"; 182 | "padding_y": "py"; 183 | "padding_left": "pl"; 184 | "padding_right": "pr"; 185 | "padding_top": "pt"; 186 | "padding_bottom": "pb"; 187 | 188 | // \| 189 | "width": "w"; 190 | "height": "h"; 191 | "top": "t"; 192 | "left": "l"; 193 | "right": "r"; 194 | "bottom": "b"; 195 | 196 | // \| 197 | "line_height": "leading"; 198 | "font_size": "text_size"; 199 | "box_shadow": "shadow"; 200 | "flex_direction": "flex"; 201 | "min_width": "min_w"; 202 | "min_height": "min_h"; 203 | "max_width": "max_w"; 204 | "max_height": "max_h"; 205 | "column_gap": "gap_x"; 206 | "row_gap": "gap_y"; 207 | "border_radius": "rounded" "round"; 208 | "font_color": "color"; 209 | "background_color": "background" "bg"; 210 | "z_index": "zindex" "z"; 211 | "z_global": "zg"; 212 | "relative_cursor_position": "cursor_position" "cursor_pos"; 213 | "focus_policy": "focus"; 214 | "scroll_position": "scroll"; 215 | "image": "img"; 216 | "image_color": "img_color"; 217 | 218 | } 219 | } 220 | } 221 | 222 | // CSS-Like \\ 223 | 224 | macro_rules! qar {($([$($tt:tt)*])*)=>{vec![$(qt!($($tt)*)),*]}} 225 | 226 | pub struct UiEntry { 227 | /// e.g: type of [Self::value] 228 | pub typ: Str, 229 | /// e.g: .width 230 | pub fields: TokenStream, 231 | // consider switching to Option<_> //<< 232 | /// e.g. Val::Px(300.) 233 | /// - can be empty, indicating the field shouldn't change 234 | pub value: TokenStream, 235 | /// any extra tokens passtru 236 | pub extra: Option 237 | } 238 | 239 | type UiMap = StackMap>; 240 | 241 | pub fn ui_style_sheet(field:TokenTree,iter:&mut PeekIter) -> UiMap { 242 | iter.skip_puncts("#-$"); 243 | let mut map = UiMap::new(); 244 | 245 | macro_rules! out { 246 | ($main:ty => $sub:ty [$($ft:tt)*][$($vt:tt)*][$($extra:tt)*])=>{{ 247 | let main = stringify!($main); 248 | let sub = stringify!($sub); 249 | out!{>main => sub [$($ft)*][$($vt)*][$($extra)*]} 250 | }}; 251 | (>$main:tt => $sub:tt [$($ft:tt)*][$($vt:tt)*][$($extra:tt)*])=>{{ 252 | map.entry($main).push(UiEntry{ 253 | typ: $sub, 254 | fields: qt!{$($ft)*}, 255 | value: qt!{$($vt)*}, 256 | extra: $($extra)* 257 | }); 258 | }} 259 | } 260 | 261 | let field = field.resolve_alias(); 262 | match field.to_string().as_str() { 263 | 264 | // Shortcuts \| 265 | 266 | "absolute" => { 267 | let posi = qts![field.span()=>PositionType::Absolute]; 268 | out!{Node => _ [.position_type][#posi] [None]} 269 | } 270 | 271 | "relative" => { 272 | let posi = qts![field.span()=>PositionType::Relative]; 273 | out!{Node => _ [.position_type][#posi] [None]} 274 | } 275 | 276 | "hidden" => { 277 | let posi = qts![field.span()=>Visibility::Hidden]; 278 | out!{Visibility => _ [][#posi] [None]} 279 | } 280 | 281 | "visible" => { 282 | let posi = qts![field.span()=>Visibility::Visible]; 283 | out!{Visibility => _ [][#posi] [None]} 284 | } 285 | 286 | "inherit" => { 287 | let posi = qts![field.span()=>Visibility::Inherit]; 288 | out!{Visibility => _ [][#posi] [None]} 289 | } 290 | 291 | 292 | // \| 293 | 294 | "scale" => { 295 | let x = iter.next(); 296 | let x_extra = iter.try_extra(); 297 | let y = iter.next(); 298 | let y_extra = iter.try_extra(); 299 | exit!{x = x} 300 | out!{Transform => f32 [.scale.x][#x] [x_extra.clone()]} 301 | match y { 302 | Some(y) => out!{Transform => f32 [.scale.y][#y] [y_extra]}, 303 | None => out!{Transform => f32 [.scale.y][#x] [x_extra]} 304 | } 305 | } 306 | 307 | "rotation" => { 308 | let iter = iter.map(|t|match t { 309 | TokenTree::Literal(r) if r.is_numeric() => { 310 | let r: LitFloat = r.into(); 311 | let val = r.base10_parse::().unwrap(); 312 | let radian = match r.suffix() { 313 | "deg" => PI * val / 180., 314 | _ => val 315 | }; 316 | TokenTree::Literal(Literal::f32_suffixed(radian)) 317 | } 318 | _ => {t} 319 | }); 320 | let mut vec = iter.collect::>(); 321 | let mut extra = None; 322 | if let Some(TokenTree::Group(grp)) = vec.last() { 323 | if grp.delimiter().is_bracket(){ 324 | extra = Some(grp.stream()); 325 | vec.pop(); 326 | } 327 | } 328 | let token = TokenStream::from_iter(vec); 329 | out!{Transform => Quat [.rotation][Quat::from_rotation_z(#token)] [extra]} 330 | } 331 | 332 | "font_color" => match iter.try_into_color().prepare() { 333 | Some((color,_,extra)) => out!{TextColor => Color [.0][#color] [extra]}, 334 | _=>()} 335 | 336 | "font_size" => { 337 | kill!{val = iter.next()} 338 | out!{TextFont => f32 [.#field][#val as f32] [None]} 339 | } 340 | 341 | "background_color" => match iter.try_into_color().prepare() { 342 | Some((color,_,extra)) => out!{BackgroundColor => Color [.0][#color] [extra]}, 343 | _=>()} 344 | 345 | "border_color" => match iter.try_into_color().prepare() { 346 | Some((color,_,extra)) => { 347 | #[cfg(not(feature="0.17"))] 348 | out!{BorderColor => Color [.0][#color] [extra]} 349 | #[cfg(feature="0.17")] 350 | for a in qar!([top][right][bottom][left]){ 351 | out!{BorderColor => Color [.#a][#color] [extra.clone()]} 352 | } 353 | } 354 | _=>()} 355 | 356 | "border_radius" => { 357 | let vals = iter.into_rect_like(true); 358 | let fields = qar!([top_left][top_right][bottom_right][bottom_left]); 359 | for (field,oval) in zip(fields,vals) { 360 | next!{val = oval.main} 361 | let field = field.with_span(oval.span); 362 | out!{BorderRadius => Val [.#field][#val] [oval.extra]} 363 | } 364 | } 365 | 366 | "outline" => { 367 | let fields = qar!([width][offset]); 368 | for (field,oval) in zip(fields,iter.into_vals()) { 369 | next!{val = oval.main} 370 | let field = field.with_span(oval.span); 371 | out!{Outline => Val [.#field][#val] [oval.extra]} 372 | } 373 | if let Some((color,span,extra)) = iter.try_into_color().prepare() { 374 | let field = "color".ident_span(span); 375 | out!{Outline => Color [.#field][#color] [extra]} 376 | } 377 | } 378 | 379 | "text_shadow" => { 380 | if let Some((x,_)) = iter.try_number(){ 381 | let extra = iter.try_extra(); 382 | out!{TextShadow => f32 [.offset.x][#x as f32] [extra]} 383 | } 384 | if let Some((y,_)) = iter.try_number(){ 385 | let extra = iter.try_extra(); 386 | out!{TextShadow => f32 [.offset.y][#y as f32] [extra]} 387 | } 388 | if let Some((color,span,extra)) = iter.try_into_color().prepare() { 389 | let name = "color".ident_span(span); 390 | out!{TextShadow => Color [.#name][#color][extra]} 391 | } 392 | } 393 | 394 | "box_shadow" => { 395 | let fields = qar!([x_offset][y_offset][blur_radius][spread_radius]); 396 | for (field,oval) in zip(fields,iter.into_vals()) { 397 | next!{val = oval.main} 398 | let field = field.with_span(oval.span); 399 | #[cfg(any(feature="0.16",feature="0.17"))] 400 | out!{BoxShadow => Val [[0].#field][#val] [oval.extra]} 401 | #[cfg(feature="0.15")] 402 | out!{BoxShadow => Val [.#field][#val] [oval.extra]} 403 | #[cfg(not(feature="0.17"))] 404 | #[cfg(not(feature="0.16"))] 405 | #[cfg(not(feature="0.15"))]{ 406 | let err = compile_error_no_version(); 407 | out!{BoxShadow => Val [;][#err] [None]} 408 | } 409 | } 410 | if let Some((color,span,extra)) = iter.try_into_color().prepare() { 411 | let field = "color".ident_span(span); 412 | #[cfg(any(feature="0.16",feature="0.17"))] 413 | out!{BoxShadow => Color [[0].#field][#color] [extra]} 414 | #[cfg(feature="0.15")] 415 | out!{BoxShadow => Color [.#field][#color] [extra]} 416 | #[cfg(not(feature="0.17"))] 417 | #[cfg(not(feature="0.16"))] 418 | #[cfg(not(feature="0.15"))]{ 419 | let err = compile_error_no_version(); 420 | out!{BoxShadow => Color [;][#err] [None]} 421 | } 422 | } 423 | } 424 | 425 | "z_global" => { 426 | exit!{(num,_l) = iter.try_number()} 427 | let name = "GlobalZIndex"; 428 | out!{>name => "_" [.0][#num] [None]} 429 | } 430 | 431 | "z_index" => { 432 | exit!{(num,lit) = iter.try_number()} 433 | let name = match lit.suffix() { 434 | "g" => "GlobalZIndex", 435 | _ => "ZIndex" 436 | }; 437 | out!{>name => "_" [.0][#num] [None]} 438 | } 439 | 440 | "interaction" => {map.entry("Interaction");} 441 | 442 | "relative_cursor_position" => { 443 | map.entry("RelativeCursorPosition"); 444 | } 445 | 446 | "focus_policy" => { 447 | let var = iter.next().unwrap_or("Pass".ident().into()); 448 | let var = var.unwrap_ident().to_case(Case::Pascal); 449 | out!{FocusPolicy => _ [][FocusPolicy::#var] [None]} 450 | } 451 | 452 | "scroll_position" => { 453 | let vals = iter.into_vals(); 454 | if vals.is_empty(){ 455 | map.entry("ScrollPosition"); 456 | } else { 457 | #[cfg(not(feature="0.17"))] 458 | let fields = qar!([x_offset][y_offset]); 459 | #[cfg(feature="0.17")] 460 | let fields = qar!([x][y]); 461 | for (field,oval) in zip(fields,iter.into_vals()) { 462 | next!{val = oval.main} 463 | let field = field.with_span(oval.span); 464 | out!{ScrollPosition => Val [.#field][#val] [oval.extra]} 465 | } 466 | } 467 | } 468 | 469 | // \\ 470 | 471 | "line_height" => { 472 | kill!{val = iter.next()} 473 | out!{TextFont => _ [.line_height][bevy::text::LineHeight::RelativeToFont(#val as f32)] [None]} 474 | } 475 | 476 | "justify_text" => { 477 | let var = iter.next().unwrap_or("Left".ident().into()); 478 | let var = var.unwrap_ident().to_case(Case::Pascal); 479 | out!{TextLayout => _ [.justify][JustifyText::#var] [None]} 480 | } 481 | 482 | "line_break" => { 483 | let var = iter.next().unwrap_or("WordBoundary".ident().into()); 484 | let var = var.unwrap_ident().to_case(Case::Pascal); 485 | out!{TextLayout => _ [.linebreak][LineBreak::#var] [None]} 486 | } 487 | 488 | "text" => match iter.peek().unwrap().clone() { 489 | TokenTree::Literal(val) => { 490 | iter.next(); 491 | out!{TextFont => f32 [.font_size][#val as f32] [None]} 492 | } 493 | _ => { 494 | let var = iter.next().unwrap_or("WordBoundary".ident().into()); 495 | let var = var.unwrap_ident().to_case(Case::Pascal); 496 | match var.to_string().as_str() { 497 | "Left"|"Center"|"Right"|"Justified" 498 | => { 499 | #[cfg(feature="0.17")] 500 | out!{TextLayout => _ [.justify][Justify::#var] [None]} 501 | #[cfg(not(feature="0.17"))] 502 | out!{TextLayout => _ [.justify][JustifyText::#var] [None]} 503 | }, 504 | _ => out!{TextLayout => _ [.linebreak][LineBreak::#var] [None]} 505 | } 506 | } 507 | } 508 | 509 | "image" => match iter.peek_punct() { 510 | '$' => { 511 | iter.next(); 512 | exit!{var = iter.next(),inner_tokens()} 513 | out!{ImageNode => _ [.image][#var] [None]} 514 | }, 515 | '#' => match iter.try_into_color().prepare() { 516 | Some((color,_,extra)) => out!{ImageNode => Color [.color][#color] [extra]}, 517 | _ => () 518 | } 519 | _ => match iter.next() { 520 | Some(t) => match t.to_string().as_str() { 521 | "flip_y" => out!{ImageNode => _ [.flip_y][true] [None]}, 522 | "flip_x" => out!{ImageNode => _ [.flip_x][true] [None]}, 523 | _ => {} 524 | } 525 | _ => {} 526 | } 527 | } 528 | 529 | "image_color" => match iter.try_into_color().prepare() { 530 | Some((color,_,extra)) => out!{ImageNode => Color [.color][#color] [extra]}, 531 | _=>()} 532 | 533 | 534 | // Custom Groups \\ 535 | 536 | "xy"|"x"|"y" => { 537 | let vals = iter.into_rect_like(false); 538 | let fields = match field.to_string().as_str() { 539 | "xy" => qar!([top][right][bottom][left]), 540 | "x" => qar!([right][left]), 541 | _ => qar!([top][bottom]), 542 | }; 543 | for (field2,oval) in zip(fields,vals) { 544 | next!{val = oval.main} 545 | let field2 = field2.with_span(oval.span); 546 | out!{Node => Val [.#field2] [#val] [oval.extra]} 547 | } 548 | } 549 | 550 | "size" => { 551 | let fields = qar!([width][height]); 552 | for (field,oval) in zip(fields,iter.into_vals()) { 553 | next!{val = oval.main} 554 | let field = field.with_span(oval.span); 555 | out!{Node => Val [.#field][#val] [oval.extra]} 556 | } 557 | } 558 | 559 | "gap" => { 560 | let fields = qar!([row_gap][column_gap]); 561 | for (field,oval) in zip(fields,iter.into_vals()) { 562 | next!{val = oval.main} 563 | let field = field.with_span(oval.span); 564 | out!{Node => Val [.#field][#val] [oval.extra]} 565 | } 566 | } 567 | 568 | "position" => { 569 | let field = TokenTree::Ident("position_type".ident_span(field.span())); 570 | let enu = field.clone().unwrap_ident().to_case(Case::Pascal); 571 | kill!{val = iter.next(),unwrap_ident().to_case(Case::Pascal)} 572 | out!{Node => _ [.#field][#enu::#val] [None]} 573 | } 574 | 575 | //\\ 576 | 577 | "grid_auto_rows"|"grid_auto_columns" => { 578 | let tracks = iter.into_grid_tracks(); 579 | out!{Node => _ [.#field] [vec![#(#tracks),*]] [None]} 580 | }, 581 | 582 | "grid_template_rows"|"grid_template_columns" => { 583 | let tracks = iter.into_repeated_grid_tracks(); 584 | out!{Node => _ [.#field] [vec![#(#tracks),*]] [None]} 585 | } 586 | 587 | "grid_row"|"grid_column" => { 588 | let mut vecy = iter.collect::>(); 589 | let val = match vecy.len() { 590 | 0|1 => qt!{GridPlacement::DEFAULT;}, 591 | 2 => { 592 | kill!{attr = vecy.pop()} 593 | kill!{func = vecy.pop()} 594 | qt!{GridPlacement::#func(#attr)} 595 | } 596 | _ => { 597 | kill!{attr2 = vecy.pop()} 598 | kill!{attr1 = vecy.pop()} 599 | kill!{func = vecy.pop()} 600 | qt!{GridPlacement::#func(#attr1,#attr2)} 601 | } 602 | }; 603 | out!{Node => _ [.#field][#val] [None]} 604 | } 605 | 606 | "overflow_clip_margin" => { 607 | let mut vecy = iter.collect::>(); 608 | match vecy.len() { 609 | 0 => out!{Node => _ [.#field][OverflowClipMargin::DEFAULT] [None]}, 610 | 1 => { 611 | kill!{vbox = vecy.pop(),unwrap_ident().to_case(Case::Pascal)} 612 | out!{Node => _ [.#field.visual_box][OverflowClipBox::#vbox] [None]} 613 | } 614 | _ => { 615 | kill!{marg = vecy.pop()} 616 | kill!{vbox = vecy.pop(),unwrap_ident().to_case(Case::Pascal)} 617 | out!{Node => _ [.#field.visual_box][OverflowClipBox::#vbox] [None]} 618 | out!{Node => f32 [.#field.margin][#marg as f32] [None]} 619 | } 620 | }; 621 | } 622 | 623 | "overflow" => { 624 | let mut vecy = iter.map(|p|p.unwrap_ident().to_case(Case::Pascal)).collect::>(); 625 | match vecy.len(){ 626 | 0 => out!{Node => _ [.#field][Overflow::DEFAULT] [None]}, 627 | 1 => { 628 | let all = vecy.pop(); 629 | out!{Node => _ [.#field.x][OverflowAxis::#all] [None]} 630 | out!{Node => _ [.#field.y][OverflowAxis::#all] [None]} 631 | } 632 | _ => { 633 | let y = vecy.pop(); 634 | let x = vecy.pop(); 635 | out!{Node => _ [.#field.x][OverflowAxis::#x] [None]} 636 | out!{Node => _ [.#field.y][OverflowAxis::#y] [None]} 637 | } 638 | }; 639 | } 640 | 641 | "display"|"position_type"|"align_items"|"justify_items"|"align_self"|"justify_self"| 642 | "align_content"|"justify_content"|"flex_direction"|"flex_wrap"|"grid_auto_flow" => { 643 | let enu = field.clone().to_case(Case::Pascal); 644 | kill!{val = iter.next(),unwrap_ident().to_case(Case::Pascal)} 645 | out!{Node => _ [.#field][#enu::#val] [None]} 646 | } 647 | 648 | "aspect_ratio" => { 649 | kill!{val = iter.next()} 650 | out!{Node => f32 [.#field][Some(#val as f32)] [None]} 651 | } 652 | 653 | "flex_grow"|"flex_shrink" => { 654 | kill!{val = iter.next()} 655 | out!{Node => f32 [.#field][#val as f32] [None]} 656 | } 657 | 658 | "border" => { 659 | let vals = iter.into_rect_like(false); 660 | let fields = qar!([top][right][bottom][left]); 661 | for (field2,oval) in zip(fields,vals) { 662 | next!{val = oval.main} 663 | let field2 = field2.with_span(oval.span); 664 | out!{Node => Val [.#field.#field2] [#val] [oval.extra]} 665 | } 666 | if let Some((color,_,extra)) = iter.try_into_color().prepare() { 667 | #[cfg(not(feature="0.17"))] 668 | out!{BorderColor => Color [.0][#color] [extra]} 669 | #[cfg(feature="0.17")] 670 | for a in qar!([top][right][bottom][left]){ 671 | out!{BorderColor => Color [.#a][#color] [extra.clone()]} 672 | } 673 | } 674 | } 675 | 676 | "margin"|"padding" => { 677 | let vals = iter.into_rect_like(false); 678 | let fields = qar!([top][right][bottom][left]); 679 | for (field2,oval) in zip(fields,vals) { 680 | next!{val = oval.main} 681 | let field2 = field2.with_span(oval.span); 682 | out!{Node => Val [.#field.#field2] [#val] [oval.extra]} 683 | } 684 | } 685 | 686 | "margin_x"|"margin_y"|"margin_left"|"margin_right"|"margin_top"|"margin_bottom" 687 | |"padding_x"|"padding_y"|"padding_left"|"padding_right"|"padding_top"|"padding_bottom" => { 688 | let (field,sub) = field.to_string().split_once("_").map(|a|(a.0.ident_span(field.span()),a.1.to_string())).unwrap(); 689 | let oval = iter.into_val(); 690 | let fields = match sub.as_str() { 691 | "x" => qar!([right][left]), 692 | "y" => qar!([top][bottom]), 693 | "left" => qar!([left]), 694 | "right" => qar!([right]), 695 | "top" => qar!([top]), 696 | "bottom" => qar!([bottom]), 697 | _ => todo!{} 698 | }; 699 | for field2 in fields { 700 | next!{val = oval.main.clone()} 701 | let field2 = field2.with_span(oval.span); 702 | out!{Node => Val [.#field.#field2] [#val] [oval.extra.clone()]} 703 | } 704 | } 705 | 706 | "left"|"right"|"top"|"bottom"|"width"|"height"| 707 | "min_width"|"min_height"|"max_width"|"max_height"| 708 | "flex_basis"|"row_gap"|"column_gap" 709 | => if let UiToken{main:Some(val),span:_,extra} = iter.into_val() { 710 | out!{Node => Val [.#field][#val] [extra]} 711 | } 712 | 713 | _ => {} 714 | } 715 | map 716 | } 717 | 718 | 719 | // Ui Token \\ 720 | 721 | #[derive(Constructor,Clone)] 722 | pub struct UiToken { 723 | main: Option, 724 | span: Span, 725 | extra: Option 726 | } 727 | 728 | impl UiToken { 729 | /// Val::Auto with dirty [Span] 730 | fn val() -> Self {Self::new(Some(qt!{Val::Auto}),Span::call_site(),None)} 731 | 732 | fn prepare(self) -> Option<(TokenStream,Span,Option)> { 733 | match (self.main.yay(),self.extra.yay()) { 734 | (true,_) => Some((self.main.unwrap(),self.span,self.extra)), 735 | (_,true) => Some((self.main.unwrap_or(qt!{}),self.span,self.extra)), 736 | _ => None 737 | } 738 | } 739 | } 740 | 741 | 742 | 743 | // Token Handling \\ 744 | 745 | #[ext(trait DTreeExt)] 746 | impl TokenTree { 747 | 748 | fn is_keep(&self) -> bool { 749 | exit!{*TokenTree::Ident(id)=self} 750 | id.to_string().as_str() == "_" 751 | } 752 | 753 | fn is_valid_keep(&self,typ:Str) -> bool { 754 | exit!{*TokenTree::Ident(id)=self} 755 | ["_",typ].contains(&id.to_string().as_str()) 756 | } 757 | 758 | } 759 | 760 | #[ext(trait DOptTreeExt)] 761 | impl <'a> Option<&'a TokenTree> { 762 | 763 | fn is_keep(&self) -> bool { 764 | exit!{tok = self} 765 | tok.is_keep() 766 | } 767 | 768 | fn is_valid_keep(&self,typ:Str) -> bool { 769 | exit!{tok = self} 770 | tok.is_valid_keep(typ) 771 | } 772 | 773 | } 774 | 775 | #[ext(pub trait DTreeIterExt)] 776 | impl PeekIter { 777 | 778 | fn into_rect_like(&mut self,corner_align:bool) -> [UiToken;4]{ 779 | let default = UiToken::val(); 780 | self.into_vals().into_rect_like(corner_align,default,|mut v|{ 781 | v.main = v.main.map(|t|t.with_span(Span::call_site())); 782 | v 783 | }) 784 | } 785 | 786 | /// tries to extract a numeric ui::Val, only progresses if first token is a number literal. 787 | fn into_val(&mut self) -> UiToken { 788 | self.try_into_val().unwrap_or(UiToken::val()) 789 | } 790 | 791 | fn try_number(&mut self) -> Option<(TokenStream,LitInt)> { 792 | let pre = match self.peek_punct() { 793 | '-' => {self.next();qt!(-)}, 794 | '+' => {self.next();qt!()}, 795 | _ => qt!() 796 | }; 797 | exit!{val = self.next()} 798 | exit!{if !val.is_numeric()} 799 | let lit: LitInt = val.clone().unwrap_literal().into(); 800 | exit!{num = lit.base10_parse::()} 801 | Some((qt![#pre #num],lit)) 802 | } 803 | 804 | fn try_extra(&mut self) -> Option { 805 | exit!{*Some(TokenTree::Group(grp)) = self.peek()} 806 | let out = Some(grp.stream()); 807 | self.next(); 808 | out 809 | } 810 | 811 | /// tries to extract a numeric ui::Val, only progresses if first token is a number literal. 812 | /// 813 | /// # Alternative 814 | /// use [Self::into_val] if you want [None] to be Val::Auto 815 | fn try_into_val(&mut self) -> Option { 816 | if self.peek().is_valid_keep("v"){ 817 | return Some(UiToken::new(None,self.next().unwrap().span(),self.try_extra())); 818 | } 819 | Some(match self.next_valvar() { 820 | Step::Base((_pct,lit)) => { 821 | let lit: LitFloat = lit.clone().into(); 822 | kill!{num = lit.base10_parse::()} 823 | let sign = !_pct.map(|a|a.as_char()=='-').unwrap_or_default(); 824 | let num = if sign {num} else {-num}; 825 | let var = qts!{lit.span()=>Px(#num)}; 826 | UiToken::new( 827 | Some(qt!{Val::#var}), 828 | lit.span(),self.try_extra() 829 | ) 830 | } 831 | Step::Shift(var) => UiToken::new( 832 | Some(qt!{Val::#var}), 833 | var.span(), 834 | self.try_extra() 835 | ), 836 | _ => { 837 | exit!{var = self.peek()} 838 | match var.clone() { 839 | TokenTree::Punct(p) if p.as_char()=='$' => { 840 | self.next(); 841 | exit!{var = self.next()} 842 | UiToken::new(Some(qt![Val::Px(#var)]),var.span(),self.try_extra()) 843 | } 844 | _ => return None 845 | } 846 | } 847 | }) 848 | } 849 | 850 | /// extracts a vec of Vals, stops at the first invalid one 851 | fn into_vals_limited(&mut self,mut limit:u32) -> Vec { 852 | let mut out = vec![]; 853 | while let Some(val) = self.try_into_val() { 854 | out.push(val); 855 | limit -= 1; 856 | hold!{if limit == 0} 857 | } 858 | out 859 | } 860 | 861 | /// extracts a vec of Vals, stops at the first invalid one 862 | fn into_vals(&mut self) -> Vec { 863 | let mut out = vec![]; 864 | while let Some(val) = self.try_into_val() { 865 | out.push(val); 866 | } 867 | out 868 | } 869 | 870 | fn into_grid_track(&mut self) -> TokenStream { 871 | self.try_into_grid_track().unwrap() 872 | } 873 | 874 | /// extracts a vec of GridTracks, stops at the first invalid one 875 | fn into_grid_tracks(&mut self) -> Vec { 876 | let mut out = vec![]; 877 | while let Some(val) = self.try_into_grid_track() { 878 | out.push(val); 879 | } 880 | out 881 | } 882 | 883 | fn into_repeated_grid_track(&mut self) -> TokenStream { 884 | self.try_into_repeated_grid_track().unwrap() 885 | } 886 | 887 | /// extracts a vec of GridTracks, stops at the first invalid one 888 | fn into_repeated_grid_tracks(&mut self) -> Vec { 889 | let mut out = vec![]; 890 | while let Some(val) = self.try_into_repeated_grid_track() { 891 | out.push(val); 892 | } 893 | out 894 | } 895 | 896 | fn try_into_track_sizing_function(&mut self) -> Step<(Option,Literal),TokenStream> { 897 | match self.next_valvar() { 898 | Step::None => Step::Shift(match self.next() { 899 | Some(TokenTree::Ident(idnt)) => match idnt.to_string().as_str() { 900 | "min" => qt!{MinContent}, 901 | "max" => qt!{MaxContent}, 902 | "auto" => qt!{Auto}, 903 | _ => panic!("Failed into TrackSizingFunction") 904 | }.with_span(idnt.span()), 905 | Some(tok) => qts!{tok.span()=>Auto}, 906 | _ => qt!{Auto}, 907 | }), 908 | Step::Shift(s) => Step::Shift(s), 909 | Step::Base(b) => Step::Base(b), 910 | } 911 | } 912 | 913 | fn try_into_track_sizing_function_max(&mut self) -> Step<(Option,Literal),TokenStream> { 914 | match self.try_into_track_sizing_function(){ 915 | Step::Shift(var) => Step::Shift(var), 916 | Step::Base((pct,lit)) => match LitFloat::from(lit.clone()).suffix() { 917 | "fit_px" => Step::Shift(qts!{lit.span()=>FitContentPx(#lit)}), 918 | "fit" => match self.peek_punct(){ 919 | '%' => {self.next();Step::Shift(qts!{lit.span()=>FitContentPercent(#lit)})} 920 | '!' => {self.next();Step::Shift(qts!{lit.span()=>FitContentPx(#lit)})} 921 | _ => Step::Base((pct,lit)) 922 | } 923 | _ => Step::Base((pct,lit)) 924 | } 925 | Step::None => Step::None 926 | } 927 | } 928 | 929 | fn try_into_grid_track(&mut self) -> Option { 930 | self.try_into_grid_track_base(TokenStream::new()).map(|t|qts!{t.span()=>GridTrack #t}) 931 | } 932 | 933 | fn try_into_grid_track_base(&mut self,fattr:TokenStream) -> Option { 934 | exit!{peek = self.peek(),clone()} 935 | match peek { 936 | TokenTree::Ident(func) => { 937 | self.next(); 938 | Some(match func.to_string().as_str() { 939 | "auto"|"min_content"|"max_content" => qt!{::#func(#fattr)}, 940 | "min" => qt!{::min_content(#fattr)}, 941 | "max" => qt!{::max_content(#fattr)}, 942 | _ => panic!{} 943 | }.with_span(func.span())) 944 | } 945 | TokenTree::Group(grp) => { 946 | self.next(); 947 | let mut iter = grp.stream().into_iter().peekable(); 948 | let min = iter.try_into_track_sizing_function().shift_or(qt!{Auto}); 949 | iter.skip_puncts("#-"); 950 | let max = iter.try_into_track_sizing_function_max().risk_shift(); 951 | let min = qts!{min.span()=>MinTrackSizingFunction::#min}; 952 | let max = qts!{max.span()=>MaxTrackSizingFunction::#max}; 953 | Some(qts!{grp.span_close()=>::minmax(#fattr #min,#max)}) 954 | } 955 | _ => { 956 | kill!{val = self.next_if_num()} 957 | let lit: LitFloat = val.clone().into(); 958 | let suffix = lit.suffix(); 959 | let func = match suffix { 960 | "" => match self.peek_punct() { 961 | '%' => {self.next();qt!{percent}} 962 | '!'|'n' => {self.next();qt!{px}} 963 | _ => panic!() 964 | } 965 | _ => suffix.ident().to_token_stream() 966 | }; 967 | exit!{val = lit.base10_parse::()} 968 | Some(qt!{::#func(#fattr #val)}.with_span(peek.span())) 969 | } 970 | } 971 | } 972 | 973 | fn try_into_repeated_grid_track(&mut self) -> Option { 974 | exit!{rep = self.next()} 975 | let repe = match &rep { 976 | TokenTree::Literal(_) => if rep.is_numeric() {qt!{#rep}} else {panic!()} 977 | TokenTree::Ident(idn) => match idn.to_string().as_str() { 978 | "fill"|"auto_fill" => qts!{idn.span()=>GridTrackRepetition::AutoFill}, 979 | "fit"|"auto_fit" => qts!{idn.span()=>GridTrackRepetition::AutoFit}, 980 | _ => panic!() 981 | } 982 | _ => panic!() 983 | }; 984 | let mut spon = self.skip_puncts("#-"); 985 | spon.insert(0,rep); 986 | let track = if !self.peek().is_any_ident(){ 987 | self.try_into_grid_track_base(qt!{#repe,}) 988 | } else { 989 | self.try_into_grid_track_base(repe) 990 | }; 991 | track.map(|t|{ 992 | let mut spon = TokenStream::from_iter(spon);spon.extend(t.clone()); 993 | qts!{spon.span()=>RepeatedGridTrack #t} 994 | }) 995 | } 996 | 997 | fn try_into_color(&mut self) -> UiToken { 998 | match self.peek_punct() { 999 | '$' => { 1000 | self.next(); 1001 | kill!{var = self.next()} 1002 | let tok = var.inner_tokens(); 1003 | UiToken::new(Some(tok),var.span(),self.try_extra()) 1004 | } 1005 | '#' => { 1006 | self.next(); 1007 | kill!{(stream,span) = self.next_hex_color()} 1008 | UiToken::new(Some(stream),span,self.try_extra()) 1009 | } 1010 | _ => { 1011 | if let Some(css) = self.next() { 1012 | if css.is_valid_keep("c"){ 1013 | UiToken::new(None,css.span(),self.try_extra()) 1014 | } else { 1015 | let css = css.unwrap_ident().to_case(Case::UpperSnake); 1016 | UiToken::new(Some(qt!{Color::Srgba(bevy::color::palettes::css::#css)}),css.span(),self.try_extra()) 1017 | } 1018 | } 1019 | else { 1020 | UiToken::new(None,Span::call_site(),None) 1021 | } 1022 | } 1023 | } 1024 | } 1025 | } 1026 | 1027 | fn compile_error_no_version() -> TokenStream { 1028 | qt!{compile_error!{"Mevy: Missing bevy version!: Specify it in Cargo.toml! e.g. feature=[\"0.15\"])"}} 1029 | } 1030 | 1031 | // EOF \\ 1032 | --------------------------------------------------------------------------------