Compare commits

..

3 Commits

Author SHA1 Message Date
79fd98899e init 2026-01-26 23:40:09 +08:00
24bf9dc3dd init 2026-01-26 23:40:05 +08:00
1ec60e3cbb init 2026-01-26 21:58:59 +08:00
8 changed files with 295 additions and 190 deletions

56
Cargo.lock generated
View File

@@ -1147,27 +1147,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "femtovg"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be5d925785ad33d7b0ae2b445d9f157c3ab42ff3c515fff0b46d53d4a86c43c5"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"fnv",
"glow",
"image",
"imgref",
"itertools 0.14.0",
"log",
"rgb",
"slotmap",
"ttf-parser 0.25.1",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "field-offset"
version = "0.3.6"
@@ -1211,12 +1190,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -1659,8 +1632,9 @@ dependencies = [
"glutin",
"i-slint-common",
"i-slint-core",
"i-slint-renderer-femtovg",
"i-slint-renderer-skia",
"input",
"memmap2",
"nix",
"raw-window-handle",
"xkbcommon",
@@ -1696,7 +1670,6 @@ dependencies = [
"i-slint-common",
"i-slint-core",
"i-slint-core-macros",
"i-slint-renderer-femtovg",
"i-slint-renderer-skia",
"lyon_path",
"muda",
@@ -1811,29 +1784,6 @@ dependencies = [
"syn",
]
[[package]]
name = "i-slint-renderer-femtovg"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d85d77f899ecb1f05c50c659a6d356fba44463686713f1f925c8be48de6afb"
dependencies = [
"cfg-if",
"const-field-offset",
"derive_more",
"femtovg",
"glow",
"i-slint-common",
"i-slint-core",
"i-slint-core-macros",
"imgref",
"lyon_path",
"pin-weak",
"rgb",
"ttf-parser 0.25.1",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "i-slint-renderer-skia"
version = "1.14.1"
@@ -3779,7 +3729,6 @@ dependencies = [
"i-slint-backend-selector",
"i-slint-core",
"i-slint-core-macros",
"i-slint-renderer-femtovg",
"num-traits",
"once_cell",
"pin-weak",
@@ -3925,6 +3874,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
dependencies = [
"as-raw-xcb-connection",
"bytemuck",
"drm",
"fastrand",
"js-sys",
"memmap2",

View File

@@ -4,11 +4,14 @@ version = "0.0.1"
edition = "2024"
[dependencies]
slint = { version = "1.14", default-features = false, features = ["backend-winit", "renderer-femtovg", "compat-1-2"] }
slint = { version = "1.14", default-features = false, features = ["backend-winit", "renderer-winit-skia", "compat-1-2"] }
[build-dependencies]
slint-build = "1.14"
[features]
default = []
lite = [] # Lightweight demo with minimal memory footprint
[profile.release]
opt-level = 'z' # 优化代码体积

View File

@@ -1,3 +1,12 @@
fn main() {
slint_build::compile("ui/demo.slint").unwrap();
// Use SLINT_DEMO environment variable to choose demo version
// cargo build -> full demo
// cargo build --features lite -> lite demo
let demo_file = if cfg!(feature = "lite") {
"ui/demo-lite.slint"
} else {
"ui/demo.slint"
};
slint_build::compile(demo_file).unwrap();
}

View File

@@ -1,59 +1,72 @@
slint::include_modules!();
#[cfg(not(feature = "lite"))]
use slint::{ComponentHandle, Model, ModelRc, VecModel};
#[cfg(not(feature = "lite"))]
use std::rc::Rc;
fn main() -> Result<(), slint::PlatformError> {
let ui = Demo::new()?;
#[cfg(feature = "lite")]
{
// Lite version - minimal memory footprint
let ui = DemoLite::new()?;
ui.run()
}
// Toast management
let toasts: Rc<VecModel<ToastMessage>> = Rc::new(VecModel::default());
ui.set_toasts(ModelRc::from(toasts.clone()));
#[cfg(not(feature = "lite"))]
{
// Full version with all features
let ui = Demo::new()?;
// Handle add-task callback
let ui_weak = ui.as_weak();
let toasts_clone = toasts.clone();
ui.on_add_task(move |task_title| {
let ui = ui_weak.unwrap();
// Toast management
let toasts: Rc<VecModel<ToastMessage>> = Rc::new(VecModel::default());
ui.set_toasts(ModelRc::from(toasts.clone()));
// Show success toast
toasts_clone.push(ToastMessage {
message: format!("Task '{}' added successfully!", task_title).into(),
variant: "success".into(),
show: true,
// Handle add-task callback
let ui_weak = ui.as_weak();
let toasts_clone = toasts.clone();
ui.on_add_task(move |task_title| {
let ui = ui_weak.unwrap();
// Show success toast
toasts_clone.push(ToastMessage {
message: format!("Task '{}' added successfully!", task_title).into(),
variant: "success".into(),
show: true,
});
// Auto-dismiss after 3 seconds
let toasts_clone2 = toasts_clone.clone();
let index = toasts_clone.row_count() - 1;
slint::Timer::single_shot(std::time::Duration::from_secs(3), move || {
if index < toasts_clone2.row_count() {
toasts_clone2.remove(index);
}
});
// Clear input
ui.set_new_task_title("".into());
});
// Auto-dismiss after 3 seconds
let toasts_clone2 = toasts_clone.clone();
let index = toasts_clone.row_count() - 1;
slint::Timer::single_shot(std::time::Duration::from_secs(3), move || {
if index < toasts_clone2.row_count() {
toasts_clone2.remove(index);
}
// Handle show-toast callback
let toasts_clone = toasts.clone();
ui.on_show_toast(move |message, variant| {
toasts_clone.push(ToastMessage {
message: message.clone(),
variant: variant.clone(),
show: true,
});
// Auto-dismiss after 3 seconds
let toasts_clone2 = toasts_clone.clone();
let index = toasts_clone.row_count() - 1;
slint::Timer::single_shot(std::time::Duration::from_secs(3), move || {
if index < toasts_clone2.row_count() {
toasts_clone2.remove(index);
}
});
});
// Clear input
ui.set_new_task_title("".into());
});
// Handle show-toast callback
let toasts_clone = toasts.clone();
ui.on_show_toast(move |message, variant| {
toasts_clone.push(ToastMessage {
message: message.clone(),
variant: variant.clone(),
show: true,
});
// Auto-dismiss after 3 seconds
let toasts_clone2 = toasts_clone.clone();
let index = toasts_clone.row_count() - 1;
slint::Timer::single_shot(std::time::Duration::from_secs(3), move || {
if index < toasts_clone2.row_count() {
toasts_clone2.remove(index);
}
});
});
ui.run()
ui.run()
}
}

View File

@@ -14,20 +14,10 @@ export component Button {
// Callbacks
callback clicked();
// Calculate background color based on variant
private property <color> base-bg-color: {
if disabled { Theme.colors.muted }
else if variant == "destructive" { Theme.colors.destructive }
else if variant == "outline" { Colors.transparent }
else if variant == "secondary" { Theme.colors.secondary }
else if variant == "ghost" { Colors.transparent }
else { Theme.colors.primary }
};
// Calculate text color based on variant
// Calculate text color based on variant (shadcn design tokens)
private property <color> text-color: {
if disabled { Theme.colors.muted-foreground }
else if variant == "destructive" { Theme.colors.destructive-foreground }
else if variant == "destructive" { Theme.colors.destructive } // destructive uses destructive color for text
else if variant == "outline" { Theme.colors.foreground }
else if variant == "secondary" { Theme.colors.secondary-foreground }
else if variant == "ghost" { Theme.colors.foreground }
@@ -42,12 +32,67 @@ export component Button {
min-width: btn-height;
min-height: btn-height;
// Focus scope for keyboard navigation
focus-scope := FocusScope {
enabled: !disabled;
key-pressed(event) => {
if (event.text == " " || event.text == "\n") {
root.clicked();
return accept;
}
return reject;
}
}
// Main container
container := Rectangle {
background: base-bg-color;
border-radius: SpacingSystem.radius.md;
border-width: variant == "outline" ? 1px : 0px;
border-color: Theme.colors.border;
// Outline variant always has border
border-width: root.variant == "outline" ? 1px : 0px;
border-color: root.variant == "outline" ? Theme.colors.border : Colors.transparent;
// Touch interaction area (must be defined before using touch.pressed/has-hover)
touch := TouchArea {
enabled: !disabled;
clicked => {
// Don't set focus on mouse click (shadcn style)
root.clicked();
}
}
// Calculate background color based on variant and state (shadcn official design)
background: {
if root.disabled {
Theme.colors.muted
} else if root.variant == "destructive" {
// destructive: bg-destructive/10 hover:bg-destructive/20
if touch.pressed { Theme.colors.destructive.with-alpha(0.25) }
else if touch.has-hover { Theme.colors.destructive.with-alpha(0.2) }
else { Theme.colors.destructive.with-alpha(0.1) }
} else if root.variant == "outline" {
// outline: bg-background hover:bg-muted
if touch.pressed { Theme.colors.muted.darker(0.05) }
else if touch.has-hover { Theme.colors.muted }
else { Theme.colors.background }
} else if root.variant == "secondary" {
// secondary: bg-secondary hover:bg-secondary/80
if touch.pressed { Theme.colors.secondary.with-alpha(0.7) }
else if touch.has-hover { Theme.colors.secondary.with-alpha(0.8) }
else { Theme.colors.secondary }
} else if root.variant == "ghost" {
// ghost: transparent hover:bg-muted
if touch.pressed { Theme.colors.muted.darker(0.05) }
else if touch.has-hover { Theme.colors.muted }
else { Colors.transparent }
} else {
// default (primary): bg-primary hover:bg-primary/80
if touch.pressed { Theme.colors.primary.with-alpha(0.7) }
else if touch.has-hover { Theme.colors.primary.with-alpha(0.8) }
else { Theme.colors.primary }
}
};
// Smooth transitions
animate background {
@@ -73,37 +118,18 @@ export component Button {
}
}
// Touch interaction area
touch := TouchArea {
enabled: !disabled;
clicked => {
root.clicked();
}
}
// Combined overlay for both hover and press effects
overlay := Rectangle {
// Focus ring only for keyboard navigation (shadcn style)
// Will only show when using Tab key, not on mouse click
focus-ring := Rectangle {
width: 100%;
height: 100%;
border-radius: SpacingSystem.radius.md;
border-width: focus-scope.has-focus ? 2px : 0px;
border-color: Theme.colors.ring;
// 直接根据状态设置背景色,不使用 states 块
background: {
if disabled {
Colors.transparent
} else if touch.pressed {
#00000030 // 按压时深色
} else if touch.has-hover {
#ffffff10 // 悬停时浅色
} else {
Colors.transparent
}
};
animate background {
duration: 200ms;
easing: ease-in-out;
animate border-width {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
}
}

102
ui/demo-lite.slint Normal file
View File

@@ -0,0 +1,102 @@
// Lightweight Demo Application
// Minimal memory footprint version
import { Theme, Typography, SpacingSystem } from "theme/theme.slint";
import { Button } from "components/button.slint";
import { Input } from "components/input.slint";
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "components/card.slint";
import { Badge } from "components/badge.slint";
export component DemoLite inherits Window {
title: "Slint Shadcn UI - Lite Demo";
background: Theme.colors.background;
// Smaller window size
min-width: 500px;
min-height: 400px;
preferred-width: 700px;
preferred-height: 500px;
// Minimal state
in-out property <string> input-text: "";
VerticalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s4;
// Header
HorizontalLayout {
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
Text {
text: "Shadcn UI Demo";
font-size: Typography.sizes.xl;
font-weight: Typography.weights.bold;
color: Theme.colors.foreground;
}
Button {
text: Theme.is-dark-mode ? "☀" : "🌙";
variant: "outline";
size: "sm";
clicked => { Theme.toggle-theme(); }
}
}
// Main card
Card {
CardHeader {
CardTitle { text: "Button Showcase"; }
CardDescription { text: "Various button styles"; }
}
CardContent {
VerticalLayout {
spacing: SpacingSystem.spacing.s3;
// Button variants
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Button { text: "Default"; }
Button { text: "Secondary"; variant: "secondary"; }
Button { text: "Outline"; variant: "outline"; }
Button { text: "Ghost"; variant: "ghost"; }
}
// Destructive button
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Button { text: "Destructive"; variant: "destructive"; }
Button { text: "Disabled"; disabled: true; }
}
}
}
}
// Input card
Card {
CardHeader {
CardTitle { text: "Input & Badge"; }
}
CardContent {
VerticalLayout {
spacing: SpacingSystem.spacing.s3;
Input {
value <=> input-text;
placeholder: "Type something...";
}
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Badge { text: "Default"; }
Badge { text: "Secondary"; variant: "secondary"; }
Badge { text: "Outline"; variant: "outline"; }
}
}
}
}
}
}

View File

@@ -15,11 +15,11 @@ export component Demo inherits Window {
title: "Slint Shadcn UI - Task Manager Demo";
background: Theme.colors.background;
// Window size settings - reduced for lower memory usage
min-width: 800px;
min-height: 600px;
preferred-width: 1000px;
preferred-height: 700px;
// Window size settings - optimized for lower memory usage
min-width: 600px;
min-height: 400px;
preferred-width: 800px;
preferred-height: 600px;
// Application state
in-out property <string> new-task-title: "";

View File

@@ -1,5 +1,6 @@
// Shadcn-style Color System
// Based on shadcn/ui zinc theme with light/dark mode support
// Based on shadcn/ui official neutral theme with light/dark mode support
// Source: https://github.com/shadcn-ui/ui/blob/main/apps/v4/styles/globals.css
// Color palette structure
export struct ColorPalette {
@@ -24,52 +25,53 @@ export struct ColorPalette {
ring: color,
}
// Light mode colors - shadcn zinc theme
// Light mode colors - shadcn official neutral theme
// oklch values converted to hex for Slint compatibility
export global LightColors {
out property <ColorPalette> palette: {
background: #ffffff,
foreground: #09090b,
card: #ffffff,
card-foreground: #09090b,
popover: #ffffff,
popover-foreground: #09090b,
primary: #18181b,
primary-foreground: #fafafa,
secondary: #f4f4f5,
secondary-foreground: #18181b,
muted: #f4f4f5,
muted-foreground: #71717a,
accent: #f4f4f5,
accent-foreground: #18181b,
destructive: #ef4444,
destructive-foreground: #fafafa,
border: #e4e4e7,
input: #e4e4e7,
ring: #18181b,
background: #ffffff, // oklch(1 0 0)
foreground: #252525, // oklch(0.145 0 0)
card: #ffffff, // oklch(1 0 0)
card-foreground: #252525, // oklch(0.145 0 0)
popover: #ffffff, // oklch(1 0 0)
popover-foreground: #252525, // oklch(0.145 0 0)
primary: #343434, // oklch(0.205 0 0)
primary-foreground: #fbfbfb, // oklch(0.985 0 0)
secondary: #f7f7f7, // oklch(0.97 0 0)
secondary-foreground: #343434, // oklch(0.205 0 0)
muted: #f7f7f7, // oklch(0.97 0 0)
muted-foreground: #8e8e8e, // oklch(0.556 0 0)
accent: #f7f7f7, // oklch(0.97 0 0)
accent-foreground: #343434, // oklch(0.205 0 0)
destructive: #ef4444, // oklch(0.577 0.245 27.325)
destructive-foreground: #f7f7f7, // oklch(0.97 0.01 17) - light red/pink, not white!
border: #ebebeb, // oklch(0.922 0 0)
input: #ebebeb, // oklch(0.922 0 0)
ring: #b4b4b4, // oklch(0.708 0 0)
};
}
// Dark mode colors - shadcn zinc theme
// Dark mode colors - shadcn official neutral theme
export global DarkColors {
out property <ColorPalette> palette: {
background: #09090b,
foreground: #fafafa,
card: #09090b,
card-foreground: #fafafa,
popover: #09090b,
popover-foreground: #fafafa,
primary: #fafafa,
primary-foreground: #18181b,
secondary: #27272a,
secondary-foreground: #fafafa,
muted: #27272a,
muted-foreground: #a1a1aa,
accent: #27272a,
accent-foreground: #fafafa,
destructive: #7f1d1d,
destructive-foreground: #fafafa,
border: #27272a,
input: #27272a,
ring: #d4d4d8,
background: #252525, // oklch(0.145 0 0)
foreground: #fbfbfb, // oklch(0.985 0 0)
card: #343434, // oklch(0.205 0 0)
card-foreground: #fbfbfb, // oklch(0.985 0 0)
popover: #444444, // oklch(0.269 0 0)
popover-foreground: #fbfbfb, // oklch(0.985 0 0)
primary: #ebebeb, // oklch(0.922 0 0)
primary-foreground: #343434, // oklch(0.205 0 0)
secondary: #444444, // oklch(0.269 0 0)
secondary-foreground: #fbfbfb, // oklch(0.985 0 0)
muted: #444444, // oklch(0.269 0 0)
muted-foreground: #b4b4b4, // oklch(0.708 0 0)
accent: #5e5e5e, // oklch(0.371 0 0)
accent-foreground: #fbfbfb, // oklch(0.985 0 0)
destructive: #dc2626, // oklch(0.704 0.191 22.216)
destructive-foreground: #ef4444, // oklch(0.58 0.22 27) - red color, not white!
border: #1a1a1a, // oklch(1 0 0 / 10%) - 10% white on dark
input: #262626, // oklch(1 0 0 / 15%) - 15% white on dark
ring: #8e8e8e, // oklch(0.556 0 0)
};
}