From 88c3196717c25c16886b3a4fe285831ea12acf3a Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Sun, 31 Aug 2025 16:10:49 +0200 Subject: [PATCH 01/29] Added Heading tag --- README.md | 8 +-- efx-core/src/doc_prelude.rs | 2 + efx-sandbox/src/main.rs | 24 +++++--- efx/Changelog.md | 3 +- efx/docs/tags.md | 26 ++++++++ efx/src/render.rs | 1 + efx/src/tags/heading.rs | 118 ++++++++++++++++++++++++++++++++++++ efx/src/tags/mod.rs | 2 + 8 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 efx/src/tags/heading.rs diff --git a/README.md b/README.md index b431f18..f764811 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can embed arbitrary Rust expressions inside braces (`{...}`). Requires `egui` (the project currently uses `egui 0.32`). Add to `Cargo.toml`: ```toml [dependencies] -efx = "0.5" +efx = "0.6" egui = "0.32" # or egui-based framework ``` @@ -123,7 +123,7 @@ EFx renders into any runtime that provides `&mut egui::Ui`. We officially build ```toml # Cargo.toml [dependencies] -efx = "0.5" +efx = "0.6" eframe = "0.32" ``` @@ -141,7 +141,7 @@ egui::CentralPanel::default().show(ctx, |ui| { ```toml # Cargo.toml [dependencies] -efx = "0.5" +efx = "0.6" bevy = "0.16" bevy_egui = "0.36" # re-exports `egui` ``` @@ -162,7 +162,7 @@ bevy_egui::egui::Window::new("EFx").show(egui_ctx.ctx_mut(), |ui| { ```toml # Cargo.toml [dependencies] -efx = "0.5" +efx = "0.6" egui = "0.32" egui-winit = "0.32" egui-wgpu = "0.32" diff --git a/efx-core/src/doc_prelude.rs b/efx-core/src/doc_prelude.rs index 520dff9..e890cb4 100644 --- a/efx-core/src/doc_prelude.rs +++ b/efx-core/src/doc_prelude.rs @@ -44,6 +44,8 @@ impl Ui { #[inline] pub fn label(&mut self, _text: T) {} #[inline] + pub fn heading(&mut self, _text: T) {} + #[inline] pub fn button(&mut self, _text: T) -> Resp { Resp } diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 99b9400..d9c19a2 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -1,4 +1,4 @@ -use eframe::{egui, NativeOptions}; +use eframe::{NativeOptions, egui}; use efx::efx; fn main() -> eframe::Result<()> { @@ -20,20 +20,27 @@ impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { // Header - let _ = efx!(ui, r#" + let _ = efx!( + ui, + r#" - "#); + "# + ); // Increment/decrement buttons - catch Response ui.horizontal(|ui| { let inc = efx!(ui, r#""#); - if inc.clicked() { self.counter += 1; } + if inc.clicked() { + self.counter += 1; + } let dec = efx!(ui, r#""#); - if dec.clicked() { self.counter -= 1; } + if dec.clicked() { + self.counter -= 1; + } }); // Dynamic text via {expr} @@ -43,7 +50,9 @@ impl eframe::App for App { let _ = efx!(ui, r#""#); // Scrolling + different tags - let _ = efx!(ui, r##" + let _ = efx!( + ui, + r##" @@ -59,7 +68,8 @@ impl eframe::App for App { - "##); + "## + ); }); } } diff --git a/efx/Changelog.md b/efx/Changelog.md index 568ddb2..f7ab2fe 100644 --- a/efx/Changelog.md +++ b/efx/Changelog.md @@ -1,8 +1,9 @@ ## Changelog -#### 0.6 (conceivably) +#### 0.6 - New Tags: Heading, Image, Grid - Added Panel Tags: Window, SidePanel +- Added examples & tests #### 0.5 - Attribute rendering (efx-core) diff --git a/efx/docs/tags.md b/efx/docs/tags.md index 56db440..1966641 100644 --- a/efx/docs/tags.md +++ b/efx/docs/tags.md @@ -253,3 +253,29 @@ efx!(Ui::default(), r#" "#); ``` + +### `Heading` + +Text heading. Generates `ui.heading(text)` with optional style overrides. + +**Attributes** + +- `level="1..6"` — heading level (integer). + *Default:* `1`. Maps to predefined `egui` text styles. +- `size="N"` — overrides the font size (f32). +- `color="name|#RRGGBB[AA]"` — text color. +- `tooltip="text"` — hover tooltip. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##" + + Main title + Section + Small note + +"##); +``` +The level attribute controls the base style (h1–h6), while size and color can further adjust the appearance. diff --git a/efx/src/render.rs b/efx/src/render.rs index 7413df3..f0acb0e 100644 --- a/efx/src/render.rs +++ b/efx/src/render.rs @@ -37,6 +37,7 @@ pub(crate) fn render_node_stmt(ui: &UI, node: &Node) -> TokenStrea fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { match el.name.as_str() { + "Heading" => render_tag::(ui, el), "CentralPanel" => render_tag::(ui, el), "ScrollArea" => render_tag::(ui, el), "Row" => render_tag::(ui, el), diff --git a/efx/src/tags/heading.rs b/efx/src/tags/heading.rs new file mode 100644 index 0000000..e01f3e7 --- /dev/null +++ b/efx/src/tags/heading.rs @@ -0,0 +1,118 @@ +use crate::tags::{Tag, TagAttributes}; +use crate::utils::attr::*; +use crate::utils::buffer::build_buffer_from_children; +use efx_attrnames::AttrNames; +use efx_core::Element; +use proc_macro2::{Span, TokenStream}; +use quote::{ToTokens, quote}; +use syn::LitStr; + +pub struct Heading { + attributes: Attributes, + element: Element, +} + +impl Tag for Heading { + fn from_element(el: &Element) -> Result { + let attributes = Attributes::new(el)?; + Ok(Self { + attributes, + element: el.clone(), + }) + } + + fn content(&self, ui: &UI) -> TokenStream { + let (buf_init, buf_build) = build_buffer_from_children(&self.element.children); + + let mods = self.get_mods(); + let has_mods = !mods.is_empty() || self.attributes.tooltip.is_some(); + + // If no mods → plain ui.heading(...) + if !has_mods { + return quote! { + #buf_init + #buf_build + let _ = #ui.heading(__efx_buf); + }; + } + + let tooltip_ts = if let Some(tt) = &self.attributes.tooltip { + let tt_lit = LitStr::new(tt, Span::call_site()); + quote! { __efx_resp = __efx_resp.on_hover_text(#tt_lit); } + } else { + quote! {} + }; + + // Otherwise → RichText + tooltip + quote! { + #buf_init + #buf_build + let __efx_rich = egui::RichText::new(__efx_buf) #mods; + let mut __efx_resp = #ui.heading(__efx_rich); + #tooltip_ts + let _ = __efx_resp; + } + } + + fn render(&self, ui: &UI) -> TokenStream { + self.content(ui) + } +} + +impl Heading { + fn get_mods(&self) -> TokenStream { + let mut ts = TokenStream::new(); + + if let Some(color_ts) = &self.attributes.color { + ts.extend(quote! { .color(#color_ts) }); + } + + if let Some(size) = self.attributes.size { + ts.extend(quote! { .size(#size as f32) }); + } else if let Some(level) = self.attributes.level { + let default_size = match level { + 1 => 24.0f32, + 2 => 20.0f32, + 3 => 18.0f32, + 4 => 16.0f32, + 5 => 14.0f32, + 6 => 12.0f32, + _ => 16.0f32, + }; + ts.extend(quote! { .size(#default_size) }); + } + + ts + } +} + +#[derive(Clone, Debug, AttrNames)] +struct Attributes { + level: Option, + size: Option, + color: Option, + tooltip: Option, +} + +impl TagAttributes for Attributes { + fn new(el: &Element) -> Result { + let map = match attr_map(el, Self::ATTR_NAMES, "Heading") { + Ok(m) => m, + Err(err) => return Err(err), + }; + + let level = u8_opt(&map, "level")?; + if let Some(lv) = level { + if lv < 1 || lv > 6 { + return Err(quote! { compile_error!("efx: `level` must be in 1..=6"); }); + } + } + + Ok(Attributes { + level, + size: f32_opt(&map, "size")?, + color: color_tokens_opt(&map, "color")?, + tooltip: map.get("tooltip").map(|s| (*s).to_string()), + }) + } +} diff --git a/efx/src/tags/mod.rs b/efx/src/tags/mod.rs index 4a906df..cf849b2 100644 --- a/efx/src/tags/mod.rs +++ b/efx/src/tags/mod.rs @@ -1,6 +1,7 @@ pub mod button; pub mod central_panel; pub mod column; +pub mod heading; pub mod hyperlink; pub mod label; pub mod row; @@ -13,6 +14,7 @@ pub use button::Button; pub use central_panel::CentralPanel; pub use column::Column; use efx_core::Element; +pub use heading::Heading; pub use hyperlink::Hyperlink; pub use label::Label; use proc_macro2::TokenStream; From 0bf24f5ff7ab563a5893e71f0c36a91526eb8aa1 Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Sun, 31 Aug 2025 17:32:27 +0200 Subject: [PATCH 02/29] Fixed Heading tag --- efx-sandbox/src/main.rs | 3 +++ efx/docs/intro.md | 4 +--- efx/src/tags/heading.rs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index d9c19a2..0597488 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -54,6 +54,9 @@ impl eframe::App for App { ui, r##" + Main title + Section + Small note diff --git a/efx/docs/intro.md b/efx/docs/intro.md index 49449ae..afe8996 100644 --- a/efx/docs/intro.md +++ b/efx/docs/intro.md @@ -37,9 +37,7 @@ efx!(Ui::default(), r#" **Where it lives** -``` -/efx-sandbox -``` +`/efx-sandbox` This crate is part of the workspace and is **not published**. diff --git a/efx/src/tags/heading.rs b/efx/src/tags/heading.rs index e01f3e7..8948646 100644 --- a/efx/src/tags/heading.rs +++ b/efx/src/tags/heading.rs @@ -47,8 +47,8 @@ impl Tag for Heading { quote! { #buf_init #buf_build - let __efx_rich = egui::RichText::new(__efx_buf) #mods; - let mut __efx_resp = #ui.heading(__efx_rich); + let __efx_rich = egui::RichText::new(__efx_buf).heading() #mods; + let mut __efx_resp = #ui.add(egui::widgets::Label::new(__efx_rich)); #tooltip_ts let _ = __efx_resp; } From 47b71dd8f3c536a3af3703ebf4e0713e162d3ccf Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Sun, 31 Aug 2025 18:32:55 +0200 Subject: [PATCH 03/29] Added tags TopPanel, SidePanel, BottomPanel --- Makefile | 3 + efx-sandbox/src/main.rs | 13 +++ efx/Changelog.md | 5 +- efx/docs/tags.md | 103 +++++++++++++++++ efx/src/render.rs | 3 + efx/src/tags/bottom_panel.rs | 154 ++++++++++++++++++++++++++ efx/src/tags/mod.rs | 6 + efx/src/tags/side_panel.rs | 209 +++++++++++++++++++++++++++++++++++ efx/src/tags/top_panel.rs | 179 ++++++++++++++++++++++++++++++ 9 files changed, 673 insertions(+), 2 deletions(-) create mode 100644 efx/src/tags/bottom_panel.rs create mode 100644 efx/src/tags/side_panel.rs create mode 100644 efx/src/tags/top_panel.rs diff --git a/Makefile b/Makefile index 6a96e19..f3826bf 100644 --- a/Makefile +++ b/Makefile @@ -42,5 +42,8 @@ dump: ## Make dump of project ! -path "./target/*" \ -exec sh -c 'echo ">>> START {}"; cat "{}"; echo ">>> END {}"; echo ""' \; > efx_code.dump.txt +sandbox: ## Run sandbox on local machine + @cargo run -p efx-sandbox + help: ## Outputs this help screen @grep -E '(^[a-zA-Z0-9\./_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 0597488..64b65a1 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -73,6 +73,19 @@ impl eframe::App for App { "## ); + + // SidePanel with other tags + let _ = efx!(ui, r##" + + + + + + + + + + "##); }); } } diff --git a/efx/Changelog.md b/efx/Changelog.md index f7ab2fe..34e28f9 100644 --- a/efx/Changelog.md +++ b/efx/Changelog.md @@ -2,8 +2,9 @@ #### 0.6 - New Tags: Heading, Image, Grid -- Added Panel Tags: Window, SidePanel -- Added examples & tests +- Added Panel Tags: Window, SidePanel, TopPanel, BottomPanel +- Sandbox +- Examples & tests #### 0.5 - Attribute rendering (efx-core) diff --git a/efx/docs/tags.md b/efx/docs/tags.md index 1966641..4d881c2 100644 --- a/efx/docs/tags.md +++ b/efx/docs/tags.md @@ -200,6 +200,109 @@ efx!(Ui::default(), r##" "##); ``` +### `` + +Docked panel attached to the left or right edge of the window. +Typically used for navigation, tool palettes, or context inspectors. + +**Children:** rendered inside the panel. + +**Required attributes** +- `side="left|right"` — which edge to dock to. +- `id="string"` — egui `Id` salt to keep layout state (width, resize state). + +**Frame & styling** +- `frame="true|false"` — enable/disable the default frame (default: `true`). +- `fill="#RRGGBB[AA]"` — background color. +- `stroke-width="number"` — border width, in points. +- `stroke-color="#RRGGBB[AA]"` — border color. +- `padding`, `padding-left|right|top|bottom` — inner margin (content padding). +- `margin`, `margin-left|right|top|bottom` — outer margin. + +**Sizing & behavior** +- `default-width="number"` — initial width. +- `min-width="number"` — lower width bound. +- `max-width="number"` — upper width bound. +- `resizable="true|false"` — whether the user can drag to resize (default: `true`). + +**Example** +```xml + + + + + + + + + +``` + +### `` + +A docked panel attached to the top edge of the window. +Useful for app bars, toolbars, status strips, or context headers. + +**Children:** rendered inside the panel. + +**Required attributes** +- `id="string"` — egui `Id` salt to persist panel state. + +**Frame & styling** +- `frame="true|false"` — enable/disable default frame (default: `true`). +- `fill="#RRGGBB[AA]"` — background color. +- `stroke-width="number"` — border width (points). +- `stroke-color="#RRGGBB[AA]"` — border color. +- `padding`, `padding-left|right|top|bottom` — inner margin. +- `margin`, `margin-left|right|top|bottom` — outer margin. + +**Sizing & behavior** +- `default-height="number"` — initial height. +- `min-height="number"` — minimum height. +- `max-height="number"` — maximum height. +- `resizable="true|false"` — allow user resize (default: `true`). + +**Example** +```xml + + + + + + + + + +``` + +### `` + +A docked panel attached to the bottom edge of the window. +Great for logs, consoles, timelines, or status bars. + +**Children**: rendered inside the panel. + +**Required attributes** +- `id="string"` — egui Id salt. + +**Frame & styling** +- `frame="true|false"`, `fill`, `stroke-width`, `stroke-color`, `padding*` / `margin*` — same as ``. + +**Sizing & behavior** +- `default-height`, `min-height`, `max-height`, `resizable` — same as . + +**Example** +```xml + + + + + + + + +``` + ### `ScrollArea` Scrollable container backed by `egui::ScrollArea`. Wraps its children and provides vertical/horizontal/both scrolling. diff --git a/efx/src/render.rs b/efx/src/render.rs index f0acb0e..3c1ee08 100644 --- a/efx/src/render.rs +++ b/efx/src/render.rs @@ -39,6 +39,9 @@ fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { match el.name.as_str() { "Heading" => render_tag::(ui, el), "CentralPanel" => render_tag::(ui, el), + "SidePanel" => render_tag::(ui, el), + "TopPanel" => render_tag::(ui, el), + "BottomPanel" => render_tag::(ui, el), "ScrollArea" => render_tag::(ui, el), "Row" => render_tag::(ui, el), "Column" => render_tag::(ui, el), diff --git a/efx/src/tags/bottom_panel.rs b/efx/src/tags/bottom_panel.rs new file mode 100644 index 0000000..c240486 --- /dev/null +++ b/efx/src/tags/bottom_panel.rs @@ -0,0 +1,154 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use efx_attrnames::AttrNames; +use efx_core::Element; +use crate::tags::{Tag, TagAttributes}; +use crate::utils::attr::*; +use crate::utils::render::render_children_stmt; + +pub struct BottomPanel { + attributes: Attributes, + element: Element, +} + +impl Tag for BottomPanel { + fn from_element(el: &Element) -> Result + where + Self: Sized + { + Ok(Self { attributes: Attributes::new(el)?, element: el.clone() }) + } + + fn content(&self, _ui: &UI) -> TokenStream { + let mut ts = TokenStream::new(); + + ts.extend(match self.attributes.frame { + Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), + _ => quote!( let mut __efx_frame = egui::Frame::default(); ), + }); + + if let Some(fill) = &self.attributes.fill { + ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); + } + if let Some(im) = self.attributes.padding_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); + } + if let Some(om) = self.attributes.margin_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + } + if let Some(st) = stroke_tokens(self.attributes.stroke_width, self.attributes.stroke_color.clone()) { + ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); + } + + ts + } + + fn render(&self, ui: &UI) -> TokenStream { + let id = match &self.attributes.id { + Some(s) if !s.is_empty() => s, + _ => { + let msg = "efx: requires non-empty `id` attribute"; + return quote! { compile_error!(#msg); }; + } + }; + + let children = render_children_stmt("e!(ui), &self.element.children); + let frame_ts = self.content(ui); + + let mut panel_ts = quote!( let mut __efx_panel = egui::TopBottomPanel::bottom(#id).frame(__efx_frame); ); + if let Some(b) = self.attributes.resizable { + panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); + } + if let Some(v) = self.attributes.default_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.default_height(#v as f32); )); + } + if let Some(v) = self.attributes.min_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.min_height(#v as f32); )); + } + if let Some(v) = self.attributes.max_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.max_height(#v as f32); )); + } + + quote! {{ + #frame_ts + #panel_ts + __efx_panel.show(&#ui.ctx(), |ui| { #children }); + }} + } +} + +#[derive(Clone, Debug, AttrNames)] +struct Attributes { + id: Option, + + frame: Option, + fill: Option, + #[attr(name = "stroke-width")] + stroke_width: Option, + #[attr(name = "stroke-color")] + stroke_color: Option, + + #[attr(name = "default-height")] + default_height: Option, + #[attr(name = "min-height")] + min_height: Option, + #[attr(name = "max-height")] + max_height: Option, + resizable: Option, + + padding: Option, + #[attr(name = "padding-left")] + padding_l: Option, + #[attr(name = "padding-right")] + padding_r: Option, + #[attr(name = "padding-top")] + padding_t: Option, + #[attr(name = "padding-bottom")] + padding_b: Option, + + margin: Option, + #[attr(name = "margin-left")] + margin_l: Option, + #[attr(name = "margin-right")] + margin_r: Option, + #[attr(name = "margin-top")] + margin_t: Option, + #[attr(name = "margin-bottom")] + margin_b: Option, +} + +impl Attributes { + fn padding_ts(&self) -> Option { + margin_tokens(self.padding, self.padding_l, self.padding_r, self.padding_t, self.padding_b) + } + fn margin_ts(&self) -> Option { + margin_tokens(self.margin, self.margin_l, self.margin_r, self.margin_t, self.margin_b) + } +} + +impl TagAttributes for Attributes { + fn new(el: &Element) -> Result { + let map = attr_map(el, Attributes::ATTR_NAMES, "BottomPanel")?; + Ok(Attributes { + id: map.get("id").map(|s| (*s).to_string()), + frame: bool_opt(&map, "frame")?, + fill: color_tokens_opt(&map, "fill")?, + stroke_width: f32_opt(&map, "stroke-width")?, + stroke_color: color_tokens_opt(&map, "stroke-color")?, + default_height: f32_opt(&map, "default-height")?, + min_height: f32_opt(&map, "min-height")?, + max_height: f32_opt(&map, "max-height")?, + resizable: bool_opt(&map, "resizable")?, + padding: f32_opt(&map, "padding")?, + padding_l: f32_opt(&map, "padding-left")?, + padding_r: f32_opt(&map, "padding-right")?, + padding_t: f32_opt(&map, "padding-top")?, + padding_b: f32_opt(&map, "padding-bottom")?, + margin: f32_opt(&map, "margin")?, + margin_l: f32_opt(&map, "margin-left")?, + margin_r: f32_opt(&map, "margin-right")?, + margin_t: f32_opt(&map, "margin-top")?, + margin_b: f32_opt(&map, "margin-bottom")?, + }) + } +} diff --git a/efx/src/tags/mod.rs b/efx/src/tags/mod.rs index cf849b2..d74e3db 100644 --- a/efx/src/tags/mod.rs +++ b/efx/src/tags/mod.rs @@ -7,8 +7,11 @@ pub mod label; pub mod row; pub mod scroll_area; pub mod separator; +pub mod side_panel; pub mod text_field; +pub mod top_panel; pub mod window; +pub mod bottom_panel; pub use button::Button; pub use central_panel::CentralPanel; @@ -22,7 +25,10 @@ use quote::{ToTokens, quote}; pub use row::Row; pub use scroll_area::ScrollArea; pub use separator::Separator; +pub use side_panel::SidePanel; +pub use top_panel::TopPanel; pub use text_field::TextField; +pub use bottom_panel::BottomPanel; pub trait Tag: Sized { /// Constructor from Element (parses attributes and captures children inside self). diff --git a/efx/src/tags/side_panel.rs b/efx/src/tags/side_panel.rs new file mode 100644 index 0000000..5d3202a --- /dev/null +++ b/efx/src/tags/side_panel.rs @@ -0,0 +1,209 @@ +use crate::tags::{Tag, TagAttributes}; +use crate::utils::attr::*; +use crate::utils::render::render_children_stmt; +use efx_attrnames::AttrNames; +use efx_core::Element; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; + +pub struct SidePanel { + attributes: Attributes, + element: Element, +} + +impl Tag for SidePanel { + fn from_element(el: &Element) -> Result + where + Self: Sized, + { + let attributes = Attributes::new(el)?; + Ok(Self { + attributes, + element: el.clone(), + }) + } + + fn content(&self, _ui: &UI) -> TokenStream { + let mut ts = TokenStream::new(); + + // default frame or none + ts.extend(match self.attributes.frame { + Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), + _ => quote!( let mut __efx_frame = egui::Frame::default(); ), + }); + + if let Some(fill) = &self.attributes.fill { + ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); + } + if let Some(im) = self.attributes.padding_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); + } + if let Some(om) = self.attributes.margin_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + } + if let Some(st) = stroke_tokens( + self.attributes.stroke_width, + self.attributes.stroke_color.clone(), + ) { + ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); + } + + ts + } + + fn render(&self, ui: &UI) -> TokenStream { + let children = render_children_stmt("e!(ui), &self.element.children); + let frame_ts = self.content(ui); + + let side = match self.attributes.side.as_deref() { + Some(s @ ("left" | "right")) => s, + Some(other) => { + let msg = format!( + "efx: `side` must be `left` or `right`, got `{}`", + other + ); + return quote! { compile_error!(#msg); }; + } + None => { + let msg = "efx: requires `side` attribute (`left` | `right`)"; + return quote! { compile_error!(#msg); }; + } + }; + let id = match &self.attributes.id { + Some(s) if !s.is_empty() => s, + _ => { + let msg = "efx: requires non-empty `id` attribute"; + return quote! { compile_error!(#msg); }; + } + }; + + let mut panel_ts = TokenStream::new(); + match side { + "left" => panel_ts.extend(quote!( let mut __efx_panel = egui::SidePanel::left(#id); )), + "right" => { + panel_ts.extend(quote!( let mut __efx_panel = egui::SidePanel::right(#id); )) + } + _ => {} + } + panel_ts.extend(quote!( __efx_panel = __efx_panel.frame(__efx_frame); )); + + if let Some(b) = self.attributes.resizable { + panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); + } + if let Some(v) = self.attributes.default_width { + panel_ts.extend(quote!( __efx_panel = __efx_panel.default_width(#v as f32); )); + } + if let Some(v) = self.attributes.min_width { + panel_ts.extend(quote!( __efx_panel = __efx_panel.min_width(#v as f32); )); + } + if let Some(v) = self.attributes.max_width { + panel_ts.extend(quote!( __efx_panel = __efx_panel.max_width(#v as f32); )); + } + + quote! {{ + #frame_ts + #panel_ts + __efx_panel.show(&#ui.ctx(), |ui| { #children }); + }} + } +} + +#[derive(Clone, Debug, AttrNames)] +struct Attributes { + /// required: left | right + side: Option, + /// required: egui Id salt + id: Option, + + // frame on/off + styling + frame: Option, + fill: Option, + #[attr(name = "stroke-width")] + stroke_width: Option, + #[attr(name = "stroke-color")] + stroke_color: Option, + + // sizing + #[attr(name = "default-width")] + default_width: Option, + #[attr(name = "min-width")] + min_width: Option, + #[attr(name = "max-width")] + max_width: Option, + resizable: Option, + + // padding (inner_margin) + padding: Option, + #[attr(name = "padding-left")] + padding_l: Option, + #[attr(name = "padding-right")] + padding_r: Option, + #[attr(name = "padding-top")] + padding_t: Option, + #[attr(name = "padding-bottom")] + padding_b: Option, + + // margin (outer_margin) + margin: Option, + #[attr(name = "margin-left")] + margin_l: Option, + #[attr(name = "margin-right")] + margin_r: Option, + #[attr(name = "margin-top")] + margin_t: Option, + #[attr(name = "margin-bottom")] + margin_b: Option, +} + +impl Attributes { + fn padding_ts(&self) -> Option { + margin_tokens( + self.padding, + self.padding_l, + self.padding_r, + self.padding_t, + self.padding_b, + ) + } + fn margin_ts(&self) -> Option { + margin_tokens( + self.margin, + self.margin_l, + self.margin_r, + self.margin_t, + self.margin_b, + ) + } +} + +impl TagAttributes for Attributes { + fn new(el: &Element) -> Result { + let map = attr_map(el, Attributes::ATTR_NAMES, "SidePanel")?; + Ok(Attributes { + side: map.get("side").map(|s| (*s).to_string()), + id: map.get("id").map(|s| (*s).to_string()), + + frame: bool_opt(&map, "frame")?, + fill: color_tokens_opt(&map, "fill")?, + stroke_width: f32_opt(&map, "stroke-width")?, + stroke_color: color_tokens_opt(&map, "stroke-color")?, + + default_width: f32_opt(&map, "default-width")?, + min_width: f32_opt(&map, "min-width")?, + max_width: f32_opt(&map, "max-width")?, + resizable: bool_opt(&map, "resizable")?, + + padding: f32_opt(&map, "padding")?, + padding_l: f32_opt(&map, "padding-left")?, + padding_r: f32_opt(&map, "padding-right")?, + padding_t: f32_opt(&map, "padding-top")?, + padding_b: f32_opt(&map, "padding-bottom")?, + + margin: f32_opt(&map, "margin")?, + margin_l: f32_opt(&map, "margin-left")?, + margin_r: f32_opt(&map, "margin-right")?, + margin_t: f32_opt(&map, "margin-top")?, + margin_b: f32_opt(&map, "margin-bottom")?, + }) + } +} diff --git a/efx/src/tags/top_panel.rs b/efx/src/tags/top_panel.rs new file mode 100644 index 0000000..1bef961 --- /dev/null +++ b/efx/src/tags/top_panel.rs @@ -0,0 +1,179 @@ +use crate::tags::{Tag, TagAttributes}; +use crate::utils::attr::*; +use crate::utils::render::render_children_stmt; +use efx_attrnames::AttrNames; +use efx_core::Element; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; + +pub struct TopPanel { + attributes: Attributes, + element: Element, +} + +impl Tag for TopPanel { + fn from_element(el: &Element) -> Result + where + Self: Sized, + { + Ok(Self { + attributes: Attributes::new(el)?, + element: el.clone(), + }) + } + + fn content(&self, _ui: &UI) -> TokenStream { + let mut ts = TokenStream::new(); + + // default / none + ts.extend(match self.attributes.frame { + Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), + _ => quote!( let mut __efx_frame = egui::Frame::default(); ), + }); + + if let Some(fill) = &self.attributes.fill { + ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); + } + if let Some(im) = self.attributes.padding_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); + } + if let Some(om) = self.attributes.margin_ts() { + ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + } + if let Some(st) = stroke_tokens( + self.attributes.stroke_width, + self.attributes.stroke_color.clone(), + ) { + ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); + } + + ts + } + + fn render(&self, ui: &UI) -> TokenStream { + let id = match &self.attributes.id { + Some(s) if !s.is_empty() => s, + _ => { + let msg = "efx: requires non-empty `id` attribute"; + return quote! { compile_error!(#msg); }; + } + }; + + let children = render_children_stmt("e!(ui), &self.element.children); + let frame_ts = self.content(ui); + + let mut panel_ts = + quote!( let mut __efx_panel = egui::TopBottomPanel::top(#id).frame(__efx_frame); ); + if let Some(b) = self.attributes.resizable { + panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); + } + if let Some(v) = self.attributes.default_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.default_height(#v as f32); )); + } + if let Some(v) = self.attributes.min_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.min_height(#v as f32); )); + } + if let Some(v) = self.attributes.max_height { + panel_ts.extend(quote!( __efx_panel = __efx_panel.max_height(#v as f32); )); + } + + quote! {{ + #frame_ts + #panel_ts + __efx_panel.show(&#ui.ctx(), |ui| { #children }); + }} + } +} + +#[derive(Clone, Debug, AttrNames)] +struct Attributes { + /// required: egui Id salt + id: Option, + + // frame + styling + frame: Option, + fill: Option, + #[attr(name = "stroke-width")] + stroke_width: Option, + #[attr(name = "stroke-color")] + stroke_color: Option, + + // sizing + #[attr(name = "default-height")] + default_height: Option, + #[attr(name = "min-height")] + min_height: Option, + #[attr(name = "max-height")] + max_height: Option, + resizable: Option, + + // padding (inner_margin) + padding: Option, + #[attr(name = "padding-left")] + padding_l: Option, + #[attr(name = "padding-right")] + padding_r: Option, + #[attr(name = "padding-top")] + padding_t: Option, + #[attr(name = "padding-bottom")] + padding_b: Option, + + // margin (outer_margin) + margin: Option, + #[attr(name = "margin-left")] + margin_l: Option, + #[attr(name = "margin-right")] + margin_r: Option, + #[attr(name = "margin-top")] + margin_t: Option, + #[attr(name = "margin-bottom")] + margin_b: Option, +} + +impl Attributes { + fn padding_ts(&self) -> Option { + margin_tokens( + self.padding, + self.padding_l, + self.padding_r, + self.padding_t, + self.padding_b, + ) + } + fn margin_ts(&self) -> Option { + margin_tokens( + self.margin, + self.margin_l, + self.margin_r, + self.margin_t, + self.margin_b, + ) + } +} + +impl TagAttributes for Attributes { + fn new(el: &Element) -> Result { + let map = attr_map(el, Attributes::ATTR_NAMES, "TopPanel")?; + Ok(Attributes { + id: map.get("id").map(|s| (*s).to_string()), + frame: bool_opt(&map, "frame")?, + fill: color_tokens_opt(&map, "fill")?, + stroke_width: f32_opt(&map, "stroke-width")?, + stroke_color: color_tokens_opt(&map, "stroke-color")?, + default_height: f32_opt(&map, "default-height")?, + min_height: f32_opt(&map, "min-height")?, + max_height: f32_opt(&map, "max-height")?, + resizable: bool_opt(&map, "resizable")?, + padding: f32_opt(&map, "padding")?, + padding_l: f32_opt(&map, "padding-left")?, + padding_r: f32_opt(&map, "padding-right")?, + padding_t: f32_opt(&map, "padding-top")?, + padding_b: f32_opt(&map, "padding-bottom")?, + margin: f32_opt(&map, "margin")?, + margin_l: f32_opt(&map, "margin-left")?, + margin_r: f32_opt(&map, "margin-right")?, + margin_t: f32_opt(&map, "margin-top")?, + margin_b: f32_opt(&map, "margin-bottom")?, + }) + } +} From 68597d002d5f76540b43255bb3d172905699f211 Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Sun, 31 Aug 2025 21:26:51 +0200 Subject: [PATCH 04/29] Refactoring panel tags, added panel utils --- efx-sandbox/src/main.rs | 23 ++++++ efx/docs/tags.md | 2 +- efx/src/tags/bottom_panel.rs | 113 +++++++++++++--------------- efx/src/tags/central_panel.rs | 88 +++++++--------------- efx/src/tags/mod.rs | 6 +- efx/src/tags/side_panel.rs | 134 ++++++++++++---------------------- efx/src/tags/top_panel.rs | 98 ++++++++----------------- efx/src/utils/mod.rs | 1 + efx/src/utils/panel.rs | 103 ++++++++++++++++++++++++++ 9 files changed, 288 insertions(+), 280 deletions(-) create mode 100644 efx/src/utils/panel.rs diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 64b65a1..2698340 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -86,6 +86,29 @@ impl eframe::App for App { "##); + + let _ = efx!(ui, r##" + + + + + + + + + + "##); + + let _ = efx!(ui, r##" + + + + + + + + + "##); }); } } diff --git a/efx/docs/tags.md b/efx/docs/tags.md index 4d881c2..9df7158 100644 --- a/efx/docs/tags.md +++ b/efx/docs/tags.md @@ -294,7 +294,7 @@ Great for logs, consoles, timelines, or status bars. **Example** ```xml - + diff --git a/efx/src/tags/bottom_panel.rs b/efx/src/tags/bottom_panel.rs index c240486..ad85b1a 100644 --- a/efx/src/tags/bottom_panel.rs +++ b/efx/src/tags/bottom_panel.rs @@ -1,10 +1,11 @@ -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use efx_attrnames::AttrNames; -use efx_core::Element; use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; +use crate::utils::panel::{Dim, FrameStyle, SizeOpts, emit_size_methods}; use crate::utils::render::render_children_stmt; +use efx_attrnames::AttrNames; +use efx_core::Element; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; pub struct BottomPanel { attributes: Attributes, @@ -12,62 +13,59 @@ pub struct BottomPanel { } impl Tag for BottomPanel { - fn from_element(el: &Element) -> Result - where - Self: Sized - { - Ok(Self { attributes: Attributes::new(el)?, element: el.clone() }) + fn from_element(el: &Element) -> Result { + Ok(Self { + attributes: Attributes::new(el)?, + element: el.clone(), + }) } fn content(&self, _ui: &UI) -> TokenStream { - let mut ts = TokenStream::new(); - - ts.extend(match self.attributes.frame { - Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), - _ => quote!( let mut __efx_frame = egui::Frame::default(); ), - }); - - if let Some(fill) = &self.attributes.fill { - ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); - } - if let Some(im) = self.attributes.padding_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); - } - if let Some(om) = self.attributes.margin_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + FrameStyle { + frame_on: self.attributes.frame, + fill: self.attributes.fill.clone(), + stroke_w: self.attributes.stroke_width, + stroke_color: self.attributes.stroke_color.clone(), + + // padding + pad: self.attributes.padding, + pad_l: self.attributes.padding_l, + pad_r: self.attributes.padding_r, + pad_t: self.attributes.padding_t, + pad_b: self.attributes.padding_b, + + // margin + mar: self.attributes.margin, + mar_l: self.attributes.margin_l, + mar_r: self.attributes.margin_r, + mar_t: self.attributes.margin_t, + mar_b: self.attributes.margin_b, } - if let Some(st) = stroke_tokens(self.attributes.stroke_width, self.attributes.stroke_color.clone()) { - ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); - } - - ts + .emit() } fn render(&self, ui: &UI) -> TokenStream { let id = match &self.attributes.id { Some(s) if !s.is_empty() => s, _ => { - let msg = "efx: requires non-empty `id` attribute"; - return quote! { compile_error!(#msg); }; + return quote! { compile_error!("efx: requires non-empty `id` attribute"); }; } }; let children = render_children_stmt("e!(ui), &self.element.children); let frame_ts = self.content(ui); - let mut panel_ts = quote!( let mut __efx_panel = egui::TopBottomPanel::bottom(#id).frame(__efx_frame); ); - if let Some(b) = self.attributes.resizable { - panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); - } - if let Some(v) = self.attributes.default_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.default_height(#v as f32); )); - } - if let Some(v) = self.attributes.min_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.min_height(#v as f32); )); - } - if let Some(v) = self.attributes.max_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.max_height(#v as f32); )); - } + let mut panel_ts = + quote!( let mut __efx_panel = egui::TopBottomPanel::bottom(#id).frame(__efx_frame); ); + panel_ts.extend(emit_size_methods( + Dim::Height, + &SizeOpts { + resizable: self.attributes.resizable, + default: self.attributes.default_height, + min: self.attributes.min_height, + max: self.attributes.max_height, + }, + )); quote! {{ #frame_ts @@ -117,15 +115,6 @@ struct Attributes { margin_b: Option, } -impl Attributes { - fn padding_ts(&self) -> Option { - margin_tokens(self.padding, self.padding_l, self.padding_r, self.padding_t, self.padding_b) - } - fn margin_ts(&self) -> Option { - margin_tokens(self.margin, self.margin_l, self.margin_r, self.margin_t, self.margin_b) - } -} - impl TagAttributes for Attributes { fn new(el: &Element) -> Result { let map = attr_map(el, Attributes::ATTR_NAMES, "BottomPanel")?; @@ -136,19 +125,19 @@ impl TagAttributes for Attributes { stroke_width: f32_opt(&map, "stroke-width")?, stroke_color: color_tokens_opt(&map, "stroke-color")?, default_height: f32_opt(&map, "default-height")?, - min_height: f32_opt(&map, "min-height")?, - max_height: f32_opt(&map, "max-height")?, - resizable: bool_opt(&map, "resizable")?, - padding: f32_opt(&map, "padding")?, + min_height: f32_opt(&map, "min-height")?, + max_height: f32_opt(&map, "max-height")?, + resizable: bool_opt(&map, "resizable")?, + padding: f32_opt(&map, "padding")?, padding_l: f32_opt(&map, "padding-left")?, padding_r: f32_opt(&map, "padding-right")?, padding_t: f32_opt(&map, "padding-top")?, padding_b: f32_opt(&map, "padding-bottom")?, - margin: f32_opt(&map, "margin")?, - margin_l: f32_opt(&map, "margin-left")?, - margin_r: f32_opt(&map, "margin-right")?, - margin_t: f32_opt(&map, "margin-top")?, - margin_b: f32_opt(&map, "margin-bottom")?, + margin: f32_opt(&map, "margin")?, + margin_l: f32_opt(&map, "margin-left")?, + margin_r: f32_opt(&map, "margin-right")?, + margin_t: f32_opt(&map, "margin-top")?, + margin_b: f32_opt(&map, "margin-bottom")?, }) } } diff --git a/efx/src/tags/central_panel.rs b/efx/src/tags/central_panel.rs index c3aaa7e..3eaed1a 100644 --- a/efx/src/tags/central_panel.rs +++ b/efx/src/tags/central_panel.rs @@ -1,5 +1,6 @@ use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; +use crate::utils::panel::FrameStyle; use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; @@ -12,57 +13,46 @@ pub struct CentralPanel { } impl Tag for CentralPanel { - fn from_element(el: &Element) -> Result - where - Self: Sized, - { - let attributes = Attributes::new(el)?; + fn from_element(el: &Element) -> Result { Ok(Self { - attributes, + attributes: Attributes::new(el)?, element: el.clone(), }) } fn content(&self, _ui: &UI) -> TokenStream { - // Assembling an expression for Frame - let mut frame_build = TokenStream::new(); - - // main frame: true/default → default(); false → none(); - frame_build.extend(match self.attributes.frame { - Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), - _ => quote!( let mut __efx_frame = egui::Frame::default(); ), - }); - - if let Some(ts) = self.attributes.fill.clone() { - frame_build.extend(quote!( __efx_frame = __efx_frame.fill(#ts); )); - } - if let Some(im) = self.attributes.padding_ts() { - frame_build.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); + FrameStyle { + frame_on: self.attributes.frame, + fill: self.attributes.fill.clone(), + stroke_w: self.attributes.stroke_width, + stroke_color: self.attributes.stroke_color.clone(), + + // padding (inner) + pad: self.attributes.padding, + pad_l: self.attributes.padding_l, + pad_r: self.attributes.padding_r, + pad_t: self.attributes.padding_t, + pad_b: self.attributes.padding_b, + + // margin (outer) + mar: self.attributes.margin, + mar_l: self.attributes.margin_l, + mar_r: self.attributes.margin_r, + mar_t: self.attributes.margin_t, + mar_b: self.attributes.margin_b, } - if let Some(om) = self.attributes.margin_ts() { - frame_build.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); - } - if let Some(st) = stroke_tokens( - self.attributes.stroke_width.clone(), - self.attributes.stroke_color.clone(), - ) { - frame_build.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); - } - - frame_build + .emit() } fn render(&self, ui: &UI) -> TokenStream { - // Generate children let children = render_children_stmt("e!(ui), &self.element.children); - // Building Frame - let frame_build = self.content(ui); + let frame_ts = self.content(ui); quote! {{ - #frame_build + #frame_ts egui::CentralPanel::default() - .frame(__efx_frame) - .show(&#ui.ctx(), |ui| { #children }); + .frame(__efx_frame) + .show(&#ui.ctx(), |ui| { #children }); }} } } @@ -99,30 +89,6 @@ struct Attributes { margin_b: Option, } -impl Attributes { - // Generate expressions for Padding - fn padding_ts(&self) -> Option { - margin_tokens( - self.padding, - self.padding_l, - self.padding_r, - self.padding_t, - self.padding_b, - ) - } - - // Generate expressions for Margin - fn margin_ts(&self) -> Option { - margin_tokens( - self.margin, - self.margin_l, - self.margin_r, - self.margin_t, - self.margin_b, - ) - } -} - impl TagAttributes for Attributes { fn new(el: &Element) -> Result { let map = match attr_map(el, Attributes::ATTR_NAMES, "CentralPanel") { diff --git a/efx/src/tags/mod.rs b/efx/src/tags/mod.rs index d74e3db..e245acb 100644 --- a/efx/src/tags/mod.rs +++ b/efx/src/tags/mod.rs @@ -1,3 +1,4 @@ +pub mod bottom_panel; pub mod button; pub mod central_panel; pub mod column; @@ -11,8 +12,8 @@ pub mod side_panel; pub mod text_field; pub mod top_panel; pub mod window; -pub mod bottom_panel; +pub use bottom_panel::BottomPanel; pub use button::Button; pub use central_panel::CentralPanel; pub use column::Column; @@ -26,9 +27,8 @@ pub use row::Row; pub use scroll_area::ScrollArea; pub use separator::Separator; pub use side_panel::SidePanel; -pub use top_panel::TopPanel; pub use text_field::TextField; -pub use bottom_panel::BottomPanel; +pub use top_panel::TopPanel; pub trait Tag: Sized { /// Constructor from Element (parses attributes and captures children inside self). diff --git a/efx/src/tags/side_panel.rs b/efx/src/tags/side_panel.rs index 5d3202a..32cd6d8 100644 --- a/efx/src/tags/side_panel.rs +++ b/efx/src/tags/side_panel.rs @@ -1,5 +1,6 @@ use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; +use crate::utils::panel::{Dim, FrameStyle, SizeOpts, emit_size_methods}; use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; @@ -12,51 +13,48 @@ pub struct SidePanel { } impl Tag for SidePanel { - fn from_element(el: &Element) -> Result - where - Self: Sized, - { - let attributes = Attributes::new(el)?; + fn from_element(el: &Element) -> Result { Ok(Self { - attributes, + attributes: Attributes::new(el)?, element: el.clone(), }) } fn content(&self, _ui: &UI) -> TokenStream { - let mut ts = TokenStream::new(); - - // default frame or none - ts.extend(match self.attributes.frame { - Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), - _ => quote!( let mut __efx_frame = egui::Frame::default(); ), - }); - - if let Some(fill) = &self.attributes.fill { - ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); - } - if let Some(im) = self.attributes.padding_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); - } - if let Some(om) = self.attributes.margin_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); - } - if let Some(st) = stroke_tokens( - self.attributes.stroke_width, - self.attributes.stroke_color.clone(), - ) { - ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); + FrameStyle { + frame_on: self.attributes.frame, + fill: self.attributes.fill.clone(), + stroke_w: self.attributes.stroke_width, + stroke_color: self.attributes.stroke_color.clone(), + + // padding (inner) + pad: self.attributes.padding, + pad_l: self.attributes.padding_l, + pad_r: self.attributes.padding_r, + pad_t: self.attributes.padding_t, + pad_b: self.attributes.padding_b, + + // margin (outer) + mar: self.attributes.margin, + mar_l: self.attributes.margin_l, + mar_r: self.attributes.margin_r, + mar_t: self.attributes.margin_t, + mar_b: self.attributes.margin_b, } - - ts + .emit() } fn render(&self, ui: &UI) -> TokenStream { - let children = render_children_stmt("e!(ui), &self.element.children); - let frame_ts = self.content(ui); + let id = match &self.attributes.id { + Some(s) if !s.is_empty() => s, + _ => { + return quote! { compile_error!("efx: requires non-empty `id` attribute"); }; + } + }; - let side = match self.attributes.side.as_deref() { - Some(s @ ("left" | "right")) => s, + let side_ctor = match self.attributes.side.as_deref() { + Some("left") => quote!( egui::SidePanel::left(#id) ), + Some("right") => quote!( egui::SidePanel::right(#id) ), Some(other) => { let msg = format!( "efx: `side` must be `left` or `right`, got `{}`", @@ -65,40 +63,23 @@ impl Tag for SidePanel { return quote! { compile_error!(#msg); }; } None => { - let msg = "efx: requires `side` attribute (`left` | `right`)"; - return quote! { compile_error!(#msg); }; - } - }; - let id = match &self.attributes.id { - Some(s) if !s.is_empty() => s, - _ => { - let msg = "efx: requires non-empty `id` attribute"; - return quote! { compile_error!(#msg); }; + return quote! { compile_error!("efx: requires `side` attribute (`left`|`right`)"); }; } }; - let mut panel_ts = TokenStream::new(); - match side { - "left" => panel_ts.extend(quote!( let mut __efx_panel = egui::SidePanel::left(#id); )), - "right" => { - panel_ts.extend(quote!( let mut __efx_panel = egui::SidePanel::right(#id); )) - } - _ => {} - } - panel_ts.extend(quote!( __efx_panel = __efx_panel.frame(__efx_frame); )); + let children = render_children_stmt("e!(ui), &self.element.children); + let frame_ts = self.content(ui); - if let Some(b) = self.attributes.resizable { - panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); - } - if let Some(v) = self.attributes.default_width { - panel_ts.extend(quote!( __efx_panel = __efx_panel.default_width(#v as f32); )); - } - if let Some(v) = self.attributes.min_width { - panel_ts.extend(quote!( __efx_panel = __efx_panel.min_width(#v as f32); )); - } - if let Some(v) = self.attributes.max_width { - panel_ts.extend(quote!( __efx_panel = __efx_panel.max_width(#v as f32); )); - } + let mut panel_ts = quote!( let mut __efx_panel = #side_ctor.frame(__efx_frame); ); + panel_ts.extend(emit_size_methods( + Dim::Width, + &SizeOpts { + resizable: self.attributes.resizable, + default: self.attributes.default_width, + min: self.attributes.min_width, + max: self.attributes.max_width, + }, + )); quote! {{ #frame_ts @@ -110,10 +91,10 @@ impl Tag for SidePanel { #[derive(Clone, Debug, AttrNames)] struct Attributes { - /// required: left | right - side: Option, /// required: egui Id salt id: Option, + /// required: left | right + side: Option, // frame on/off + styling frame: Option, @@ -155,27 +136,6 @@ struct Attributes { margin_b: Option, } -impl Attributes { - fn padding_ts(&self) -> Option { - margin_tokens( - self.padding, - self.padding_l, - self.padding_r, - self.padding_t, - self.padding_b, - ) - } - fn margin_ts(&self) -> Option { - margin_tokens( - self.margin, - self.margin_l, - self.margin_r, - self.margin_t, - self.margin_b, - ) - } -} - impl TagAttributes for Attributes { fn new(el: &Element) -> Result { let map = attr_map(el, Attributes::ATTR_NAMES, "SidePanel")?; diff --git a/efx/src/tags/top_panel.rs b/efx/src/tags/top_panel.rs index 1bef961..760e2ee 100644 --- a/efx/src/tags/top_panel.rs +++ b/efx/src/tags/top_panel.rs @@ -1,5 +1,6 @@ use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; +use crate::utils::panel::*; use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; @@ -12,10 +13,7 @@ pub struct TopPanel { } impl Tag for TopPanel { - fn from_element(el: &Element) -> Result - where - Self: Sized, - { + fn from_element(el: &Element) -> Result { Ok(Self { attributes: Attributes::new(el)?, element: el.clone(), @@ -23,59 +21,48 @@ impl Tag for TopPanel { } fn content(&self, _ui: &UI) -> TokenStream { - let mut ts = TokenStream::new(); - - // default / none - ts.extend(match self.attributes.frame { - Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), - _ => quote!( let mut __efx_frame = egui::Frame::default(); ), - }); - - if let Some(fill) = &self.attributes.fill { - ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); - } - if let Some(im) = self.attributes.padding_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); - } - if let Some(om) = self.attributes.margin_ts() { - ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + FrameStyle { + frame_on: self.attributes.frame, + fill: self.attributes.fill.clone(), + stroke_w: self.attributes.stroke_width, + stroke_color: self.attributes.stroke_color.clone(), + + // padding (inner) + pad: self.attributes.padding, + pad_l: self.attributes.padding_l, + pad_r: self.attributes.padding_r, + pad_t: self.attributes.padding_t, + pad_b: self.attributes.padding_b, + + // margin (outer) + mar: self.attributes.margin, + mar_l: self.attributes.margin_l, + mar_r: self.attributes.margin_r, + mar_t: self.attributes.margin_t, + mar_b: self.attributes.margin_b, } - if let Some(st) = stroke_tokens( - self.attributes.stroke_width, - self.attributes.stroke_color.clone(), - ) { - ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); - } - - ts + .emit() } fn render(&self, ui: &UI) -> TokenStream { let id = match &self.attributes.id { Some(s) if !s.is_empty() => s, - _ => { - let msg = "efx: requires non-empty `id` attribute"; - return quote! { compile_error!(#msg); }; - } + _ => return quote! { compile_error!("efx: requires non-empty `id`"); }, }; - let children = render_children_stmt("e!(ui), &self.element.children); let frame_ts = self.content(ui); let mut panel_ts = quote!( let mut __efx_panel = egui::TopBottomPanel::top(#id).frame(__efx_frame); ); - if let Some(b) = self.attributes.resizable { - panel_ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); - } - if let Some(v) = self.attributes.default_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.default_height(#v as f32); )); - } - if let Some(v) = self.attributes.min_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.min_height(#v as f32); )); - } - if let Some(v) = self.attributes.max_height { - panel_ts.extend(quote!( __efx_panel = __efx_panel.max_height(#v as f32); )); - } + panel_ts.extend(emit_size_methods( + Dim::Height, + &SizeOpts { + resizable: self.attributes.resizable, + default: self.attributes.default_height, + min: self.attributes.min_height, + max: self.attributes.max_height, + }, + )); quote! {{ #frame_ts @@ -130,27 +117,6 @@ struct Attributes { margin_b: Option, } -impl Attributes { - fn padding_ts(&self) -> Option { - margin_tokens( - self.padding, - self.padding_l, - self.padding_r, - self.padding_t, - self.padding_b, - ) - } - fn margin_ts(&self) -> Option { - margin_tokens( - self.margin, - self.margin_l, - self.margin_r, - self.margin_t, - self.margin_b, - ) - } -} - impl TagAttributes for Attributes { fn new(el: &Element) -> Result { let map = attr_map(el, Attributes::ATTR_NAMES, "TopPanel")?; diff --git a/efx/src/utils/mod.rs b/efx/src/utils/mod.rs index a17d413..e7ed9eb 100644 --- a/efx/src/utils/mod.rs +++ b/efx/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod attr; pub mod buffer; pub mod expr; +pub mod panel; pub mod render; diff --git a/efx/src/utils/panel.rs b/efx/src/utils/panel.rs new file mode 100644 index 0000000..327c171 --- /dev/null +++ b/efx/src/utils/panel.rs @@ -0,0 +1,103 @@ +use crate::utils::attr::*; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +#[derive(Copy, Clone, Debug)] +pub enum Dim { + Width, + Height, +} + +/// Set of general options for the panel frame. +#[derive(Clone, Debug)] +pub struct FrameStyle { + pub frame_on: Option, // true/None => default(), false => none() + pub fill: Option, // Color32 expression + pub stroke_w: Option, // points + pub stroke_color: Option, // Color32 expression + + // padding (inner_margin) + pub pad: Option, + pub pad_l: Option, + pub pad_r: Option, + pub pad_t: Option, + pub pad_b: Option, + + // margin (outer_margin) + pub mar: Option, + pub mar_l: Option, + pub mar_r: Option, + pub mar_t: Option, + pub mar_b: Option, +} + +impl FrameStyle { + /// Generate tokens with `let mut __efx_frame = ...;` construction and all modifiers. + pub fn emit(&self) -> TokenStream { + let mut ts = TokenStream::new(); + + // default/none + ts.extend(match self.frame_on { + Some(false) => quote!( let mut __efx_frame = egui::Frame::none(); ), + _ => quote!( let mut __efx_frame = egui::Frame::default(); ), + }); + + if let Some(fill) = &self.fill { + ts.extend(quote!( __efx_frame = __efx_frame.fill(#fill); )); + } + + if let Some(im) = margin_tokens(self.pad, self.pad_l, self.pad_r, self.pad_t, self.pad_b) { + ts.extend(quote!( __efx_frame = __efx_frame.inner_margin(#im); )); + } + if let Some(om) = margin_tokens(self.mar, self.mar_l, self.mar_r, self.mar_t, self.mar_b) { + ts.extend(quote!( __efx_frame = __efx_frame.outer_margin(#om); )); + } + + if let Some(st) = stroke_tokens(self.stroke_w, self.stroke_color.clone()) { + ts.extend(quote!( __efx_frame = __efx_frame.stroke(#st); )); + } + + ts + } +} + +/// General options for panel size and resizing. +#[derive(Clone, Debug, Default)] +pub struct SizeOpts { + pub resizable: Option, + pub default: Option, + pub min: Option, + pub max: Option, +} + +/// Apply default_/min_/max_ methods depending on the dimension. +/// Assumes the panel variable is named `__efx_panel`. +pub fn emit_size_methods(dim: Dim, s: &SizeOpts) -> TokenStream { + let (def_m, min_m, max_m) = match dim { + Dim::Width => ( + format_ident!("default_width"), + format_ident!("min_width"), + format_ident!("max_width"), + ), + Dim::Height => ( + format_ident!("default_height"), + format_ident!("min_height"), + format_ident!("max_height"), + ), + }; + + let mut ts = TokenStream::new(); + if let Some(b) = s.resizable { + ts.extend(quote!( __efx_panel = __efx_panel.resizable(#b); )); + } + if let Some(v) = s.default { + ts.extend(quote!( __efx_panel = __efx_panel.#def_m(#v as f32); )); + } + if let Some(v) = s.min { + ts.extend(quote!( __efx_panel = __efx_panel.#min_m(#v as f32); )); + } + if let Some(v) = s.max { + ts.extend(quote!( __efx_panel = __efx_panel.#max_m(#v as f32); )); + } + ts +} From 2f5b556cf61053246fcdc9adfbcf678bf89fc627 Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Sun, 31 Aug 2025 22:59:24 +0200 Subject: [PATCH 05/29] Added Resize tag, refactoring panel tags --- efx-sandbox/src/main.rs | 14 ++++ efx/docs/tags.md | 35 ++++++++++ efx/src/render.rs | 1 + efx/src/tags/bottom_panel.rs | 93 +-------------------------- efx/src/tags/central_panel.rs | 88 +------------------------- efx/src/tags/mod.rs | 2 + efx/src/tags/resize.rs | 116 ++++++++++++++++++++++++++++++++++ efx/src/tags/side_panel.rs | 4 +- efx/src/tags/top_panel.rs | 96 +--------------------------- efx/src/utils/panel.rs | 104 ++++++++++++++++++++++++++++++ 10 files changed, 280 insertions(+), 273 deletions(-) create mode 100644 efx/src/tags/resize.rs diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 2698340..b4919b9 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -109,6 +109,20 @@ impl eframe::App for App { "##); + + // Resize + let _ = efx!(ui, r##" + + + + + + + + + + + "##); }); } } diff --git a/efx/docs/tags.md b/efx/docs/tags.md index 9df7158..699a50d 100644 --- a/efx/docs/tags.md +++ b/efx/docs/tags.md @@ -357,6 +357,41 @@ efx!(Ui::default(), r#" "#); ``` +### `` + +A resizable container that lets the user drag a handle to change the size of its content. +Useful for side views, inspectors, consoles, etc., when a full docked panel is too heavy. + +**Children:** rendered inside the resizable area. + +**Required attributes** +- `id="string"` — egui `Id` salt to persist the size across frames. + +**Behavior** +- `resizable="true|false"` — enable/disable user resizing (default: `true` in egui). +- `clip="true|false"` — clip the content to the current size (optional). + +**Sizing** +- `default-width="number"`, `default-height="number"` — initial size. +- `min-width="number"`, `min-height="number"` — lower bounds. +- `max-width="number"`, `max-height="number"` — upper bounds. + +> Each dimension is optional. If only one dimension is provided, the other falls back to `0.0` (for min/default) or `∞` (for max). + +**Example** +```xml + + + + + + + + + + +``` + ### `Heading` Text heading. Generates `ui.heading(text)` with optional style overrides. diff --git a/efx/src/render.rs b/efx/src/render.rs index 3c1ee08..daa6325 100644 --- a/efx/src/render.rs +++ b/efx/src/render.rs @@ -45,6 +45,7 @@ fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { "ScrollArea" => render_tag::(ui, el), "Row" => render_tag::(ui, el), "Column" => render_tag::(ui, el), + "Resize" => render_tag::(ui, el), "Label" => render_tag:: + + +``` + ### `Column` Vertical container. Generates `ui.vertical(|ui| { ... })`. diff --git a/efx/src/render.rs b/efx/src/render.rs index daa6325..fa0e478 100644 --- a/efx/src/render.rs +++ b/efx/src/render.rs @@ -37,6 +37,7 @@ pub(crate) fn render_node_stmt(ui: &UI, node: &Node) -> TokenStrea fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { match el.name.as_str() { + "Window" => render_tag::(ui, el), "Heading" => render_tag::(ui, el), "CentralPanel" => render_tag::(ui, el), "SidePanel" => render_tag::(ui, el), diff --git a/efx/src/tags/bottom_panel.rs b/efx/src/tags/bottom_panel.rs index 3ebd0ad..31025fd 100644 --- a/efx/src/tags/bottom_panel.rs +++ b/efx/src/tags/bottom_panel.rs @@ -25,9 +25,7 @@ impl Tag for BottomPanel { fn render(&self, ui: &UI) -> TokenStream { let id = match &self.attributes.id { Some(s) if !s.is_empty() => s, - _ => { - return quote! { compile_error!("efx: requires non-empty `id` attribute"); }; - } + _ => return quote! { compile_error!("efx: requires non-empty `id`"); }, }; let children = render_children_stmt("e!(ui), &self.element.children); diff --git a/efx/src/tags/mod.rs b/efx/src/tags/mod.rs index d9cfde1..eeee71a 100644 --- a/efx/src/tags/mod.rs +++ b/efx/src/tags/mod.rs @@ -31,6 +31,7 @@ pub use separator::Separator; pub use side_panel::SidePanel; pub use text_field::TextField; pub use top_panel::TopPanel; +pub use window::Window; pub trait Tag: Sized { /// Constructor from Element (parses attributes and captures children inside self). diff --git a/efx/src/tags/top_panel.rs b/efx/src/tags/top_panel.rs index 7480ba3..38b830d 100644 --- a/efx/src/tags/top_panel.rs +++ b/efx/src/tags/top_panel.rs @@ -27,6 +27,7 @@ impl Tag for TopPanel { Some(s) if !s.is_empty() => s, _ => return quote! { compile_error!("efx: requires non-empty `id`"); }, }; + let children = render_children_stmt("e!(ui), &self.element.children); let frame_ts = self.content(ui); diff --git a/efx/src/tags/window.rs b/efx/src/tags/window.rs index 8b13789..eda3f25 100644 --- a/efx/src/tags/window.rs +++ b/efx/src/tags/window.rs @@ -1 +1,314 @@ +use crate::tags::{Tag, TagAttributes}; +use crate::utils::attr::*; +use crate::utils::panel::*; +use crate::utils::render::render_children_stmt; +use efx_attrnames::AttrNames; +use efx_core::Element; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use crate::utils::expr::expr_opt; +pub struct Window { + attributes: Attributes, + element: Element, +} + +impl Tag for Window { + fn from_element(el: &Element) -> Result { + Ok(Self { + attributes: Attributes::new(el)?, + element: el.clone(), + }) + } + + /// Build __efx_frame (fill/padding/margin/stroke) + fn content(&self, _ui: &UI) -> TokenStream { + FrameStyle { + frame_on: self.attributes.frame, + fill: self.attributes.fill.clone(), + stroke_w: self.attributes.stroke_width, + stroke_color: self.attributes.stroke_color.clone(), + + // padding + pad: self.attributes.padding, + pad_l: self.attributes.padding_l, + pad_r: self.attributes.padding_r, + pad_t: self.attributes.padding_t, + pad_b: self.attributes.padding_b, + + // margin + mar: self.attributes.margin, + mar_l: self.attributes.margin_l, + mar_r: self.attributes.margin_r, + mar_t: self.attributes.margin_t, + mar_b: self.attributes.margin_b, + } + .emit() + } + + fn render(&self, ui: &UI) -> TokenStream { + let title = match &self.attributes.title { + Some(s) if !s.is_empty() => s, + _ => { + return quote! { compile_error!("efx: requires non-empty `title` attribute"); }; + } + }; + + let children = render_children_stmt("e!(ui), &self.element.children); + let frame_ts = self.content(ui); + + let mut win = + quote!( let mut __efx_window = egui::Window::new(#title).frame(__efx_frame); ); + + // id + if let Some(id) = &self.attributes.id { + win.extend(quote!( __efx_window = __efx_window.id(egui::Id::new(#id)); )); + } + + // behavior + if let Some(b) = self.attributes.movable { + win.extend(quote!( __efx_window = __efx_window.movable(#b); )); + } + if let Some(b) = self.attributes.resizable { + win.extend(quote!( __efx_window = __efx_window.resizable(#b); )); + } + if let Some(b) = self.attributes.collapsible { + win.extend(quote!( __efx_window = __efx_window.collapsible(#b); )); + } + if let Some(b) = self.attributes.title_bar { + win.extend(quote!( __efx_window = __efx_window.title_bar(#b); )); + } + if let Some(b) = self.attributes.enabled { + win.extend(quote!( __efx_window = __efx_window.enabled(#b); )); + } + if let Some(b) = self.attributes.constrain { + win.extend(quote!( __efx_window = __efx_window.constrain(#b); )); + } + if self.attributes.auto_sized == Some(true) { + win.extend(quote!( __efx_window = __efx_window.auto_sized(); )); + } + + // geometry: default_pos / current_pos + if self.attributes.default_x.is_some() || self.attributes.default_y.is_some() { + let x = self.attributes.default_x.unwrap_or(0.0); + let y = self.attributes.default_y.unwrap_or(0.0); + win.extend(quote!( __efx_window = __efx_window.default_pos(egui::pos2(#x as f32, #y as f32)); )); + } + if self.attributes.pos_x.is_some() || self.attributes.pos_y.is_some() { + let x = self.attributes.pos_x.unwrap_or(0.0); + let y = self.attributes.pos_y.unwrap_or(0.0); + win.extend(quote!( __efx_window = __efx_window.current_pos(egui::pos2(#x as f32, #y as f32)); )); + } + + // size: default/min/max size + if self.attributes.default_width.is_some() || self.attributes.default_height.is_some() { + let w = self.attributes.default_width.unwrap_or(0.0); + let h = self.attributes.default_height.unwrap_or(0.0); + win.extend(quote!( __efx_window = __efx_window.default_size(egui::vec2(#w as f32, #h as f32)); )); + } + if self.attributes.min_width.is_some() || self.attributes.min_height.is_some() { + let w = self.attributes.min_width.unwrap_or(0.0); + let h = self.attributes.min_height.unwrap_or(0.0); + win.extend( + quote!( __efx_window = __efx_window.min_size(egui::vec2(#w as f32, #h as f32)); ), + ); + } + if self.attributes.max_width.is_some() || self.attributes.max_height.is_some() { + let w = self.attributes.max_width.unwrap_or(f32::INFINITY); + let h = self.attributes.max_height.unwrap_or(f32::INFINITY); + win.extend( + quote!( __efx_window = __efx_window.max_size(egui::vec2(#w as f32, #h as f32)); ), + ); + } + + // anchor (Align2 + offset) + if self.attributes.anchor_h.is_some() || self.attributes.anchor_v.is_some() { + let h = self.attributes.anchor_h.clone().unwrap_or_else(|| "center".to_string()); + let v = self.attributes.anchor_v.clone().unwrap_or_else(|| "center".to_string()); + + let h_align = match h.as_str() { + "left" => quote!( egui::Align::Min ), + "center" => quote!( egui::Align::Center ), + "right" => quote!( egui::Align::Max ), + other => { + let msg = format!("efx: `anchor-h` expected left|center|right, got `{}`", other); + return quote! { compile_error!(#msg); }; + } + }; + let v_align = match v.as_str() { + "top" => quote!( egui::Align::Min ), + "center" => quote!( egui::Align::Center ), + "bottom" => quote!( egui::Align::Max ), + other => { + let msg = format!("efx: `anchor-v` expected top|center|bottom, got `{}`", other); + return quote! { compile_error!(#msg); }; + } + }; + + let ax = self.attributes.anchor_x.unwrap_or(0.0); + let ay = self.attributes.anchor_y.unwrap_or(0.0); + + win.extend(quote!( + __efx_window = __efx_window.anchor(egui::Align2(#h_align, #v_align), egui::vec2(#ax as f32, #ay as f32)); + )); + } + + let open_bind = if let Some(expr) = &self.attributes.open_expr { + quote! { + let mut __efx_open = (#expr); + __efx_window = __efx_window.open(&mut __efx_open); + #expr = __efx_open; + } + } else { + quote!() + }; + + quote! {{ + #frame_ts + #win + #open_bind + __efx_window.show(&#ui.ctx(), |ui| { #children }); + }} + } +} + +#[derive(Clone, Debug, AttrNames)] +struct Attributes { + // required + title: Option, + + // optional id + id: Option, + + // behavior + movable: Option, + resizable: Option, + collapsible: Option, + title_bar: Option, + enabled: Option, + constrain: Option, + auto_sized: Option, + + // opening state binding (expression) + #[attr(name = "open")] + open_expr: Option, + + // geometry: positions + #[attr(name = "default-x")] + default_x: Option, + #[attr(name = "default-y")] + default_y: Option, + #[attr(name = "pos-x")] + pos_x: Option, + #[attr(name = "pos-y")] + pos_y: Option, + + // geometry: sizes + #[attr(name = "default-width")] + default_width: Option, + #[attr(name = "default-height")] + default_height: Option, + #[attr(name = "min-width")] + min_width: Option, + #[attr(name = "min-height")] + min_height: Option, + #[attr(name = "max-width")] + max_width: Option, + #[attr(name = "max-height")] + max_height: Option, + + // anchor + #[attr(name = "anchor-h")] + anchor_h: Option, // left|center|right + #[attr(name = "anchor-v")] + anchor_v: Option, // top|center|bottom + #[attr(name = "anchor-x")] + anchor_x: Option, // offset + #[attr(name = "anchor-y")] + anchor_y: Option, + + // frame + style + frame: Option, + fill: Option, + #[attr(name = "stroke-width")] + stroke_width: Option, + #[attr(name = "stroke-color")] + stroke_color: Option, + + // padding + padding: Option, + #[attr(name = "padding-left")] + padding_l: Option, + #[attr(name = "padding-right")] + padding_r: Option, + #[attr(name = "padding-top")] + padding_t: Option, + #[attr(name = "padding-bottom")] + padding_b: Option, + + // margin + margin: Option, + #[attr(name = "margin-left")] + margin_l: Option, + #[attr(name = "margin-right")] + margin_r: Option, + #[attr(name = "margin-top")] + margin_t: Option, + #[attr(name = "margin-bottom")] + margin_b: Option, +} + +impl TagAttributes for Attributes { + fn new(el: &Element) -> Result { + let map = attr_map(el, Self::ATTR_NAMES, "Window")?; + + Ok(Attributes { + title: map.get("title").map(|s| (*s).to_string()), + id: map.get("id").map(|s| (*s).to_string()), + + movable: bool_opt(&map, "movable")?, + resizable: bool_opt(&map, "resizable")?, + collapsible: bool_opt(&map, "collapsible")?, + title_bar: bool_opt(&map, "title-bar")?, + enabled: bool_opt(&map, "enabled")?, + constrain: bool_opt(&map, "constrain")?, + auto_sized: bool_opt(&map, "auto-sized")?, + + open_expr: expr_opt(&map, "open")?, + + default_x: f32_opt(&map, "default-x")?, + default_y: f32_opt(&map, "default-y")?, + pos_x: f32_opt(&map, "pos-x")?, + pos_y: f32_opt(&map, "pos-y")?, + + default_width: f32_opt(&map, "default-width")?, + default_height: f32_opt(&map, "default-height")?, + min_width: f32_opt(&map, "min-width")?, + min_height: f32_opt(&map, "min-height")?, + max_width: f32_opt(&map, "max-width")?, + max_height: f32_opt(&map, "max-height")?, + + anchor_h: map.get("anchor-h").map(|s| (*s).to_string()), + anchor_v: map.get("anchor-v").map(|s| (*s).to_string()), + anchor_x: f32_opt(&map, "anchor-x")?, + anchor_y: f32_opt(&map, "anchor-y")?, + + frame: bool_opt(&map, "frame")?, + fill: color_tokens_opt(&map, "fill")?, + stroke_width: f32_opt(&map, "stroke-width")?, + stroke_color: color_tokens_opt(&map, "stroke-color")?, + + padding: f32_opt(&map, "padding")?, + padding_l: f32_opt(&map, "padding-left")?, + padding_r: f32_opt(&map, "padding-right")?, + padding_t: f32_opt(&map, "padding-top")?, + padding_b: f32_opt(&map, "padding-bottom")?, + + margin: f32_opt(&map, "margin")?, + margin_l: f32_opt(&map, "margin-left")?, + margin_r: f32_opt(&map, "margin-right")?, + margin_t: f32_opt(&map, "margin-top")?, + margin_b: f32_opt(&map, "margin-bottom")?, + }) + } +} From 3faee1d28396223c8c67d10dc2a6cc99eb989a33 Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Mon, 1 Sep 2025 15:44:05 +0200 Subject: [PATCH 08/29] Fixing bugs --- Cargo.toml | 2 ++ efx-sandbox/src/main.rs | 51 ++++++++++++++++++++++++------- efx/src/tags/bottom_panel.rs | 7 ++++- efx/src/tags/central_panel.rs | 11 +++++-- efx/src/tags/side_panel.rs | 7 ++++- efx/src/tags/top_panel.rs | 7 ++++- efx/src/tags/window.rs | 56 ++++++++++++++++++++++++++--------- efx/src/utils/attr.rs | 16 ++++++++-- 8 files changed, 124 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2b8088..a2542bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,6 @@ [workspace] members = ["efx", "efx-core", "efx-attrnames", "efx-sandbox"] resolver = "2" + +[workspace.package] rust-version = "1.85" diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 86e0e89..6d3c36b 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -10,25 +10,54 @@ fn main() -> eframe::Result<()> { ) } -#[derive(Default)] struct App { counter: i32, input: String, + show_settings: bool, +} + +impl Default for App { + fn default() -> Self { + Self { + counter: 0, + input: String::new(), + show_settings: true, + } + } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - // Header - let _ = efx!( - ui, - r#" - - - - - "# - ); + ui.scope(|ui| { + // Window + Column + efx!( + ui, + r##" + + + + + + + "## + ); + }); // Increment/decrement buttons - catch Response ui.horizontal(|ui| { diff --git a/efx/src/tags/bottom_panel.rs b/efx/src/tags/bottom_panel.rs index 31025fd..1292225 100644 --- a/efx/src/tags/bottom_panel.rs +++ b/efx/src/tags/bottom_panel.rs @@ -46,7 +46,12 @@ impl Tag for BottomPanel { quote! {{ #frame_ts #panel_ts - __efx_panel.show(&#ui.ctx(), |ui| { #children }); + let __efx_ctx = #ui.ctx().clone(); + { + let __efx_tmp = __efx_panel.show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; + } + () }} } } diff --git a/efx/src/tags/central_panel.rs b/efx/src/tags/central_panel.rs index cd56ec4..42fd13d 100644 --- a/efx/src/tags/central_panel.rs +++ b/efx/src/tags/central_panel.rs @@ -28,9 +28,14 @@ impl Tag for CentralPanel { quote! {{ #frame_ts - egui::CentralPanel::default() - .frame(__efx_frame) - .show(&#ui.ctx(), |ui| { #children }); + let __efx_ctx = #ui.ctx().clone(); + { + let __efx_tmp = egui::CentralPanel::default() + .frame(__efx_frame) + .show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; + } + () }} } } diff --git a/efx/src/tags/side_panel.rs b/efx/src/tags/side_panel.rs index df64acc..d735257 100644 --- a/efx/src/tags/side_panel.rs +++ b/efx/src/tags/side_panel.rs @@ -84,7 +84,12 @@ impl Tag for SidePanel { quote! {{ #frame_ts #panel_ts - __efx_panel.show(&#ui.ctx(), |ui| { #children }); + let __efx_ctx = #ui.ctx().clone(); + { + let __efx_tmp = __efx_panel.show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; + } + () }} } } diff --git a/efx/src/tags/top_panel.rs b/efx/src/tags/top_panel.rs index 38b830d..f28332c 100644 --- a/efx/src/tags/top_panel.rs +++ b/efx/src/tags/top_panel.rs @@ -46,7 +46,12 @@ impl Tag for TopPanel { quote! {{ #frame_ts #panel_ts - __efx_panel.show(&#ui.ctx(), |ui| { #children }); + let __efx_ctx = #ui.ctx().clone(); + { + let __efx_tmp = __efx_panel.show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; + } + () }} } } diff --git a/efx/src/tags/window.rs b/efx/src/tags/window.rs index eda3f25..4aecc6e 100644 --- a/efx/src/tags/window.rs +++ b/efx/src/tags/window.rs @@ -1,12 +1,13 @@ use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; +use crate::utils::expr::expr_opt; use crate::utils::panel::*; use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; -use crate::utils::expr::expr_opt; +use syn::Expr; pub struct Window { attributes: Attributes, @@ -123,24 +124,38 @@ impl Tag for Window { // anchor (Align2 + offset) if self.attributes.anchor_h.is_some() || self.attributes.anchor_v.is_some() { - let h = self.attributes.anchor_h.clone().unwrap_or_else(|| "center".to_string()); - let v = self.attributes.anchor_v.clone().unwrap_or_else(|| "center".to_string()); + let h = self + .attributes + .anchor_h + .clone() + .unwrap_or_else(|| "center".to_string()); + let v = self + .attributes + .anchor_v + .clone() + .unwrap_or_else(|| "center".to_string()); let h_align = match h.as_str() { - "left" => quote!( egui::Align::Min ), - "center" => quote!( egui::Align::Center ), - "right" => quote!( egui::Align::Max ), + "left" => quote!(egui::Align::Min), + "center" => quote!(egui::Align::Center), + "right" => quote!(egui::Align::Max), other => { - let msg = format!("efx: `anchor-h` expected left|center|right, got `{}`", other); + let msg = format!( + "efx: `anchor-h` expected left|center|right, got `{}`", + other + ); return quote! { compile_error!(#msg); }; } }; let v_align = match v.as_str() { - "top" => quote!( egui::Align::Min ), - "center" => quote!( egui::Align::Center ), - "bottom" => quote!( egui::Align::Max ), + "top" => quote!(egui::Align::Min), + "center" => quote!(egui::Align::Center), + "bottom" => quote!(egui::Align::Max), other => { - let msg = format!("efx: `anchor-v` expected top|center|bottom, got `{}`", other); + let msg = format!( + "efx: `anchor-v` expected top|center|bottom, got `{}`", + other + ); return quote! { compile_error!(#msg); }; } }; @@ -149,11 +164,16 @@ impl Tag for Window { let ay = self.attributes.anchor_y.unwrap_or(0.0); win.extend(quote!( - __efx_window = __efx_window.anchor(egui::Align2(#h_align, #v_align), egui::vec2(#ax as f32, #ay as f32)); + __efx_window = __efx_window.anchor(egui::Align2([#h_align, #v_align]), egui::vec2(#ax as f32, #ay as f32)); )); } let open_bind = if let Some(expr) = &self.attributes.open_expr { + if !is_assignable_expr(expr) { + return quote! { + compile_error!("efx: `open` must be an assignable boolean lvalue (e.g. {self.show_window})"); + }; + } quote! { let mut __efx_open = (#expr); __efx_window = __efx_window.open(&mut __efx_open); @@ -167,7 +187,15 @@ impl Tag for Window { #frame_ts #win #open_bind - __efx_window.show(&#ui.ctx(), |ui| { #children }); + let __efx_ctx = #ui.ctx().clone(); + // Explicitly limit the lifetime of the result of show(...) + { + let __efx_tmp = __efx_window.show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; + } + + // Nothing comes back out + () }} } } @@ -191,7 +219,7 @@ struct Attributes { // opening state binding (expression) #[attr(name = "open")] - open_expr: Option, + open_expr: Option, // geometry: positions #[attr(name = "default-x")] diff --git a/efx/src/utils/attr.rs b/efx/src/utils/attr.rs index 67f68ad..a44d996 100644 --- a/efx/src/utils/attr.rs +++ b/efx/src/utils/attr.rs @@ -1,9 +1,9 @@ +use crate::attr_adapters as A; use efx_core::Element; use proc_macro2::TokenStream; use quote::quote; use std::collections::BTreeMap; - -use crate::attr_adapters as A; +use syn::Expr; #[inline] pub fn attr_map<'a>( @@ -115,3 +115,15 @@ pub fn stroke_tokens(width: Option, color: Option) -> Option bool { + use syn::{ + Expr::{Field, Index, Paren, Path}, + ExprParen, + }; + match e { + Path(_) | Field(_) | Index(_) => true, + Paren(ExprParen { expr, .. }) => is_assignable_expr(expr), + _ => false, + } +} From cd2fb64f72c35d8a20def6994e6869badfd303dc Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Wed, 3 Sep 2025 04:22:23 +0200 Subject: [PATCH 09/29] Fixed Window tag, updated sandbox --- efx-sandbox/src/main.rs | 176 +++++++--------- efx/docs/tags.md | 394 +++++++++++++++++------------------ efx/src/input.rs | 14 ++ efx/src/lib.rs | 84 ++++++++ efx/src/render.rs | 2 +- efx/src/tags/bottom_panel.rs | 2 +- efx/src/tags/heading.rs | 2 +- efx/src/tags/mod.rs | 2 +- efx/src/tags/resize.rs | 2 +- efx/src/tags/side_panel.rs | 4 +- efx/src/tags/top_panel.rs | 2 +- efx/src/tags/window.rs | 68 +++--- 12 files changed, 413 insertions(+), 339 deletions(-) diff --git a/efx-sandbox/src/main.rs b/efx-sandbox/src/main.rs index 6d3c36b..94c9ad9 100644 --- a/efx-sandbox/src/main.rs +++ b/efx-sandbox/src/main.rs @@ -1,5 +1,5 @@ -use eframe::{NativeOptions, egui}; -use efx::efx; +use eframe::{egui, NativeOptions}; +use efx::{efx, efx_ctx}; fn main() -> eframe::Result<()> { let native = NativeOptions::default(); @@ -28,60 +28,37 @@ impl Default for App { impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.scope(|ui| { - // Window + Column - efx!( - ui, - r##" - - - - - - - "## - ); - }); - - // Increment/decrement buttons - catch Response - ui.horizontal(|ui| { - let inc = efx!(ui, r#""#); - if inc.clicked() { - self.counter += 1; - } - - let dec = efx!(ui, r#""#); - if dec.clicked() { - self.counter -= 1; - } - }); - - // Dynamic text via {expr} - let _ = efx!(ui, r#""#); - - // Input field (binding directly to the state field) - let _ = efx!(ui, r#""#); + efx_ctx!( + ctx, + r##" + + + + + + + + + + + + + + + + + + + + + + + + + + + - // Scrolling + different tags - let _ = efx!( - ui, - r##" Main title Section @@ -100,58 +77,55 @@ impl eframe::App for App { - "## - ); - - // SidePanel with other tags - let _ = efx!(ui, r##" - - - - - - - - - - "##); - let _ = efx!(ui, r##" - - - - - - - - - - "##); + - let _ = efx!(ui, r##" - - - + + + - - "##); + + + + + + + + + + "## + ); - // Resize - let _ = efx!(ui, r##" - - - - - - - - - - - "##); - }); + // egui::CentralPanel::default().show(ctx, |ui| { + // ui.horizontal(|ui| { + // let inc = efx!(ui, r#""#); + // if inc.clicked() { self.counter += 1; } + // + // let dec = efx!(ui, r#""#); + // if dec.clicked() { self.counter -= 1; } + // }); + // + // // Текст + // let _ = efx!(ui, r#""#); + // + // }); } } diff --git a/efx/docs/tags.md b/efx/docs/tags.md index 21dc1d8..72e8e84 100644 --- a/efx/docs/tags.md +++ b/efx/docs/tags.md @@ -3,77 +3,6 @@ > Starting with 0.5 some tags support attributes. > Unknown attributes result in `compile_error!`. -### `` - -An independent floating window (overlay) with optional frame and persistent state. - -**Children:** rendered inside the window. - -**Required attributes** -- `title="string"` — window title. - -**Optional** -- `id="string"` — egui `Id` to persist window state (position/size). If omitted, egui derives an id from the title. - -**Behavior** -- `open="{expr_bool}"` — binds to a boolean state; user closing the window writes back to the expression. -- `movable="true|false"` — allow dragging. -- `resizable="true|false"` — allow resizing. -- `collapsible="true|false"` — allow collapsing to title bar. -- `title-bar="true|false"` — show/hide title bar. -- `enabled="true|false"` — disable all contents when false. -- `constrain="true|false"` — constrain to viewport. -- `auto-sized="true"` — size to fit content initially. - -**Positioning** -- `default-x="number"`, `default-y="number"` — initial position. -- `pos-x="number"`, `pos-y="number"` — force current position each frame. -- `anchor-h="left|center|right"`, `anchor-v="top|center|bottom"`, `anchor-x="number"`, `anchor-y="number"` — anchor to a screen corner/edge with an offset. - -**Sizing** -- `default-width`, `default-height` — initial size. -- `min-width`, `min-height` — lower bounds. -- `max-width`, `max-height` — upper bounds. - -**Frame & styling** -- `frame="true|false"` — enable/disable default frame (default: `true`). -- `fill="#RRGGBB[AA]"` — background color. -- `stroke-width="number"` — border width. -- `stroke-color="#RRGGBB[AA]"` — border color. -- `padding`, `padding-left|right|top|bottom` — inner margin. -- `margin`, `margin-left|right|top|bottom` — outer margin. - -**Example** -```xml - - - - - - - - - - - - -``` - ### `Column` Vertical container. Generates `ui.vertical(|ui| { ... })`. @@ -112,132 +41,6 @@ efx!(Ui::default(), r#" ``` -### `Label` -Text widget. Only text and interpolations (`{expr}`) in child nodes are allowed. - -**Attributes** - -- `color="name|#RRGGBB[AA]"` — text color. -- `size="N"` — font size (f32). -- `bold="true|false"`. -- `italic="true|false"`. -- `underline="true|false"`. -- `strike="true|false"`. -- `monospace="true|false"`. -- `wrap="true|false"` — enable line wrapping. - -```rust -use efx_core::doc_prelude::*; -use efx::*; - -efx!(Ui::default(), r##""##); -``` - -### `Separator` -Self-closing divider. No children allowed (otherwise `compile_error!`). - -**Attributes** - -- `space="N"` — uniform spacing before & after (f32). -- `space_before="N"` — spacing above. -- `space_after="N"` — spacing below. - -```rust -use efx_core::doc_prelude::*; -use efx::*; - -efx!(Ui::default(), r#""#); -efx!(Ui::default(), r#""#); -``` - -```rust,compile_fail -use efx_core::doc_prelude::*; -use efx::*; - -/// compile_fail -efx!(Ui::default(), "child"); -``` - -### `Button` -Button is the only tag that returns a response value (`Resp`) at the root of an expression. - -**Attributes** - -- `fill="color`" — background fill color. -- `rounding="N"` — rounding radius (f32). -- `min_width="N", min_height="N"` — minimum size. -- `frame="true|false"` — draw background/border. -- `enabled="true|false"` — disable/enable button. -- `tooltip="text"` — hover tooltip. - -```rust -use efx_core::doc_prelude::*; -use efx::*; - -let resp: Resp = efx!(Ui::default(), r#""#); -assert!(!resp.clicked()); -``` - -### `Hyperlink` -Clickable link widget. Generates `ui.hyperlink(url)` or `ui.hyperlink_to(label, url)`. - -**Attributes** - -- `url="..."` — destination address (string, required). -- `open_external="true|false"` — open link in system browser (default true). -- `color="name|#RRGGBB[AA]"` — link text color. -- `underline="true|false"` — underline link text (default true). -- `tooltip="text"` — hover tooltip. - -Cross-platform usage - -- **Web:** renders as standard `` link. -- **Desktop (eframe, bevy_egui):** opens system browser via `ui.hyperlink(...)`. -- **Game/tool overlays:** convenient way to link to docs, repos, or help. -- **Offline apps:** with custom URL schemes (e.g. `help://topic`) may open in-app help instead of browser. - -```rust -use efx_core::doc_prelude::*; -use efx::*; - -efx!(Ui::default(), r##" - - - About - -"##); -``` - -### `TextField` -Single-line or multi-line text input. Generates `egui::TextEdit` and inserts it via `ui.add(...)`. Must be self-closing (no children). - -**Attributes** - -- `value=""` — **required**. Rust lvalue expression of type `String`, e.g. `state.name`. The generator takes `&mut ()` automatically. -- `hint="text"` — placeholder text shown when empty. -- `password="true|false"` — mask characters (applies to single-line; ignored with `multiline="true"`). -- `width="N"` — desired width in points (f32). -- `multiline="true|false"` — multi-line editor (`TextEdit::multiline`). - -```rust -use efx_core::doc_prelude::*; -use efx::*; - -#[derive(Default)] -struct State { name: String } - -let mut state = State::default(); - -// Single-line with placeholder and width -efx!(Ui::default(), r#""#); - -// Password field (single-line) -efx!(Ui::default(), r#""#); - -// Multiline editor -efx!(Ui::default(), r#""#); -``` - ### `CentralPanel` Main content area that fills all remaining space. Wraps children in `egui::CentralPanel` and applies an optional `Frame`. @@ -428,6 +231,203 @@ efx!(Ui::default(), r#" "#); ``` +### `` + +An independent floating window (overlay) with optional frame and persistent state. + +**Children:** rendered inside the window. + +**Required attributes** +- `title="string"` — window title. + +**Optional** +- `id="string"` — egui `Id` to persist window state (position/size). If omitted, egui derives an id from the title. + +**Behavior** +- `open="{expr_bool}"` — binds to a boolean state; user closing the window writes back to the expression. +- `movable="true|false"` — allow dragging. +- `resizable="true|false"` — allow resizing. +- `collapsible="true|false"` — allow collapsing to title bar. +- `title-bar="true|false"` — show/hide title bar. +- `enabled="true|false"` — disable all contents when false. +- `constrain="true|false"` — constrain to viewport. +- `auto-sized="true"` — size to fit content initially. + +**Positioning** +- `default-x="number"`, `default-y="number"` — initial position. +- `pos-x="number"`, `pos-y="number"` — force current position each frame. +- `anchor-h="left|center|right"`, `anchor-v="top|center|bottom"`, `anchor-x="number"`, `anchor-y="number"` — anchor to a screen corner/edge with an offset. + +**Sizing** +- `default-width`, `default-height` — initial size. +- `min-width`, `min-height` — lower bounds. +- `max-width`, `max-height` — upper bounds. + +**Frame & styling** +- `frame="true|false"` — enable/disable default frame (default: `true`). +- `fill="#RRGGBB[AA]"` — background color. +- `stroke-width="number"` — border width. +- `stroke-color="#RRGGBB[AA]"` — border color. +- `padding`, `padding-left|right|top|bottom` — inner margin. +- `margin`, `margin-left|right|top|bottom` — outer margin. + +**Example** +```xml + + + + + + + + + + + + +``` + +### `Label` +Text widget. Only text and interpolations (`{expr}`) in child nodes are allowed. + +**Attributes** + +- `color="name|#RRGGBB[AA]"` — text color. +- `size="N"` — font size (f32). +- `bold="true|false"`. +- `italic="true|false"`. +- `underline="true|false"`. +- `strike="true|false"`. +- `monospace="true|false"`. +- `wrap="true|false"` — enable line wrapping. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##""##); +``` + +### `Separator` +Self-closing divider. No children allowed (otherwise `compile_error!`). + +**Attributes** + +- `space="N"` — uniform spacing before & after (f32). +- `space_before="N"` — spacing above. +- `space_after="N"` — spacing below. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r#""#); +efx!(Ui::default(), r#""#); +``` + +```rust,compile_fail +use efx_core::doc_prelude::*; +use efx::*; + +/// compile_fail +efx!(Ui::default(), "child"); +``` + +### `Button` +Button is the only tag that returns a response value (`Resp`) at the root of an expression. + +**Attributes** + +- `fill="color`" — background fill color. +- `rounding="N"` — rounding radius (f32). +- `min_width="N", min_height="N"` — minimum size. +- `frame="true|false"` — draw background/border. +- `enabled="true|false"` — disable/enable button. +- `tooltip="text"` — hover tooltip. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +let resp: Resp = efx!(Ui::default(), r#""#); +assert!(!resp.clicked()); +``` + +### `Hyperlink` +Clickable link widget. Generates `ui.hyperlink(url)` or `ui.hyperlink_to(label, url)`. + +**Attributes** + +- `url="..."` — destination address (string, required). +- `open_external="true|false"` — open link in system browser (default true). +- `color="name|#RRGGBB[AA]"` — link text color. +- `underline="true|false"` — underline link text (default true). +- `tooltip="text"` — hover tooltip. + +Cross-platform usage + +- **Web:** renders as standard `` link. +- **Desktop (eframe, bevy_egui):** opens system browser via `ui.hyperlink(...)`. +- **Game/tool overlays:** convenient way to link to docs, repos, or help. +- **Offline apps:** with custom URL schemes (e.g. `help://topic`) may open in-app help instead of browser. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##" + + + About + +"##); +``` + +### `TextField` +Single-line or multi-line text input. Generates `egui::TextEdit` and inserts it via `ui.add(...)`. Must be self-closing (no children). + +**Attributes** + +- `value=""` — **required**. Rust lvalue expression of type `String`, e.g. `state.name`. The generator takes `&mut ()` automatically. +- `hint="text"` — placeholder text shown when empty. +- `password="true|false"` — mask characters (applies to single-line; ignored with `multiline="true"`). +- `width="N"` — desired width in points (f32). +- `multiline="true|false"` — multi-line editor (`TextEdit::multiline`). + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +#[derive(Default)] +struct State { name: String } + +let mut state = State::default(); + +// Single-line with placeholder and width +efx!(Ui::default(), r#""#); + +// Password field (single-line) +efx!(Ui::default(), r#""#); + +// Multiline editor +efx!(Ui::default(), r#""#); +``` + ### `` A resizable container that lets the user drag a handle to change the size of its content. diff --git a/efx/src/input.rs b/efx/src/input.rs index 4b67d75..088aeee 100644 --- a/efx/src/input.rs +++ b/efx/src/input.rs @@ -13,3 +13,17 @@ impl syn::parse::Parse for EfxInput { Ok(EfxInput { ui, template }) } } + +pub(crate) struct EfxCtxInput { + pub(crate) ctx: Expr, + pub(crate) template: LitStr, +} + +impl syn::parse::Parse for EfxCtxInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let ctx = input.parse::()?; + input.parse::()?; + let template = input.parse::()?; + Ok(EfxCtxInput { ctx, template }) + } +} diff --git a/efx/src/lib.rs b/efx/src/lib.rs index b29b984..5d54921 100644 --- a/efx/src/lib.rs +++ b/efx/src/lib.rs @@ -87,3 +87,87 @@ pub fn efx(input: TokenStream) -> TokenStream { expanded.into() } + +#[proc_macro] +pub fn efx_ctx(input: TokenStream) -> TokenStream { + use crate::input::EfxCtxInput; + use efx_core::{parse_str, Node}; + + let args = syn::parse_macro_input!(input as EfxCtxInput); + let ctx_expr = args.ctx; + let template = args.template.value(); + + let ast = match parse_str(&template) { + Ok(nodes) => nodes, + Err(err) => { + let msg = format!("efx parse error: {}", err); + return quote! { compile_error!(#msg); }.into(); + } + }; + + let roots: Vec = ast + .into_iter() + .filter(|n| match n { + Node::Text(t) => !t.value.trim().is_empty(), + _ => true, + }) + .collect(); + + let root = match roots.as_slice() { + // ровно один корневой элемент — OK + [Node::Element(el)] => el.clone(), + + // пусто — ожидаем единственный корневой элемент + [] => { + let msg = "efx_ctx!: expected a single root element"; + return quote! { compile_error!(#msg); }.into(); + } + + // один узел, но не Element (например, текст/интерполяция) — ошибка + [_] => { + let msg = "efx_ctx!: root must be an element"; + return quote! { compile_error!(#msg); }.into(); + } + + // больше одного корневого узла — тоже ошибка + [_, ..] => { + let msg = "efx_ctx!: expected a single root element"; + return quote! { compile_error!(#msg); }.into(); + } + }; + + let allowed = [ + "Window", + "CentralPanel", + "TopPanel", + "BottomPanel", + "SidePanel", + ]; + if !allowed.iter().any(|&n| n == root.name) { + let msg = format!( + "efx_ctx!: root <{}> is not context-root. Use efx!(ui, ...) instead.", + root.name + ); + return quote! { compile_error!(#msg); }.into(); + } + + // Generating a render for the root + let ui_param = quote!(__efx_ui_ctx); + let body = render::render_element_stmt(&ui_param, &root); + + quote! {{ + // local wrapper: send only .ctx() + struct __EfxUiCtxShim<'a>(&'a egui::Context); + impl<'a> __EfxUiCtxShim<'a> { + #[inline] + fn ctx(&self) -> &egui::Context { self.0 } + } + + // clone the context and render the root + let __efx_ctx_local = (#ctx_expr).clone(); + let __efx_ui_ctx = __EfxUiCtxShim(&__efx_ctx_local); + #body + () + }} + .into() +} diff --git a/efx/src/render.rs b/efx/src/render.rs index 2d4d8a6..7f26f5c 100644 --- a/efx/src/render.rs +++ b/efx/src/render.rs @@ -35,7 +35,7 @@ pub(crate) fn render_node_stmt(ui: &UI, node: &Node) -> TokenStrea } } -fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { +pub(crate) fn render_element_stmt(ui: &UI, el: &Element) -> TokenStream { match el.name.as_str() { "Window" => render_tag::(ui, el), "Heading" => render_tag::(ui, el), diff --git a/efx/src/tags/bottom_panel.rs b/efx/src/tags/bottom_panel.rs index 1292225..c6f9b7c 100644 --- a/efx/src/tags/bottom_panel.rs +++ b/efx/src/tags/bottom_panel.rs @@ -3,7 +3,7 @@ use crate::utils::panel::*; use crate::utils::render::render_children_stmt; use efx_core::Element; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; pub struct BottomPanel { attributes: Attributes, diff --git a/efx/src/tags/heading.rs b/efx/src/tags/heading.rs index 8948646..2b0c27f 100644 --- a/efx/src/tags/heading.rs +++ b/efx/src/tags/heading.rs @@ -4,7 +4,7 @@ use crate::utils::buffer::build_buffer_from_children; use efx_attrnames::AttrNames; use efx_core::Element; use proc_macro2::{Span, TokenStream}; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; use syn::LitStr; pub struct Heading { diff --git a/efx/src/tags/mod.rs b/efx/src/tags/mod.rs index eeee71a..abfb783 100644 --- a/efx/src/tags/mod.rs +++ b/efx/src/tags/mod.rs @@ -23,7 +23,7 @@ pub use heading::Heading; pub use hyperlink::Hyperlink; pub use label::Label; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; pub use resize::Resize; pub use row::Row; pub use scroll_area::ScrollArea; diff --git a/efx/src/tags/resize.rs b/efx/src/tags/resize.rs index f7f9e0c..1635a01 100644 --- a/efx/src/tags/resize.rs +++ b/efx/src/tags/resize.rs @@ -4,7 +4,7 @@ use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; pub struct Resize { attributes: Attributes, diff --git a/efx/src/tags/side_panel.rs b/efx/src/tags/side_panel.rs index d735257..14f651b 100644 --- a/efx/src/tags/side_panel.rs +++ b/efx/src/tags/side_panel.rs @@ -1,11 +1,11 @@ use crate::tags::{Tag, TagAttributes}; use crate::utils::attr::*; -use crate::utils::panel::{Dim, FrameStyle, SizeOpts, emit_size_methods}; +use crate::utils::panel::{emit_size_methods, Dim, FrameStyle, SizeOpts}; use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; pub struct SidePanel { attributes: Attributes, diff --git a/efx/src/tags/top_panel.rs b/efx/src/tags/top_panel.rs index f28332c..ec6eba8 100644 --- a/efx/src/tags/top_panel.rs +++ b/efx/src/tags/top_panel.rs @@ -3,7 +3,7 @@ use crate::utils::panel::*; use crate::utils::render::render_children_stmt; use efx_core::Element; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; pub struct TopPanel { attributes: Attributes, diff --git a/efx/src/tags/window.rs b/efx/src/tags/window.rs index 4aecc6e..d1496e1 100644 --- a/efx/src/tags/window.rs +++ b/efx/src/tags/window.rs @@ -6,7 +6,7 @@ use crate::utils::render::render_children_stmt; use efx_attrnames::AttrNames; use efx_core::Element; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{quote, ToTokens}; use syn::Expr; pub struct Window { @@ -48,22 +48,20 @@ impl Tag for Window { } fn render(&self, ui: &UI) -> TokenStream { - let title = match &self.attributes.title { - Some(s) if !s.is_empty() => s, - _ => { - return quote! { compile_error!("efx: requires non-empty `title` attribute"); }; - } - }; - let children = render_children_stmt("e!(ui), &self.element.children); let frame_ts = self.content(ui); - let mut win = - quote!( let mut __efx_window = egui::Window::new(#title).frame(__efx_frame); ); + let title_ts = if let Some(t) = &self.attributes.title { + quote!( #t ) + } else { + quote!("") + }; - // id + let mut win = TokenStream::new(); if let Some(id) = &self.attributes.id { - win.extend(quote!( __efx_window = __efx_window.id(egui::Id::new(#id)); )); + win.extend(quote! { + __efx_window = __efx_window.id(egui::Id::new(#id)); + }); } // behavior @@ -71,7 +69,7 @@ impl Tag for Window { win.extend(quote!( __efx_window = __efx_window.movable(#b); )); } if let Some(b) = self.attributes.resizable { - win.extend(quote!( __efx_window = __efx_window.resizable(#b); )); + win.extend(quote! { __efx_window = __efx_window.resizable(#b); }); } if let Some(b) = self.attributes.collapsible { win.extend(quote!( __efx_window = __efx_window.collapsible(#b); )); @@ -168,32 +166,36 @@ impl Tag for Window { )); } - let open_bind = if let Some(expr) = &self.attributes.open_expr { - if !is_assignable_expr(expr) { - return quote! { - compile_error!("efx: `open` must be an assignable boolean lvalue (e.g. {self.show_window})"); - }; - } - quote! { - let mut __efx_open = (#expr); - __efx_window = __efx_window.open(&mut __efx_open); - #expr = __efx_open; - } - } else { - quote!() - }; + let (open_prefix, open_bind, open_writeback) = + if let Some(expr) = &self.attributes.open_expr { + ( + quote!( let mut __efx_open = (#expr); ), + quote!( __efx_window = __efx_window.open(&mut __efx_open); ), + quote!( #expr = __efx_open; ), + ) + } else { + (quote!(), quote!(), quote!()) + }; quote! {{ #frame_ts - #win - #open_bind - let __efx_ctx = #ui.ctx().clone(); - // Explicitly limit the lifetime of the result of show(...) + + #open_prefix { - let __efx_tmp = __efx_window.show(&__efx_ctx, |ui| { #children }); - let _ = __efx_tmp; + let mut __efx_window = egui::Window::new(#title_ts); + + #win + + #open_bind + + let __efx_ctx = #ui.ctx().clone(); + { + let __efx_tmp = __efx_window.show(&__efx_ctx, |ui| { #children }); + let _ = __efx_tmp; // drop + } } + #open_writeback // Nothing comes back out () }} From 01ed474d38a40de141a278a3a226d64f639abda1 Mon Sep 17 00:00:00 2001 From: Max Zhuk Date: Mon, 8 Sep 2025 13:58:11 +0400 Subject: [PATCH 10/29] Made lang syntax files --- efx-syntax/LICENSE | 21 ++++++ efx-syntax/README.md | 34 +++++++++ efx-syntax/language-configuration.json | 70 ++++++++++++++++++ efx-syntax/package.json | 39 ++++++++++ efx-syntax/snippets/efx.json | 50 +++++++++++++ efx-syntax/syntaxes/efx.tmLanguage.json | 95 +++++++++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 efx-syntax/LICENSE create mode 100644 efx-syntax/README.md create mode 100644 efx-syntax/language-configuration.json create mode 100644 efx-syntax/package.json create mode 100644 efx-syntax/snippets/efx.json create mode 100644 efx-syntax/syntaxes/efx.tmLanguage.json diff --git a/efx-syntax/LICENSE b/efx-syntax/LICENSE new file mode 100644 index 0000000..192f2c3 --- /dev/null +++ b/efx-syntax/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Max Zhuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/efx-syntax/README.md b/efx-syntax/README.md new file mode 100644 index 0000000..032c54a --- /dev/null +++ b/efx-syntax/README.md @@ -0,0 +1,34 @@ + +# EFx Syntax (TextMate / VS Code / JetBrains) + +Syntax highlighting for EFx templates: + +- Files: `*.efx` +- Basic constructions: tags ``, `` standard. +* **Desktop (eframe, bevy\_egui) :** ouvre le navigateur système via `ui.hyperlink(...)`. +* **Overlays jeu/outils :** pratique pour relier docs, dépôts, aide. +* **Apps hors-ligne :** avec schémas d’URL personnalisés (ex. `help://topic`). + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##" + + + À propos + +"##); +``` + +### `TextField` + +Champ de saisie (une ou plusieurs lignes). Génère `egui::TextEdit` et l’insère via `ui.add(...)`. Doit être auto-fermable (pas d’enfants). + +**Attributs** + +* `value=""` — **obligatoire**. Expression Rust de type `String`, ex. `state.name`. Le générateur prend automatiquement `&mut ()`. +* `hint="text"` — texte indicatif affiché quand vide. +* `password="true|false"` — masquer les caractères (uniquement en mode ligne simple). +* `width="N"` — largeur souhaitée (f32). +* `multiline="true|false"` — éditeur multi-lignes (`TextEdit::multiline`). + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +#[derive(Default)] +struct State { name: String } + +let mut state = State::default(); + +// Ligne simple avec placeholder et largeur +efx!(Ui::default(), r#""#); + +// Champ mot de passe +efx!(Ui::default(), r#""#); + +// Éditeur multi-lignes +efx!(Ui::default(), r#""#); +``` + +### `CentralPanel` + +Zone principale remplissant tout l’espace disponible. Enveloppe ses enfants dans `egui::CentralPanel` et applique éventuellement un `Frame`. + +**Attributs** + +* `frame="true|false"` — utiliser un frame par défaut (`true`, par défaut) ou aucun (`false`). +* `fill="name|#RRGGBB[AA]"` — couleur de fond. +* `stroke-width="N"` — épaisseur du trait (f32). +* `stroke-color="name|#RRGGBB[AA]"` — couleur du trait. +* `padding="N"` — marge intérieure (f32). +* `padding-left|padding-right|padding-top|padding-bottom="N"`. +* `margin="N"` — marge extérieure (f32). +* `margin-left|margin-right|margin-top|margin-bottom="N"`. + +```rust,no_run +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##" + + + + + + + Docs + + + +"##); +``` + +### `ScrollArea` + +Conteneur défilant basé sur `egui::ScrollArea`. Enveloppe ses enfants et fournit un défilement vertical/horizontal/les deux. + +**Attributs** + +* `axis="vertical|horizontal|both"` — axe de défilement (par défaut: vertical). +* `always-show="true|false"` — toujours afficher la barre de défilement. +* `max-height="N"` — hauteur maximale (f32). +* `max-width="N"` — largeur maximale (f32). +* `id="text"` — identifiant pour persister l’état. +* `bottom="true|false"` — rester collé en bas (logs/chats). +* `right="true|false"` — rester collé à droite. + +```rust,ignore +use efx_core::doc_prelude::*; +use efx::*; + +// Panneau log vertical collé en bas +efx!(Ui::default(), r#" + + + + + + + + +"#); + +// Scroll horizontal +efx!(Ui::default(), r#" + + + + + + + + +"#); + +// Les deux directions (ex. grosse grille) +efx!(Ui::default(), r#" + + + + + + + + +"#); +``` + +### `Heading` + +Titre de section. Génère `ui.heading(text)` avec des styles optionnels. + +**Attributs** + +* `level="1..6"` — niveau de titre (entier). + *Défaut :* `1`. Correspond aux styles prédéfinis d’`egui`. +* `size="N"` — taille de police personnalisée (f32). +* `color="name|#RRGGBB[AA]"` — couleur du texte. +* `tooltip="text"` — info-bulle au survol. + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r##" + + Titre principal + Section + Note + +"##); +``` + +L’attribut `level` contrôle le style de base (h1–h6), tandis que `size` et `color` permettent d’affiner l’apparence. From 7f37387e3cd423ebae44110786b7fe5b2d170371 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 10 Sep 2025 01:55:28 +0400 Subject: [PATCH 21/29] Create guide.md --- efx/docs/fr/guide.md | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 efx/docs/fr/guide.md diff --git a/efx/docs/fr/guide.md b/efx/docs/fr/guide.md new file mode 100644 index 0000000..73dd0b5 --- /dev/null +++ b/efx/docs/fr/guide.md @@ -0,0 +1,85 @@ +## Guide de syntaxe + +### Structure + +* Éléments : `enfants` et auto-fermants ``. +* Les nœuds de texte et les interpolations `{expr}` sont autorisés à l’intérieur de `Label`/`Button`. +* Plusieurs éléments sont autorisés à la racine — un bloc avec une liste d’expressions sera généré. + +### Interpolations + +Vous pouvez insérer des expressions Rust arbitraires dans le texte : + +```rust +use efx_core::doc_prelude::*; +use efx::*; + +efx!(Ui::default(), r#""#); +``` + +--- + +### Sécurité des interpolations `{expr}` + +Certains développeurs, habitués aux moteurs de templates PHP ou JavaScript, peuvent craindre que les expressions dans les templates soient dangereuses ou mélangent logique et balisage. + +EFx fonctionne différemment : + +* **Uniquement à la compilation** : `{expr}` est développé par le compilateur Rust. Il n’y a pas de `eval`, pas d’exécution de chaîne dynamique au runtime. +* **Typé et sûr** : le code inséré est du Rust normal, entièrement vérifié par le compilateur. + Si l’expression ne compile pas, le template échoue à la compilation. +* **Portée limitée** : les interpolations ne sont autorisées que dans des tags textuels comme `