Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,10 @@ Tags inside `.uix` templates map directly to native widgets:

| Category | Tags |
|---|---|
| **Containers** | `div` (Flexbox: `flex-direction` / `flex` / `gap` / `padding`) |
| **Containers** | `div` (Flexbox: `flex-direction` / `flex` / `gap` / `padding`), `Stack`, `SplitView` |
| **Text** | `label` (multi-line, auto-wrap) |
| **Buttons** | `button` |
| **Navigation** | `NavItem` |
| **Input** | `input` (type=`text` / `password` / `checkbox` / `radio` / `range` / `number`), `textarea` |
| **Selection** | `toggle`, `select` |
| **Status** | `progressbar` |
Expand Down
14 changes: 14 additions & 0 deletions docs/c-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ ui_tab_add(tabs, L"Title", content);
ui_tab_get_active(tabs);
ui_tab_set_active(tabs, 0);

// Stack
ui_stack_get_active_index(stack);
ui_stack_set_active_index(stack, 0);

// NavItem
ui_nav_get_selected(nav);
ui_nav_set_selected(nav, 1);

// SplitView
ui_split_set_pane(split, pane);
ui_split_set_content(split, content);
ui_split_set_mode(split, UI_SPLIT_VIEW_INLINE);
ui_split_set_open(split, 1);

// ScrollView
ui_scroll_set_content(sv, content);

