Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ members = [
"examples/file-picker-demo",
"examples/gradient-demo",
"examples/typography-demo",
"examples/shadcn-demo",
]
resolver = "2"
2 changes: 1 addition & 1 deletion examples/hello-oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub extern "C" fn on_frame(_dt_ms: u32) {
// ── Text input demo ─────────────────────────────────────────────
canvas_text(20.0, 355.0, 16.0, 180, 140, 255, 255, "Text Input");

let name = ui_text_input(30, 20.0, 380.0, 300.0, "");
let name = ui_text_input(30, 20.0, 380.0, 300.0, "Type your name…");
if !name.is_empty() {
canvas_text(
20.0,
Expand Down
11 changes: 11 additions & 0 deletions examples/shadcn-demo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "shadcn-demo"
version = "0.1.0"
edition = "2021"
description = "Tour of Oxide's shadcn/ui-inspired widget primitives"

[lib]
crate-type = ["cdylib"]

[dependencies]
oxide-sdk = { path = "../../oxide-sdk" }
156 changes: 156 additions & 0 deletions examples/shadcn-demo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! Tour of Oxide's shadcn/ui-inspired widget primitives.
//!
//! Each section demonstrates one component family. Run with:
//!
//! ```bash
//! cargo build --target wasm32-unknown-unknown --release -p shadcn-demo
//! ```
//!
//! Then open the resulting `.wasm` from `target/wasm32-unknown-unknown/release/shadcn_demo.wasm`
//! inside the Oxide browser (File → Open) or pass its path on the command line.

use oxide_sdk::*;

#[no_mangle]
pub extern "C" fn start_app() {
log("shadcn-demo loaded");
}

#[no_mangle]
pub extern "C" fn on_frame(_dt_ms: u32) {
// Match the host's dark surface so the page feels native.
canvas_clear(0x0a, 0x0a, 0x0b, 0xff);
set_content_size(960, 940);

// ── Header ──────────────────────────────────────────────────────
ui_label(24.0, 24.0, "Oxide UI Kit", 28.0);
ui_label_muted(
24.0,
58.0,
"shadcn/ui-inspired primitives rendered by the host.",
14.0,
);
ui_separator(24.0, 92.0, 912.0);

// ── Buttons row ─────────────────────────────────────────────────
ui_label(24.0, 112.0, "Buttons", 16.0);
let _ = ui_button_variant(100, 24.0, 140.0, 110.0, 36.0, "Default", UiVariant::Default);
let _ = ui_button_variant(
101,
144.0,
140.0,
110.0,
36.0,
"Secondary",
UiVariant::Secondary,
);
let _ = ui_button_variant(
102,
264.0,
140.0,
110.0,
36.0,
"Outline",
UiVariant::Outline,
);
let _ = ui_button_variant(103, 384.0, 140.0, 110.0, 36.0, "Ghost", UiVariant::Ghost);
let _ = ui_button_variant(
104,
504.0,
140.0,
110.0,
36.0,
"Destructive",
UiVariant::Destructive,
);

// ── Badges ──────────────────────────────────────────────────────
ui_label(24.0, 204.0, "Badges", 16.0);
ui_badge(24.0, 232.0, "Default", UiVariant::Default);
ui_badge(108.0, 232.0, "Secondary", UiVariant::Secondary);
ui_badge(204.0, 232.0, "Outline", UiVariant::Outline);
ui_badge(288.0, 232.0, "Destructive", UiVariant::Destructive);
ui_badge(396.0, 232.0, "v0.7.0", UiVariant::Ghost);

ui_separator(24.0, 276.0, 912.0);

// ── Form section: inputs + switches ─────────────────────────────
ui_label(24.0, 296.0, "Form", 16.0);

ui_label_muted(24.0, 326.0, "Email", 13.0);
let email = ui_text_input(200, 24.0, 348.0, 360.0, "name@example.com");

ui_label_muted(24.0, 396.0, "Password", 13.0);
let _password = ui_text_input(201, 24.0, 418.0, 360.0, "Enter a password");

ui_label_muted(24.0, 466.0, "Message", 13.0);
let message = ui_textarea(
202,
24.0,
488.0,
360.0,
110.0,
"Tell us what's on your mind…",
);

let remember = ui_checkbox(210, 24.0, 614.0, "Remember me", true);
let marketing = ui_switch(211, 200.0, 614.0, "Marketing emails", false);

let _submit = ui_button_variant(220, 24.0, 654.0, 120.0, 36.0, "Sign in", UiVariant::Default);
let _cancel = ui_button_variant(221, 156.0, 654.0, 100.0, 36.0, "Cancel", UiVariant::Ghost);

// ── Live preview card ───────────────────────────────────────────
ui_card(
420.0,
296.0,
516.0,
296.0,
"Live preview",
"Every field updates this card in real time.",
);

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);
Comment on lines +112 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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!.


// ── Sliders + progress ─────────────────────────────────────────
ui_label(24.0, 720.0, "Slider & progress", 16.0);

let volume = ui_slider(300, 24.0, 752.0, 360.0, 0.0, 100.0, 32.0);
ui_label_muted(400.0, 756.0, &format!("Volume {volume:.0}%"), 13.0);

let goal = ui_slider(301, 24.0, 794.0, 360.0, 0.0, 1.0, 0.6);
ui_label_muted(400.0, 798.0, "Goal progress", 13.0);

ui_progress(24.0, 832.0, 912.0, goal);

ui_separator(24.0, 868.0, 912.0);
ui_label_muted(
24.0,
884.0,
"Tip: arrow keys, shift-arrows, Cmd/Ctrl+A/C/X/V all work inside text fields.",
12.0,
);
}
Loading
Loading