From 061ecebfe316b3fdcff6c9076de0143955b67b90 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 1 Feb 2026 20:21:07 +0100 Subject: [PATCH 1/5] Update roadmap with completed Zed and VS Code extensions Update roadmap with completed Zed and VS Code extensions --- ROADMAP.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6bc115b..278e7e2 100755 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -154,14 +154,30 @@ The long-term goal is to enable Rust developers to create modern desktop applica | --------------- | ------ | ------------------------------- | | Language Server | ✅ | LSP support for `.dampen` files | +### v0.3.1 - Advanced Widgets (In progress) + +**Objective**: Enrich available widget library + +| Milestone | Status | Description | +| --------- | ------ | ------------------- | +| Tabs | 🔲 | Tabs/TabBar widgets | + +### v0.3.2 - Advanced Widgets (planned) + +**Objective**: Enhance widget Datatable + +| Milestone | Status | Description | +| --------- | ------ | ------------------------ | +| DataTable | 🔲 | Add missing click events | + ### Developer Experience (planned) **Objective**: Improve tooling and DX | Milestone | Status | Priority | Description | | -------------------- | ------ | -------- | --------------------------------- | -| Zed Extension | 🔲 | High | Official Zed extension | -| VS Code Extension | 🔲 | High | Official VS Code extension | +| Zed Extension | ✅ | High | Official Zed extension | +| VS Code Extension | ✅ | High | Official VS Code extension | | Interactive CLI | 🔲 | Low | Interactive mode for `dampen new` | | Visual Hot Reload | 🔲 | Medium | Improved error overlay | | Debugger Integration | 🔲 | Low | IDE debugging support | From bf84294e28fa04593dfd02fea0100a5d461ac156 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 1 Feb 2026 23:48:43 +0100 Subject: [PATCH 2/5] Add TabBar and Tab widgets Add TabBar and Tab widgets with support for: - Static and dynamic selection - Icon and text labels - Event handling - Proper nesting validation - Codegen and runtime support - Example showcase integration --- .../src/commands/check/main_command.rs | 2 + crates/dampen-core/src/codegen/view.rs | 172 ++++++++++++++++++ crates/dampen-core/src/ir/node.rs | 11 +- crates/dampen-core/src/parser/mod.rs | 26 +++ crates/dampen-core/src/schema/mod.rs | 22 +++ crates/dampen-iced/Cargo.toml | 2 +- crates/dampen-iced/src/builder/mod.rs | 5 + crates/dampen-iced/src/builder/widgets/mod.rs | 1 + crates/dampen-iced/src/lib.rs | 4 +- examples/widget-showcase/src/ui/mod.rs | 1 + examples/widget-showcase/src/ui/window.dampen | 1 + examples/widget-showcase/src/ui/window.rs | 4 + tests/integration/Cargo.toml | 5 + 13 files changed, 253 insertions(+), 3 deletions(-) diff --git a/crates/dampen-cli/src/commands/check/main_command.rs b/crates/dampen-cli/src/commands/check/main_command.rs index 72b2506..81efd5f 100755 --- a/crates/dampen-cli/src/commands/check/main_command.rs +++ b/crates/dampen-cli/src/commands/check/main_command.rs @@ -1281,6 +1281,8 @@ impl WidgetKindExt for WidgetKind { WidgetKind::DataColumn, WidgetKind::TreeView, WidgetKind::TreeNode, + WidgetKind::TabBar, + WidgetKind::Tab, WidgetKind::For, WidgetKind::If, ] diff --git a/crates/dampen-core/src/codegen/view.rs b/crates/dampen-core/src/codegen/view.rs index c85e0ac..80b3cd4 100755 --- a/crates/dampen-core/src/codegen/view.rs +++ b/crates/dampen-core/src/codegen/view.rs @@ -247,6 +247,13 @@ fn generate_widget_with_locals( node.kind ))) } + WidgetKind::TabBar => generate_tab_bar(node, message_ident, style_classes), + WidgetKind::Tab => { + // Tab must be inside TabBar, handled by generate_tab_bar + Err(super::CodegenError::InvalidWidget( + "Tab must be inside TabBar".to_string(), + )) + } } } @@ -4921,3 +4928,168 @@ mod tests { assert!(code.contains("border")); } } + +/// Generate TabBar widget code +fn generate_tab_bar( + node: &crate::WidgetNode, + message_ident: &syn::Ident, + _style_classes: &HashMap, +) -> Result { + use proc_macro2::Span; + use quote::quote; + + // Get selected index attribute + let selected_attr = node.attributes.get("selected").ok_or_else(|| { + super::CodegenError::InvalidWidget("TabBar requires 'selected' attribute".to_string()) + })?; + + // Generate selected index expression + let selected_expr = match selected_attr { + AttributeValue::Static(s) => { + let idx: usize = s.parse().map_err(|_| { + super::CodegenError::InvalidWidget(format!("Invalid selected index: {}", s)) + })?; + quote! { #idx } + } + AttributeValue::Binding(_) => { + // For now, use a placeholder - this would need proper binding resolution + quote! { 0usize } + } + _ => quote! { 0usize }, + }; + + // Find on_select event handler + let on_select_handler = node + .events + .iter() + .find(|e| matches!(e.event, crate::ir::EventKind::Select)) + .map(|e| syn::Ident::new(&e.handler, Span::call_site())); + + // Generate tab labels + let tab_labels: Vec<_> = node + .children + .iter() + .enumerate() + .map(|(idx, child)| { + let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx); + + // Get label from tab + let label_expr = if let Some(label_attr) = child.attributes.get("label") { + match label_attr { + AttributeValue::Static(s) => Some(quote! { #s.to_string() }), + _ => None, + } + } else { + None + }; + + // Get icon from tab + let icon_expr = if let Some(icon_attr) = child.attributes.get("icon") { + match icon_attr { + AttributeValue::Static(s) => { + let icon_char = resolve_icon_for_codegen(s); + Some(quote! { #icon_char }) + } + _ => None, + } + } else { + None + }; + + // Build TabLabel expression based on what we have + let tab_label_expr = match (icon_expr, label_expr) { + (Some(icon), Some(label)) => { + quote! { iced_aw::tab_bar::TabLabel::IconText(#icon, #label) } + } + (Some(icon), None) => { + quote! { iced_aw::tab_bar::TabLabel::Icon(#icon) } + } + (None, Some(label)) => { + quote! { iced_aw::tab_bar::TabLabel::Text(#label) } + } + (None, None) => { + quote! { iced_aw::tab_bar::TabLabel::Text("Tab".to_string()) } + } + }; + + quote! { + tab_bar = tab_bar.push(#idx_lit, #tab_label_expr); + } + }) + .collect(); + + // Generate on_select callback if handler exists + let on_select_expr = if let Some(handler) = on_select_handler { + quote! { + .on_select(|idx| #message_ident::#handler(idx)) + } + } else { + quote! {} + }; + + // Generate icon_size if specified + let icon_size_expr = if let Some(icon_size_attr) = node.attributes.get("icon_size") { + match icon_size_attr { + AttributeValue::Static(s) => { + if let Ok(icon_size) = s.parse::() { + Some(quote! { .icon_size(#icon_size) }) + } else { + None + } + } + _ => None, + } + } else { + None + }; + + // Generate text_size if specified + let text_size_expr = if let Some(text_size_attr) = node.attributes.get("text_size") { + match text_size_attr { + AttributeValue::Static(s) => { + if let Ok(text_size) = s.parse::() { + Some(quote! { .text_size(#text_size) }) + } else { + None + } + } + _ => None, + } + } else { + None + }; + + // Build the TabBar widget + let tab_bar = quote! { + { + let mut tab_bar = iced_aw::TabBar::new(#selected_expr) + #on_select_expr + #icon_size_expr + #text_size_expr; + + #(#tab_labels)* + + tab_bar + } + }; + + Ok(tab_bar) +} + +/// Resolve icon name to Unicode character for codegen +fn resolve_icon_for_codegen(name: &str) -> char { + match name { + "home" => '\u{F015}', + "settings" => '\u{F013}', + "user" => '\u{F007}', + "search" => '\u{F002}', + "add" => '\u{F067}', + "delete" => '\u{F1F8}', + "edit" => '\u{F044}', + "save" => '\u{F0C7}', + "close" => '\u{F00D}', + "back" => '\u{F060}', + "forward" => '\u{F061}', + _ => '\u{F111}', // Circle as fallback + } +} diff --git a/crates/dampen-core/src/ir/node.rs b/crates/dampen-core/src/ir/node.rs index 427fa86..e22e086 100755 --- a/crates/dampen-core/src/ir/node.rs +++ b/crates/dampen-core/src/ir/node.rs @@ -74,6 +74,9 @@ pub enum WidgetKind { // Tree widget TreeView, TreeNode, + // Tab widgets + TabBar, + Tab, // Control flow For, If, @@ -263,6 +266,8 @@ impl std::fmt::Display for WidgetKind { WidgetKind::DataColumn => "data_column", WidgetKind::TreeView => "tree_view", WidgetKind::TreeNode => "tree_node", + WidgetKind::TabBar => "tab_bar", + WidgetKind::Tab => "tab", WidgetKind::For => "for", WidgetKind::If => "if", WidgetKind::Custom(name) => return write!(f, "{}", name), @@ -314,6 +319,8 @@ impl WidgetKind { "data_column", "tree_view", "tree_node", + "tab_bar", + "tab", "for", "if", ] @@ -363,7 +370,9 @@ impl WidgetKind { | WidgetKind::DataTable | WidgetKind::DataColumn | WidgetKind::TreeView - | WidgetKind::TreeNode => crate::ir::SchemaVersion { major: 1, minor: 1 }, + | WidgetKind::TreeNode + | WidgetKind::TabBar + | WidgetKind::Tab => crate::ir::SchemaVersion { major: 1, minor: 1 }, _ => crate::ir::SchemaVersion { major: 1, minor: 0 }, } } diff --git a/crates/dampen-core/src/parser/mod.rs b/crates/dampen-core/src/parser/mod.rs index 65b9440..090443a 100755 --- a/crates/dampen-core/src/parser/mod.rs +++ b/crates/dampen-core/src/parser/mod.rs @@ -735,6 +735,8 @@ fn parse_node(node: Node, source: &str) -> Result { "data_column" => WidgetKind::DataColumn, "tree_view" => WidgetKind::TreeView, "tree_node" => WidgetKind::TreeNode, + "tab_bar" => WidgetKind::TabBar, + "tab" => WidgetKind::Tab, "template" => WidgetKind::Custom("template".to_string()), "for" => WidgetKind::For, "if" => WidgetKind::If, @@ -1142,6 +1144,30 @@ fn validate_nesting_constraints( }); } + // Rule: Tab must be inside TabBar + if node.kind == WidgetKind::Tab && parent_kind != Some(&WidgetKind::TabBar) { + return Err(ParseError { + kind: ParseErrorKind::InvalidChild, + message: "Tab must be inside TabBar".to_string(), + span: node.span, + suggestion: Some("Wrap this tab in a ".to_string()), + }); + } + + // Rule: TabBar must contain only Tab children + if node.kind == WidgetKind::TabBar { + for child in &node.children { + if child.kind != WidgetKind::Tab { + return Err(ParseError { + kind: ParseErrorKind::InvalidChild, + message: "TabBar can only contain Tab widgets".to_string(), + span: child.span, + suggestion: Some("Use elements inside ".to_string()), + }); + } + } + } + // Recurse for child in &node.children { validate_nesting_constraints(child, Some(&node.kind))?; diff --git a/crates/dampen-core/src/schema/mod.rs b/crates/dampen-core/src/schema/mod.rs index 1a6e227..b5dd627 100755 --- a/crates/dampen-core/src/schema/mod.rs +++ b/crates/dampen-core/src/schema/mod.rs @@ -437,6 +437,28 @@ pub fn get_widget_schema(kind: &WidgetKind) -> WidgetSchema { style_attributes: &[], layout_attributes: &[], }, + WidgetKind::TabBar => WidgetSchema { + required: &["selected"], + optional: &[ + "spacing", + "padding", + "icon_size", + "text_size", + "width", + "height", + "class", + ], + events: &["on_select"], + style_attributes: COMMON_STYLE_ATTRIBUTES, + layout_attributes: &["width", "height"], + }, + WidgetKind::Tab => WidgetSchema { + required: &[], + optional: &["label", "icon", "enabled"], + events: &["on_click"], + style_attributes: COMMON_STYLE_ATTRIBUTES, + layout_attributes: &[], + }, WidgetKind::Custom(_) => WidgetSchema { required: &[], optional: &[], diff --git a/crates/dampen-iced/Cargo.toml b/crates/dampen-iced/Cargo.toml index 9b9b756..1afd918 100755 --- a/crates/dampen-iced/Cargo.toml +++ b/crates/dampen-iced/Cargo.toml @@ -16,7 +16,7 @@ categories = { workspace = true } dampen-core = { workspace = true } iced = { workspace = true } serde_json = { workspace = true } -iced_aw = { version = "0.13", default-features = false, features = ["date_picker", "time_picker", "color_picker", "context_menu", "menu"] } +iced_aw = { version = "0.13", default-features = false, features = ["date_picker", "time_picker", "color_picker", "context_menu", "menu", "tab_bar"] } chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] diff --git a/crates/dampen-iced/src/builder/mod.rs b/crates/dampen-iced/src/builder/mod.rs index f93eeb6..3cd9765 100755 --- a/crates/dampen-iced/src/builder/mod.rs +++ b/crates/dampen-iced/src/builder/mod.rs @@ -525,6 +525,11 @@ impl<'a> DampenWidgetBuilder<'a> { // TreeNode is handled within build_tree_view, shouldn't appear as top-level iced::widget::column(Vec::new()).into() } + WidgetKind::TabBar => self.build_tab_bar(node), + WidgetKind::Tab => { + // Tab is handled within build_tab_bar, shouldn't appear as top-level + iced::widget::column(Vec::new()).into() + } WidgetKind::DataColumn | WidgetKind::CanvasRect | WidgetKind::CanvasCircle diff --git a/crates/dampen-iced/src/builder/widgets/mod.rs b/crates/dampen-iced/src/builder/widgets/mod.rs index c51cef9..8023293 100755 --- a/crates/dampen-iced/src/builder/widgets/mod.rs +++ b/crates/dampen-iced/src/builder/widgets/mod.rs @@ -29,6 +29,7 @@ mod slider; mod space; mod stack; mod svg; +mod tab_bar; mod text; mod text_input; mod time_picker; diff --git a/crates/dampen-iced/src/lib.rs b/crates/dampen-iced/src/lib.rs index e33a35a..1157d23 100755 --- a/crates/dampen-iced/src/lib.rs +++ b/crates/dampen-iced/src/lib.rs @@ -528,7 +528,9 @@ pub fn render<'a>( | WidgetKind::DataTable | WidgetKind::DataColumn | WidgetKind::TreeView - | WidgetKind::TreeNode => backend.column(Vec::new()), + | WidgetKind::TreeNode + | WidgetKind::TabBar + | WidgetKind::Tab => backend.column(Vec::new()), } } diff --git a/examples/widget-showcase/src/ui/mod.rs b/examples/widget-showcase/src/ui/mod.rs index ebdd53a..784746d 100755 --- a/examples/widget-showcase/src/ui/mod.rs +++ b/examples/widget-showcase/src/ui/mod.rs @@ -22,6 +22,7 @@ pub mod slider; pub mod space; pub mod stack; pub mod svg; +pub mod tab_bar; pub mod text; pub mod textinput; pub mod toggler; diff --git a/examples/widget-showcase/src/ui/window.dampen b/examples/widget-showcase/src/ui/window.dampen index 71fba39..a753110 100755 --- a/examples/widget-showcase/src/ui/window.dampen +++ b/examples/widget-showcase/src/ui/window.dampen @@ -61,6 +61,7 @@