Expand Down
3 changes: 3 additions & 0 deletions docs/uix-ai-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,11 @@ CSS 子集 + 选择器 + cascade。常用属性:
| 标签 | 作用 |
|------|------|
| `div` / `section` / `nav` | flex 容器 (默认 column) |
| `Stack` | 只显示一个子页面的容器;`active` 指定当前索引 |
| `SplitView` | 侧栏 + 内容双槽布局;前两个元素子节点分别作为侧栏和内容 |
| `label` / `span` / `p` / `h1`-`h6` | 文本(不可编辑) |
| `button` | 按钮 |
| `NavItem` | 侧栏导航项;支持 `text` / `svg` / `glyph` / `selected` |
| `input` | 文本输入;`type="password"` / `checkbox` / `radio` / `range` / `number` 切换形态 |
| `textarea` | 多行文本 |
| `toggle` | 开关 |
Expand Down
6 changes: 6 additions & 0 deletions docs/uix-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,11 +539,14 @@ obj.unknown # 返回 null(不报错)
| 标签 | Widget | 说明 |
|---|---|---|
| `<div>` | `VBoxWidget`(或 `HBoxWidget` 如果 `flex-direction: row`)| 通用容器 |
| `<Stack active="0">` | `StackWidget` | 只显示一个直接子元素;`:active` 可反应式切换页面 |
| `<SplitView mode="inline" open="true">` | `SplitViewWidget` | 恰好两个元素子节点,依次为 pane 和 content;`:open` 可反应式控制侧栏 |
| `<span>` | `LabelWidget` | |
| `<p>` | `LabelWidget`(支持换行)| **没有 inline 流**——子元素 `<strong>`/`<em>` 会堆叠显示,不会 inline 混排 |
| `<h1>`–`<h6>` | `LabelWidget` | 预设字号 + 粗体 |
| `<a>` | `LabelWidget` | 加 cursor:pointer(不自动导航)|
| `<button>` | `ButtonWidget` | CSS `background-color` 映射 `SetCustomBgColor` |
| `<NavItem text="..." selected="true"/>` | `NavItemWidget` | SplitView 侧栏导航项;`:text` / `:selected` 支持反应式绑定 |
| `<input type="text"/>` | `TextInputWidget` | |
| `<input type="checkbox"/>` | `CheckBoxWidget` | |
| `<input type="radio"/>` | `RadioButtonWidget` | `name` 属性作为同组标识 |
Expand Down Expand Up @@ -574,6 +577,9 @@ obj.unknown # 返回 null(不报错)
| 标签 | 属性 | 作用 |
|---|---|---|
| `<TitleBar>` | `title`、`show-minimize`、`show-maximize`、`show-close`、`show-icon`(都是 `true`/`false`)| |
| `<Stack>` | `active` | 初始显示的直接子元素索引 |
| `<SplitView>` | `mode`、`open`、`openPaneLength`、`compactPaneLength` | mode 为 `overlay` / `inline` / `compactOverlay` / `compactInline` |
| `<NavItem>` | `text`、`svg`、`glyph`、`selected` | `selected` 控制选中指示器 |
| `<ProgressBar>` | `min`、`max`、`value`(数字)、`indeterminate`(true/1 = 动画 stripe)| 构造时解析 |
| `<input type="range">` | `min`、`max`、`value` | |
| `<input type="number">` | `min`、`max`、`value`、`step` | step 决定显示小数位 |
Expand Down
34 changes: 34 additions & 0 deletions include/ui_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ UI_API void ui_window_on_key(UiWindow win, UiWindowKeyCallback cb, void* userdat
/* ------------------------------------------------------------------ */
UI_API UiWidget ui_vbox(void);
UI_API UiWidget ui_hbox(void);
UI_API UiWidget ui_stack(void);
UI_API UiWidget ui_spacer(float size); /* 0 = expanding */
UI_API UiWidget ui_panel(UiColor bg);
UI_API UiWidget ui_panel_themed(int theme_color_id); /* 0=sidebar_bg, 1=toolbar_bg, 2=content_bg */
Expand All @@ -410,6 +411,8 @@ UI_API UiWidget ui_toggle(const wchar_t* text);
UI_API UiWidget ui_progress_bar(float min_val, float max_val, float value);
UI_API UiWidget ui_tab_control(void);
UI_API UiWidget ui_scroll_view(void);
UI_API UiWidget ui_nav_item(const wchar_t* text, const char* svg);
UI_API UiWidget ui_split_view(void);
UI_API UiWidget ui_dialog(void);

/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -853,6 +856,37 @@ UI_API void ui_tab_add(UiWidget tab_control, const wchar_t* title, UiWidget cont
UI_API int ui_tab_get_active(UiWidget tab_control);
UI_API void ui_tab_set_active(UiWidget tab_control, int index);

/* ------------------------------------------------------------------ */
/* Stack */
/* ------------------------------------------------------------------ */
UI_API int ui_stack_get_active_index(UiWidget stack);
UI_API void ui_stack_set_active_index(UiWidget stack, int index);

/* ------------------------------------------------------------------ */
/* NavItem */
/* ------------------------------------------------------------------ */
UI_API int ui_nav_get_selected(UiWidget nav_item);
UI_API void ui_nav_set_selected(UiWidget nav_item, int selected);

/* ------------------------------------------------------------------ */
/* SplitView */
/* ------------------------------------------------------------------ */
typedef enum UiSplitViewMode {
UI_SPLIT_VIEW_OVERLAY = 0,
UI_SPLIT_VIEW_INLINE = 1,
UI_SPLIT_VIEW_COMPACT_OVERLAY = 2,
UI_SPLIT_VIEW_COMPACT_INLINE = 3
} UiSplitViewMode;

UI_API void ui_split_set_pane(UiWidget split_view, UiWidget pane);
UI_API void ui_split_set_content(UiWidget split_view, UiWidget content);
UI_API int ui_split_get_open(UiWidget split_view);
UI_API void ui_split_set_open(UiWidget split_view, int open);
UI_API void ui_split_set_open_immediate(UiWidget split_view, int open);
UI_API void ui_split_set_mode(UiWidget split_view, UiSplitViewMode mode);
UI_API void ui_split_set_open_pane_length(UiWidget split_view, float length);
UI_API void ui_split_set_compact_pane_length(UiWidget split_view, float length);

/* ------------------------------------------------------------------ */
/* ScrollView */
/* ------------------------------------------------------------------ */
Expand Down
37 changes: 37 additions & 0 deletions src/ui/page/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,8 @@ void CompileElement(CompilerCtx& ctx,
}
}

bool isSplitView = (node.tag == "SplitView" || node.tag == "split-view");

// ---- <svg>: fold child elements into SvgShape / SvgGradient entries ----
// Children like <circle>/<rect>/... do NOT become widgets. Their static
// attrs populate the shape; :attr bindings become CompiledBindings whose
Expand Down Expand Up @@ -1335,14 +1337,49 @@ void CompileElement(CompilerCtx& ctx,
}
}
if (!reportedNest) {
int splitSlot = 0;
for (const auto& c : node.children) {
if (c->kind == ui::uix::NodeKind::Element) {
if (isSplitView && splitSlot >= 2) {
ctx.out->errors.push_back(
"<SplitView> accepts exactly two element children: pane and content");
continue;
}
CompileElement(ctx, *c, &m, w.get());
if (isSplitView) ++splitSlot;
}
}
}
}

// ---- <SplitView> post-process ----
// Children are compiled normally first so their bindings/events keep the
// same path as any other UIX subtree. Then assign the first child to the
// pane slot and the second to the content slot expected by SplitViewWidget.
if (isSplitView) {
auto* split = dynamic_cast<SplitViewWidget*>(w.get());
auto children = w->Children();
if (children.size() != 2) {
ctx.out->errors.push_back(
"<SplitView> requires exactly two element children: pane and content");
} else if (split) {
w->Children().clear();
split->SetPane(children[0]);
split->SetContent(children[1]);
}
}

