feat(ui): shadcn/ui-inspired component kit — buttons, inputs, switches#47
feat(ui): shadcn/ui-inspired component kit — buttons, inputs, switches#47niklabh wants to merge 1 commit into
Conversation
…s, cards, badges, separators, progress, labels This adds a rich set of UI widget primitives to the Oxide host/SDK boundary, modelled after the shadcn/ui design system. Guests can now compose native-looking UIs entirely through the FFI widget layer. Key changes: Host (oxide-browser): - `WidgetVariant` enum (Default, Secondary, Outline, Ghost, Destructive) with colour scheme helpers (variant_colors, variant_hover_bg) - Extended `WidgetCommand` with 8 new widget types: • Textarea — multi-line text input with scroll support • Card — container with title/description, border, rounded corners • Badge — pill-shaped status indicator with variant support • Switch — pill-shaped on/off toggle (bool state in WidgetValue) • Separator — 1px horizontal or vertical divider • Progress — horizontal progress bar (0.0..=1.0) • Label — static text label with muted variant and font size control - Updated Button to accept variant; TextInput now carries placeholder - Full keyboard input handling for text fields (handle_widget_key): • ←/→/↑/↓/Home/End/PgUp/PgDn navigation • Shift-selection (extending from anchor) • Cmd/Ctrl+A/C/X/V (select-all, copy, cut, paste) • Backspace/Delete with selection support • Enter inserts newline in textareas • UTF-8 boundary-aware cursor movement - GPUI render functions for all widget types with proper: • Font shaping via Window::text_system() • Selection highlight painting • Blinking caret • Placeholder text in muted colour • Mouse down/move/up for text selection • Scroll wheel in textareas • Hover and click state for interactive widgets - `widget_bounds` now returns Option — decorative widgets (Badge, Separator, Progress, Label) do not block canvas click-through - New host functions registered: api_ui_textarea, api_ui_switch, api_ui_card, api_ui_badge, api_ui_separator, api_ui_progress, SDK (oxide-sdk): - `UiVariant` enum with `as_u32()` codec - FFI declarations for all new host functions - `ui_button_variant(id, x, y, w, h, label, variant)` — builder with style - `ui_text_input` now takes placeholder; `ui_text_input_with_value` seeds init - `ui_textarea` / `ui_textarea_with_value` — multi-line with 16 KiB buffer - `ui_switch`, `ui_card`, `ui_badge`, `ui_separator`, `ui_separator_vertical`, `ui_progress`, `ui_label`, `ui_label_muted` — complete surface area - All widgets documented with /// doc comments Example (examples/shadcn-demo): - New workspace member showcasing every widget primitive in a single-page tour: buttons (5 variants), badges (5 variants), form with email/password/message fields, checkbox, switch, live preview card, sliders, progress bar, labels, separator Example (examples/hello-oxide): - Text input now defaults to "Type your name…" placeholder Builds: - Cargo.toml / Cargo.lock updated for new workspace member
📝 WalkthroughWalkthroughThis PR expands the oxide UI widget system with visual variants, new interactive controls (switches, textareas, cards, badges, separators, progress bars, labels), placeholder-aware text inputs, and a comprehensive shadcn-ui-inspired demo showcasing the complete component library with theme support, edit-state tracking, and keyboard interaction. ChangesWidget System Expansion
Sequence DiagramsequenceDiagram
participant GuestApp as Guest App
participant SDK as oxide-sdk
participant HostRuntime as oxide-browser Runtime
participant WidgetState as Widget State
participant Renderer as Canvas Renderer
GuestApp->>SDK: ui_text_input(id, x, y, w, placeholder)
SDK->>HostRuntime: _api_ui_text_input(ptr, len, placeholder_ptr, len, output_buf)
HostRuntime->>WidgetState: enqueue WidgetCommand::TextInput { placeholder }
HostRuntime->>WidgetState: initialize WidgetEditState if new id
HostRuntime->>HostRuntime: cache bounds for hit-testing
GuestApp->>HostRuntime: key event (focused widget)
HostRuntime->>WidgetState: handle_widget_key updates cursor, text
HostRuntime->>WidgetState: update widget_edits state
HostRuntime->>Renderer: snapshot widget_edits, widget_bounds_cache
Renderer->>Renderer: render_text_input with edit state
Renderer->>Renderer: draw text, selection, caret
Renderer->>GuestApp: return rendered frame
GuestApp->>SDK: read current widget text value
HostRuntime->>GuestApp: return persisted text from widget_states
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
oxide-browser/src/capabilities.rs (1)
3964-4011:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftExpose the full text length from text widgets.
api_ui_text_inputandapi_ui_textareacurrently returnmin(text.len(), out_cap). Once the widget content exceeds the caller’s buffer, the guest cannot tell “full value” from “truncated value”, so SDK callers silently lose everything past their fixed buffers. Return the required length, or a retry sentinel, so callers can resize and read the complete value.Also applies to: 4013-4062
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@oxide-browser/src/capabilities.rs` around lines 3964 - 4011, api_ui_text_input currently writes up to out_cap bytes but returns write_len, hiding whether the string was truncated; change the handler to still write min(bytes.len(), out_cap) via write_guest_bytes (using out_ptr/out_cap) but return the full required length (bytes.len() as u32) so the guest can detect truncation and resize; apply the identical change to api_ui_textarea where WidgetValue::Text is read/written, and ensure you keep using read_guest_string, write_guest_bytes, out_ptr and out_cap as in the current implementation while only changing the returned value.oxide-sdk/src/lib.rs (1)
3491-3521:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftKeep
ui_text_inputandui_textareasource-compatible.These wrappers changed the existing string parameter from “initial value” to “placeholder” without changing the function names. Existing guest code will still compile, but prefilled fields will now come up empty after upgrade. Preserve the old behavior under these names and add placeholder-specific helpers instead.
Also applies to: 3523-3560
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@oxide-sdk/src/lib.rs` around lines 3491 - 3521, The wrappers accidentally changed the semantics of ui_text_input and ui_textarea to treat their string argument as a placeholder instead of the original initial value; restore backward compatibility by making ui_text_input and ui_textarea keep the original "initial value" behavior (i.e., forward their string argument as the initial value to the underlying implementation or to ui_text_input_with_value/ui_textarea_with_value), and introduce new placeholder-specific helpers (e.g., ui_text_input_with_placeholder and ui_textarea_with_placeholder or rename the current *_with_value variants) that accept a placeholder parameter so callers can opt into placeholder behavior; update or add the corresponding helper names (ui_text_input_with_value, ui_textarea_with_value, or new *_with_placeholder) so the intent is clear and existing guest code continues to prefill fields as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/shadcn-demo/src/lib.rs`:
- Around line 112-136: The render loop currently creates new Strings each frame
via to_string()/format! for the email preview, message stats, and prefs, which
causes per-frame heap allocations; fix this by reusing preallocated mutable
String buffers (e.g., preview_buf, stats_buf, prefs_buf) declared outside the
hot render loop, clear them each frame and write into them with write! or
push_str/format_to-like operations, and pass their &str slices to ui_label and
ui_label_muted (or for the email preview use a borrowed &str when
email.is_empty() -> "(no email yet)" to avoid allocation); update the code paths
that build preview, the Message: ... string, and prefs (references to variables
email, message, remember, marketing, ui_label, ui_label_muted) to use these
reusable buffers instead of to_string()/format!.
In `@oxide-browser/src/ui.rs`:
- Around line 4800-4812: The stored widget edit offsets in tab.widget_edits
(variable edit) are being reused against the freshly loaded text (variable text)
and can fall on invalid UTF-8 byte boundaries, causing panics when slicing
(&text[range]) or mutating (replace_range, insert_str); before any slicing or
mutation, validate and normalize the edit offsets against the current text by
clamping start/end to 0..=text.len() and moving each offset to the nearest valid
char boundary (using str::is_char_boundary or scanning with char_indices) so
ranges become safe; apply this normalization wherever edit offsets are used (the
locations around the retrieval of text/edit and before any
replace_range/insert_str usages) and consider adding a small helper (e.g.,
normalize_widget_edit_offsets(edit, &text)) to centralize the logic.
- Around line 3989-4003: The widget_edits and widget_bounds_cache maps are never
pruned and can grow unbounded; before inserting current-frame entries in the
loop over widget_commands (the match handling WidgetCommand::TextInput and
WidgetCommand::Textarea), compute the set of current editable widget IDs from
widget_commands, then remove any keys from self.tabs[active].widget_edits and
self.tabs[active].widget_bounds_cache that are not in that set (and also clear
any stale text_input_focus if its id is no longer present); after pruning,
proceed to or_insert the Bounds and other per-frame entries as you already do.
---
Outside diff comments:
In `@oxide-browser/src/capabilities.rs`:
- Around line 3964-4011: api_ui_text_input currently writes up to out_cap bytes
but returns write_len, hiding whether the string was truncated; change the
handler to still write min(bytes.len(), out_cap) via write_guest_bytes (using
out_ptr/out_cap) but return the full required length (bytes.len() as u32) so the
guest can detect truncation and resize; apply the identical change to
api_ui_textarea where WidgetValue::Text is read/written, and ensure you keep
using read_guest_string, write_guest_bytes, out_ptr and out_cap as in the
current implementation while only changing the returned value.
In `@oxide-sdk/src/lib.rs`:
- Around line 3491-3521: The wrappers accidentally changed the semantics of
ui_text_input and ui_textarea to treat their string argument as a placeholder
instead of the original initial value; restore backward compatibility by making
ui_text_input and ui_textarea keep the original "initial value" behavior (i.e.,
forward their string argument as the initial value to the underlying
implementation or to ui_text_input_with_value/ui_textarea_with_value), and
introduce new placeholder-specific helpers (e.g., ui_text_input_with_placeholder
and ui_textarea_with_placeholder or rename the current *_with_value variants)
that accept a placeholder parameter so callers can opt into placeholder
behavior; update or add the corresponding helper names
(ui_text_input_with_value, ui_textarea_with_value, or new *_with_placeholder) so
the intent is clear and existing guest code continues to prefill fields as
before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 417024e6-3910-489b-a69e-2f52533b50fa
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
Cargo.tomlexamples/hello-oxide/src/lib.rsexamples/shadcn-demo/Cargo.tomlexamples/shadcn-demo/src/lib.rsoxide-browser/src/capabilities.rsoxide-browser/src/ui.rsoxide-sdk/src/lib.rs
| let mut row_y = 376.0; | ||
| let preview = if email.is_empty() { | ||
| "(no email yet)".to_string() | ||
| } else { | ||
| format!("📧 {email}") | ||
| }; | ||
| ui_label(440.0, row_y, &preview, 14.0); | ||
| row_y += 28.0; | ||
|
|
||
| let lines = message.lines().count(); | ||
| let chars = message.chars().count(); | ||
| ui_label_muted( | ||
| 440.0, | ||
| row_y, | ||
| &format!("Message: {chars} chars · {lines} lines"), | ||
| 13.0, | ||
| ); | ||
| row_y += 28.0; | ||
|
|
||
| let prefs = format!( | ||
| "Remember: {} · Marketing: {}", | ||
| if remember { "yes" } else { "no" }, | ||
| if marketing { "on" } else { "off" }, | ||
| ); | ||
| ui_label_muted(440.0, row_y, &prefs, 13.0); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Avoid per-frame heap allocations in the render loop.
Line 114/116/126/131 allocate new Strings every frame (to_string/format!). In this hot path, that violates the allocation-minimal guest guideline and adds avoidable wasm overhead.
Suggested change (remove obvious per-frame allocations)
- let preview = if email.is_empty() {
- "(no email yet)".to_string()
- } else {
- format!("📧 {email}")
- };
- ui_label(440.0, row_y, &preview, 14.0);
+ if email.is_empty() {
+ ui_label(440.0, row_y, "(no email yet)", 14.0);
+ } else {
+ ui_label(440.0, row_y, email.as_str(), 14.0);
+ }As per coding guidelines, "{oxide-sdk,examples}/**/src/**/*.rs: Guest app code must remain allocation-minimal since examples run on wasm32-unknown-unknown with no std allocator by default unless alloc is explicitly linked".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut row_y = 376.0; | |
| let preview = if email.is_empty() { | |
| "(no email yet)".to_string() | |
| } else { | |
| format!("📧 {email}") | |
| }; | |
| ui_label(440.0, row_y, &preview, 14.0); | |
| row_y += 28.0; | |
| let lines = message.lines().count(); | |
| let chars = message.chars().count(); | |
| ui_label_muted( | |
| 440.0, | |
| row_y, | |
| &format!("Message: {chars} chars · {lines} lines"), | |
| 13.0, | |
| ); | |
| row_y += 28.0; | |
| let prefs = format!( | |
| "Remember: {} · Marketing: {}", | |
| if remember { "yes" } else { "no" }, | |
| if marketing { "on" } else { "off" }, | |
| ); | |
| ui_label_muted(440.0, row_y, &prefs, 13.0); | |
| let mut row_y = 376.0; | |
| if email.is_empty() { | |
| ui_label(440.0, row_y, "(no email yet)", 14.0); | |
| } else { | |
| ui_label(440.0, row_y, email.as_str(), 14.0); | |
| } | |
| row_y += 28.0; | |
| let lines = message.lines().count(); | |
| let chars = message.chars().count(); | |
| ui_label_muted( | |
| 440.0, | |
| row_y, | |
| &format!("Message: {chars} chars · {lines} lines"), | |
| 13.0, | |
| ); | |
| row_y += 28.0; | |
| let prefs = format!( | |
| "Remember: {} · Marketing: {}", | |
| if remember { "yes" } else { "no" }, | |
| if marketing { "on" } else { "off" }, | |
| ); | |
| ui_label_muted(440.0, row_y, &prefs, 13.0); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/shadcn-demo/src/lib.rs` around lines 112 - 136, The render loop
currently creates new Strings each frame via to_string()/format! for the email
preview, message stats, and prefs, which causes per-frame heap allocations; fix
this by reusing preallocated mutable String buffers (e.g., preview_buf,
stats_buf, prefs_buf) declared outside the hot render loop, clear them each
frame and write into them with write! or push_str/format_to-like operations, and
pass their &str slices to ui_label and ui_label_muted (or for the email preview
use a borrowed &str when email.is_empty() -> "(no email yet)" to avoid
allocation); update the code paths that build preview, the Message: ... string,
and prefs (references to variables email, message, remember, marketing,
ui_label, ui_label_muted) to use these reusable buffers instead of
to_string()/format!.
| // Ensure each editable widget has a stable bounds cache so mouse hit-tests | ||
| // can locate the text canvas after layout. | ||
| for cmd in &widget_commands { | ||
| match cmd { | ||
| WidgetCommand::TextInput { id, .. } | WidgetCommand::Textarea { id, .. } => { | ||
| self.tabs[active] | ||
| .widget_bounds_cache | ||
| .entry(*id) | ||
| .or_insert_with(|| Arc::new(Mutex::new(Bounds::default()))); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| let widget_bounds_cache_snapshot = self.tabs[active].widget_bounds_cache.clone(); | ||
|
|
There was a problem hiding this comment.
Prune stale editable-widget caches before inserting current-frame entries.
widget_edits and widget_bounds_cache only grow. If guests emit transient/incrementing widget IDs, these maps grow unbounded and can leave stale text_input_focus IDs around.
💡 Suggested fix
- // Ensure each editable widget has a stable bounds cache so mouse hit-tests
- // can locate the text canvas after layout.
- for cmd in &widget_commands {
- match cmd {
- WidgetCommand::TextInput { id, .. } | WidgetCommand::Textarea { id, .. } => {
- self.tabs[active]
- .widget_bounds_cache
- .entry(*id)
- .or_insert_with(|| Arc::new(Mutex::new(Bounds::default())));
- }
- _ => {}
- }
- }
+ let editable_ids: HashSet<u32> = widget_commands
+ .iter()
+ .filter_map(|cmd| match cmd {
+ WidgetCommand::TextInput { id, .. } | WidgetCommand::Textarea { id, .. } => {
+ Some(*id)
+ }
+ _ => None,
+ })
+ .collect();
+
+ {
+ let tab = &mut self.tabs[active];
+ tab.widget_edits.retain(|id, _| editable_ids.contains(id));
+ tab.widget_bounds_cache
+ .retain(|id, _| editable_ids.contains(id));
+ if let Some(focused_id) = tab.text_input_focus {
+ if !editable_ids.contains(&focused_id) {
+ tab.text_input_focus = None;
+ }
+ }
+ for id in &editable_ids {
+ tab.widget_bounds_cache
+ .entry(*id)
+ .or_insert_with(|| Arc::new(Mutex::new(Bounds::default())));
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Ensure each editable widget has a stable bounds cache so mouse hit-tests | |
| // can locate the text canvas after layout. | |
| for cmd in &widget_commands { | |
| match cmd { | |
| WidgetCommand::TextInput { id, .. } | WidgetCommand::Textarea { id, .. } => { | |
| self.tabs[active] | |
| .widget_bounds_cache | |
| .entry(*id) | |
| .or_insert_with(|| Arc::new(Mutex::new(Bounds::default()))); | |
| } | |
| _ => {} | |
| } | |
| } | |
| let widget_bounds_cache_snapshot = self.tabs[active].widget_bounds_cache.clone(); | |
| let editable_ids: HashSet<u32> = widget_commands | |
| .iter() | |
| .filter_map(|cmd| match cmd { | |
| WidgetCommand::TextInput { id, .. } | WidgetCommand::Textarea { id, .. } => { | |
| Some(*id) | |
| } | |
| _ => None, | |
| }) | |
| .collect(); | |
| { | |
| let tab = &mut self.tabs[active]; | |
| tab.widget_edits.retain(|id, _| editable_ids.contains(id)); | |
| tab.widget_bounds_cache | |
| .retain(|id, _| editable_ids.contains(id)); | |
| if let Some(focused_id) = tab.text_input_focus { | |
| if !editable_ids.contains(&focused_id) { | |
| tab.text_input_focus = None; | |
| } | |
| } | |
| for id in &editable_ids { | |
| tab.widget_bounds_cache | |
| .entry(*id) | |
| .or_insert_with(|| Arc::new(Mutex::new(Bounds::default()))); | |
| } | |
| } | |
| let widget_bounds_cache_snapshot = self.tabs[active].widget_bounds_cache.clone(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@oxide-browser/src/ui.rs` around lines 3989 - 4003, The widget_edits and
widget_bounds_cache maps are never pruned and can grow unbounded; before
inserting current-frame entries in the loop over widget_commands (the match
handling WidgetCommand::TextInput and WidgetCommand::Textarea), compute the set
of current editable widget IDs from widget_commands, then remove any keys from
self.tabs[active].widget_edits and self.tabs[active].widget_bounds_cache that
are not in that set (and also clear any stale text_input_focus if its id is no
longer present); after pruning, proceed to or_insert the Bounds and other
per-frame entries as you already do.
| let mut text = tab | ||
| .host_state | ||
| .widget_states | ||
| .lock() | ||
| .unwrap() | ||
| .get(&id) | ||
| .and_then(|v| match v { | ||
| WidgetValue::Text(t) => Some(t.clone()), | ||
| _ => None, | ||
| }) | ||
| .unwrap_or_default(); | ||
| let mut edit = tab.widget_edits.get(&id).cloned().unwrap_or_default(); | ||
|
|
There was a problem hiding this comment.
Guard widget edit offsets against stale UTF-8 boundaries.
Line 4811 reuses persisted cursor/selection offsets without normalizing to the current text. If guest code changes widget text between frames, subsequent &text[range], replace_range, or insert_str can panic on invalid boundaries/ranges.
💡 Suggested fix
+fn clamp_utf8_boundary(text: &str, mut idx: usize) -> usize {
+ idx = idx.min(text.len());
+ while idx > 0 && !text.is_char_boundary(idx) {
+ idx -= 1;
+ }
+ idx
+}
+
fn handle_widget_key(view: &mut OxideBrowserView, id: u32, event: &KeyDownEvent) {
@@
- let mut edit = tab.widget_edits.get(&id).cloned().unwrap_or_default();
+ let mut edit = tab.widget_edits.get(&id).cloned().unwrap_or_default();
+ edit.cursor = clamp_utf8_boundary(&text, edit.cursor);
+ edit.sel_start = clamp_utf8_boundary(&text, edit.sel_start);Also applies to: 4825-4833, 4850-4858, 4974-5001
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@oxide-browser/src/ui.rs` around lines 4800 - 4812, The stored widget edit
offsets in tab.widget_edits (variable edit) are being reused against the freshly
loaded text (variable text) and can fall on invalid UTF-8 byte boundaries,
causing panics when slicing (&text[range]) or mutating (replace_range,
insert_str); before any slicing or mutation, validate and normalize the edit
offsets against the current text by clamping start/end to 0..=text.len() and
moving each offset to the nearest valid char boundary (using
str::is_char_boundary or scanning with char_indices) so ranges become safe;
apply this normalization wherever edit offsets are used (the locations around
the retrieval of text/edit and before any replace_range/insert_str usages) and
consider adding a small helper (e.g., normalize_widget_edit_offsets(edit,
&text)) to centralize the logic.
... ,cards, badges, separators, progress, labels
This adds a rich set of UI widget primitives to the Oxide host/SDK boundary, modelled after the shadcn/ui design system. Guests can now compose native-looking UIs entirely through the FFI widget layer. Key changes:
Host (oxide-browser):
WidgetVariantenum (Default, Secondary, Outline, Ghost, Destructive) with colour scheme helpers (variant_colors, variant_hover_bg)WidgetCommandwith 8 new widget types: • Textarea — multi-line text input with scroll support • Card — container with title/description, border, rounded corners • Badge — pill-shaped status indicator with variant support • Switch — pill-shaped on/off toggle (bool state in WidgetValue) • Separator — 1px horizontal or vertical divider • Progress — horizontal progress bar (0.0..=1.0) • Label — static text label with muted variant and font size controlwidget_boundsnow returns Option — decorative widgets (Badge, Separator, Progress, Label) do not block canvas click-throughSDK (oxide-sdk):
UiVariantenum withas_u32()codecui_button_variant(id, x, y, w, h, label, variant)— builder with styleui_text_inputnow takes placeholder;ui_text_input_with_valueseeds initui_textarea/ui_textarea_with_value— multi-line with 16 KiB bufferui_switch,ui_card,ui_badge,ui_separator,ui_separator_vertical,ui_progress,ui_label,ui_label_muted— complete surface areaExample (examples/shadcn-demo):
Example (examples/hello-oxide):
Builds:
Summary by CodeRabbit
Release Notes