// In-app toast overlay — the Slint port of Tauri's Toast.svelte. // // Mounted once at the app root (app.slint) so it floats over every // screen. It renders whatever `ToastState` holds; the Rust layer // (crate::toast) owns showing + auto-hide timing. A single toast at a // time, anchored bottom-center, matching Tauri (bottom: 100px, centered). // // The toast is always in the tree (never `if`-gated) so the entry // animation runs: opacity + a small upward slide are driven off // `ToastState.visible`. The close TouchArea is disabled while hidden so // the (transparent) overlay never eats clicks meant for the content // below it. import { Theme } from "../foundation/semantic-colors.slint"; import { QbzIcon } from "QbzIcon.slint"; import { ToastState, ToastKind } from "../state.slint"; export component Toast inherits Rectangle { // Full-bleed, non-interactive layer (no background, no TouchArea on // the root, so pointer events pass through to the app underneath). property kind: ToastState.kind; // Per-kind icon, matching Tauri's lucide set. property icon-source: root.kind == ToastKind.success ? @image-url("../assets/icons/circle-check-big.svg") : root.kind == ToastKind.error ? @image-url("../assets/icons/circle-alert.svg") : root.kind == ToastKind.warning ? @image-url("../assets/icons/triangle-alert.svg") : root.kind == ToastKind.buffering ? @image-url("../assets/icons/loader-circle.svg") : @image-url("../assets/icons/info.svg"); // Per-kind accent, matching Tauri's colors. property icon-color: root.kind == ToastKind.success ? #4CAF50 : root.kind == ToastKind.error ? #ff6b6b : root.kind == ToastKind.warning ? #FFA726 : Theme.accent; toast := Rectangle { // Hug the content, but never spill past the window edges. width: min(content.preferred-width, root.width - 80px); height: 44px; x: (root.width - self.width) / 2; // Bottom-center, ~100px up from the bottom (Tauri parity). Slides // up the last 16px as it appears. y: parent.height - self.height - (ToastState.visible ? 100px : 84px); opacity: ToastState.visible ? 1.0 : 0.0; animate opacity { duration: 200ms; easing: ease-out; } animate y { duration: 200ms; easing: ease-out; } background: Theme.surface-elevated; border-radius: 8px; drop-shadow-blur: 16px; drop-shadow-color: #00000066; content := HorizontalLayout { padding-left: 16px; padding-right: 8px; spacing: 12px; // Type icon. VerticalLayout { alignment: center; horizontal-stretch: 0; QbzIcon { source: root.icon-source; width: 20px; height: 20px; tint: root.icon-color; } } // Message. Single line, elided if it would overflow the cap. VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: ToastState.message; color: Theme.text-primary; font-size: 14px; vertical-alignment: center; overflow: elide; } } // Close button. VerticalLayout { alignment: center; horizontal-stretch: 0; Rectangle { width: 24px; height: 24px; border-radius: 4px; background: close-ta.has-hover ? Theme.surface-hover : transparent; QbzIcon { source: @image-url("../assets/icons/x.svg"); width: 16px; height: 16px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: close-ta.has-hover ? Theme.text-primary : Theme.text-muted; } close-ta := TouchArea { // Only live while a toast is actually showing, so // the hidden overlay doesn't capture clicks. enabled: ToastState.visible; mouse-cursor: pointer; clicked => { ToastState.visible = false; } } } } } } }