// ---- <Stack> static active index ----
// Apply after child compilation; applying in the factory would run before
// the child count is known and SetActiveIndex would reject non-zero pages.
if (auto* stack = dynamic_cast<StackWidget*>(w.get())) {
for (const auto& a : node.attrs) {
if (a.kind != ui::uix::AttrKind::Static || a.name != "active") continue;
try { stack->SetActiveIndex(std::stoi(a.rawValue)); } catch (...) {}
break;
}
}

// ---- <ScrollView> post-process ----
// Children were AddChild'd during recursion. ScrollViewWidget expects a
// single content widget via SetContent().
Expand Down
27 changes: 27 additions & 0 deletions src/ui/page/page_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ void PageState::ApplyBindingToWidget(Widget* w, const std::string& prop, const u
if (prop == "text") {
if (auto* lbl = dynamic_cast<LabelWidget*>(w)) lbl->SetText(ToWide(v.ToString()));
else if (auto* btn = dynamic_cast<ButtonWidget*>(w)) btn->SetText(ToWide(v.ToString()));
else if (auto* nav = dynamic_cast<NavItemWidget*>(w)) nav->SetText(ToWide(v.ToString()));
return;
}
if (prop == "id") {
Expand Down Expand Up @@ -759,6 +760,32 @@ void PageState::ApplyBindingToWidget(Widget* w, const std::string& prop, const u
return;
}
painted ? tg->SetOn(b) : tg->SetOnImmediate(b);
} else if (auto* nav = dynamic_cast<NavItemWidget*>(w)) {
if (nav->IsSelected() == b) {
traceBindingNoop();
shouldInvalidate = false;
return;
}
nav->SetSelected(b);
}
return;
}
if (prop == "open") {
if (auto* split = dynamic_cast<SplitViewWidget*>(w)) {
const bool open = v.ToBool();
if (split->IsPaneOpen() == open) {
traceBindingNoop();
shouldInvalidate = false;
return;
}
w->PaintedOnce() ? split->SetPaneOpen(open) : split->SetPaneOpenImmediate(open);
}
return;
}
if (prop == "active" || prop == "activeIndex") {
if (auto* stack = dynamic_cast<StackWidget*>(w)) {
stack->SetActiveIndex(static_cast<int>(v.ToNumber()));
ui::RequestLayout();
}
return;
}
Expand Down
42 changes: 42 additions & 0 deletions src/ui/page/widget_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,30 @@ WidgetPtr ConstructByTag(const std::string& tag, const std::string& text,
if (tag == "li") {
return std::make_shared<HBoxWidget>();
}
if (tag == "Stack" || tag == "stack") {
return std::make_shared<StackWidget>();
}
if (tag == "SplitView" || tag == "split-view") {
auto split = std::make_shared<SplitViewWidget>();
for (const auto& a : node.attrs) {
if (a.kind != ui::uix::AttrKind::Static) continue;
if (a.name == "mode") {
if (a.rawValue == "overlay") split->SetDisplayMode(SplitViewMode::Overlay);
else if (a.rawValue == "inline") split->SetDisplayMode(SplitViewMode::Inline);
else if (a.rawValue == "compactOverlay") split->SetDisplayMode(SplitViewMode::CompactOverlay);
else if (a.rawValue == "compactInline") split->SetDisplayMode(SplitViewMode::CompactInline);
} else if (a.name == "openPaneLength" || a.name == "open-pane-length") {
const float length = ResolvePx(a.rawValue);
if (length > 0.0f) split->SetOpenPaneLength(length);
} else if (a.name == "compactPaneLength" || a.name == "compact-pane-length") {
const float length = ResolvePx(a.rawValue);
if (length >= 0.0f) split->SetCompactPaneLength(length);
} else if (a.name == "open" && ParseBoolAttr(a.rawValue)) {
split->SetPaneOpenImmediate(true);
}
}
return split;
}

// Text widgets
if (tag == "span" || tag == "a" || tag == "label" || tag == "small" || tag == "strong" || tag == "em") {
Expand Down Expand Up @@ -983,6 +1007,24 @@ WidgetPtr ConstructByTag(const std::string& tag, const std::string& text,
}
return btn;
}
if (tag == "NavItem" || tag == "nav-item") {
std::wstring label = wtext;
std::string svg;
std::wstring glyph;
bool selected = false;
for (const auto& a : node.attrs) {
if (a.kind != ui::uix::AttrKind::Static) continue;
if (a.name == "text") label = ToWide(a.rawValue);
else if (a.name == "svg") svg = a.rawValue;
else if (a.name == "glyph") glyph = ToWide(a.rawValue);
else if (a.name == "selected") selected = ParseBoolAttr(a.rawValue);
}
auto nav = std::make_shared<NavItemWidget>(label, svg);
if (!glyph.empty()) nav->SetGlyph(glyph);
nav->SetSelected(selected);
nav->cursor = ui::CursorKind::Pointer;
return nav;
}
if (tag == "hr") {
return std::make_shared<SeparatorWidget>();
}
Expand Down
Loading