Nuevas capacidades de rendering: - ShadowCommand: sombras multi-capa con blur simulado - Helpers: shadow(), shadowDrop(), shadowFloat() - Quadratic alpha falloff para bordes suaves - GradientCommand: gradientes suaves pixel a pixel - Direcciones: vertical, horizontal, diagonal - Helpers: gradientV/H(), gradientButton(), gradientProgress() - Soporte esquinas redondeadas Widgets actualizados: - Panel/Modal: sombras en fancy mode - Select/Menu: dropdown con sombra + rounded corners - Tooltip/Toast: sombra sutil + rounded corners - Button: gradiente 3D (lighten top, darken bottom) - Progress: gradientes suaves vs 4 bandas IMPORTANTE: Compila y pasa tests (370/370) pero NO probado visualmente 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
698 lines
20 KiB
Zig
698 lines
20 KiB
Zig
//! Toast/Notification Widget
|
|
//!
|
|
//! Non-blocking notifications that appear temporarily to inform users.
|
|
//!
|
|
//! ## Features
|
|
//! - Multiple toast types (info, success, warning, error)
|
|
//! - Configurable position and duration
|
|
//! - Stack multiple toasts
|
|
//! - Optional action buttons
|
|
//! - Auto-dismiss with countdown
|
|
//!
|
|
//! ## Usage
|
|
//! ```zig
|
|
//! var toasts = ToastManager.init();
|
|
//!
|
|
//! // Show a toast
|
|
//! toasts.info("File saved successfully");
|
|
//! toasts.warning("Low disk space");
|
|
//! toasts.error("Failed to connect");
|
|
//!
|
|
//! // In your render loop
|
|
//! toasts.render(ctx);
|
|
//! ```
|
|
|
|
const std = @import("std");
|
|
const Context = @import("../core/context.zig").Context;
|
|
const Command = @import("../core/command.zig");
|
|
const Layout = @import("../core/layout.zig");
|
|
const Style = @import("../core/style.zig");
|
|
const Rect = Layout.Rect;
|
|
const Color = Style.Color;
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
/// Toast notification type
|
|
pub const ToastType = enum {
|
|
info,
|
|
success,
|
|
warning,
|
|
@"error",
|
|
|
|
pub fn getColor(self: ToastType) Color {
|
|
const theme = Style.currentTheme();
|
|
return switch (self) {
|
|
.info => theme.primary,
|
|
.success => theme.success,
|
|
.warning => theme.warning,
|
|
.@"error" => theme.danger,
|
|
};
|
|
}
|
|
|
|
pub fn getIcon(self: ToastType) []const u8 {
|
|
return switch (self) {
|
|
.info => "i",
|
|
.success => "+",
|
|
.warning => "!",
|
|
.@"error" => "x",
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Toast position on screen
|
|
pub const Position = enum {
|
|
top_left,
|
|
top_center,
|
|
top_right,
|
|
bottom_left,
|
|
bottom_center,
|
|
bottom_right,
|
|
};
|
|
|
|
/// Toast configuration
|
|
pub const Config = struct {
|
|
/// Default duration in milliseconds
|
|
duration_ms: u32 = 4000,
|
|
/// Position on screen
|
|
position: Position = .bottom_right,
|
|
/// Maximum number of visible toasts
|
|
max_visible: u8 = 5,
|
|
/// Width of each toast
|
|
width: u16 = 300,
|
|
/// Padding inside toast
|
|
padding: u8 = 12,
|
|
/// Gap between toasts
|
|
gap: u8 = 8,
|
|
/// Show dismiss button
|
|
show_dismiss: bool = true,
|
|
/// Animate entrance/exit
|
|
animated: bool = true,
|
|
/// Margin from screen edge
|
|
margin: u16 = 16,
|
|
};
|
|
|
|
/// Toast colors
|
|
pub const Colors = struct {
|
|
background: Color,
|
|
text: Color,
|
|
icon_bg: Color,
|
|
border: Color,
|
|
|
|
pub fn fromTheme(toast_type: ToastType) Colors {
|
|
const theme = Style.currentTheme();
|
|
return .{
|
|
.background = theme.surface,
|
|
.text = theme.text_primary,
|
|
.icon_bg = toast_type.getColor(),
|
|
.border = theme.border,
|
|
};
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Toast Item
|
|
// =============================================================================
|
|
|
|
/// Individual toast notification
|
|
pub const Toast = struct {
|
|
/// Unique identifier
|
|
id: u32,
|
|
/// Toast type
|
|
toast_type: ToastType,
|
|
/// Message text
|
|
message: [256]u8,
|
|
message_len: usize,
|
|
/// Creation timestamp
|
|
created_ms: i64,
|
|
/// Duration in milliseconds (0 = persistent)
|
|
duration_ms: u32,
|
|
/// Whether it's being dismissed
|
|
dismissing: bool,
|
|
/// Animation progress (0-1)
|
|
animation: f32,
|
|
/// Action button text (if any)
|
|
action_text: [32]u8,
|
|
action_len: usize,
|
|
/// Whether action was clicked
|
|
action_clicked: bool,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn init(id: u32, toast_type: ToastType, message: []const u8, duration_ms: u32) Self {
|
|
var toast = Self{
|
|
.id = id,
|
|
.toast_type = toast_type,
|
|
.message = undefined,
|
|
.message_len = @min(message.len, 256),
|
|
.created_ms = std.time.milliTimestamp(),
|
|
.duration_ms = duration_ms,
|
|
.dismissing = false,
|
|
.animation = 0,
|
|
.action_text = undefined,
|
|
.action_len = 0,
|
|
.action_clicked = false,
|
|
};
|
|
@memcpy(toast.message[0..toast.message_len], message[0..toast.message_len]);
|
|
return toast;
|
|
}
|
|
|
|
pub fn getMessage(self: *const Self) []const u8 {
|
|
return self.message[0..self.message_len];
|
|
}
|
|
|
|
pub fn getAction(self: *const Self) ?[]const u8 {
|
|
if (self.action_len == 0) return null;
|
|
return self.action_text[0..self.action_len];
|
|
}
|
|
|
|
pub fn setAction(self: *Self, text: []const u8) void {
|
|
self.action_len = @min(text.len, 32);
|
|
@memcpy(self.action_text[0..self.action_len], text[0..self.action_len]);
|
|
}
|
|
|
|
pub fn shouldDismiss(self: *const Self) bool {
|
|
if (self.duration_ms == 0) return false;
|
|
const elapsed = std.time.milliTimestamp() - self.created_ms;
|
|
return elapsed >= self.duration_ms;
|
|
}
|
|
|
|
pub fn getRemainingMs(self: *const Self) i64 {
|
|
if (self.duration_ms == 0) return -1;
|
|
const elapsed = std.time.milliTimestamp() - self.created_ms;
|
|
return @max(0, @as(i64, self.duration_ms) - elapsed);
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Toast Manager
|
|
// =============================================================================
|
|
|
|
/// Maximum number of toasts to track
|
|
pub const MAX_TOASTS = 16;
|
|
|
|
/// Toast manager - handles multiple toasts
|
|
pub const Manager = struct {
|
|
/// Active toasts
|
|
toasts: [MAX_TOASTS]Toast,
|
|
/// Number of active toasts
|
|
count: usize,
|
|
/// Next toast ID
|
|
next_id: u32,
|
|
/// Configuration
|
|
config: Config,
|
|
|
|
const Self = @This();
|
|
|
|
/// Initialize toast manager
|
|
pub fn init() Self {
|
|
return initWithConfig(.{});
|
|
}
|
|
|
|
/// Initialize with custom config
|
|
pub fn initWithConfig(config: Config) Self {
|
|
return .{
|
|
.toasts = undefined,
|
|
.count = 0,
|
|
.next_id = 1,
|
|
.config = config,
|
|
};
|
|
}
|
|
|
|
// =========================================================================
|
|
// Show Methods
|
|
// =========================================================================
|
|
|
|
/// Show an info toast
|
|
pub fn info(self: *Self, message: []const u8) u32 {
|
|
return self.show(message, .info);
|
|
}
|
|
|
|
/// Show a success toast
|
|
pub fn success(self: *Self, message: []const u8) u32 {
|
|
return self.show(message, .success);
|
|
}
|
|
|
|
/// Show a warning toast
|
|
pub fn warning(self: *Self, message: []const u8) u32 {
|
|
return self.show(message, .warning);
|
|
}
|
|
|
|
/// Show an error toast
|
|
pub fn err(self: *Self, message: []const u8) u32 {
|
|
return self.show(message, .@"error");
|
|
}
|
|
|
|
/// Show a toast with specific type
|
|
pub fn show(self: *Self, message: []const u8, toast_type: ToastType) u32 {
|
|
return self.showWithDuration(message, toast_type, self.config.duration_ms);
|
|
}
|
|
|
|
/// Show a toast with custom duration
|
|
pub fn showWithDuration(self: *Self, message: []const u8, toast_type: ToastType, duration_ms: u32) u32 {
|
|
// Remove oldest if at capacity
|
|
if (self.count >= MAX_TOASTS) {
|
|
self.removeAt(0);
|
|
}
|
|
|
|
const id = self.next_id;
|
|
self.next_id += 1;
|
|
|
|
self.toasts[self.count] = Toast.init(id, toast_type, message, duration_ms);
|
|
self.count += 1;
|
|
|
|
return id;
|
|
}
|
|
|
|
/// Show a toast with action button
|
|
pub fn showWithAction(self: *Self, message: []const u8, toast_type: ToastType, action: []const u8) u32 {
|
|
const id = self.show(message, toast_type);
|
|
|
|
// Find and set action
|
|
var i: usize = 0;
|
|
while (i < self.count) : (i += 1) {
|
|
if (self.toasts[i].id == id) {
|
|
self.toasts[i].setAction(action);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Dismiss Methods
|
|
// =========================================================================
|
|
|
|
/// Dismiss a specific toast by ID
|
|
pub fn dismiss(self: *Self, id: u32) void {
|
|
var i: usize = 0;
|
|
while (i < self.count) : (i += 1) {
|
|
if (self.toasts[i].id == id) {
|
|
self.toasts[i].dismissing = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dismiss all toasts
|
|
pub fn dismissAll(self: *Self) void {
|
|
self.count = 0;
|
|
}
|
|
|
|
/// Remove toast at index
|
|
fn removeAt(self: *Self, index: usize) void {
|
|
if (index >= self.count) return;
|
|
|
|
// Shift remaining toasts down
|
|
var i = index;
|
|
while (i < self.count - 1) : (i += 1) {
|
|
self.toasts[i] = self.toasts[i + 1];
|
|
}
|
|
self.count -= 1;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update & Render
|
|
// =========================================================================
|
|
|
|
/// Update toast states (call each frame)
|
|
pub fn update(self: *Self) void {
|
|
var i: usize = 0;
|
|
while (i < self.count) {
|
|
// Check if should auto-dismiss
|
|
if (self.toasts[i].shouldDismiss() or self.toasts[i].dismissing) {
|
|
self.removeAt(i);
|
|
// Don't increment i since we removed an item
|
|
} else {
|
|
// Update animation
|
|
if (self.toasts[i].animation < 1.0) {
|
|
self.toasts[i].animation = @min(1.0, self.toasts[i].animation + 0.1);
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Render all toasts
|
|
pub fn render(self: *Self, ctx: *Context) ToastResult {
|
|
self.update();
|
|
|
|
var result = ToastResult{
|
|
.visible_count = 0,
|
|
.action_clicked = null,
|
|
};
|
|
|
|
if (self.count == 0) return result;
|
|
|
|
const screen_w = ctx.width;
|
|
const screen_h = ctx.height;
|
|
|
|
// Calculate starting position based on config
|
|
var base_x: i32 = 0;
|
|
var base_y: i32 = 0;
|
|
const toast_height: u32 = 60; // Approximate height
|
|
|
|
switch (self.config.position) {
|
|
.top_left => {
|
|
base_x = @intCast(self.config.margin);
|
|
base_y = @intCast(self.config.margin);
|
|
},
|
|
.top_center => {
|
|
base_x = @as(i32, @intCast(screen_w / 2)) - @as(i32, @intCast(self.config.width / 2));
|
|
base_y = @intCast(self.config.margin);
|
|
},
|
|
.top_right => {
|
|
base_x = @as(i32, @intCast(screen_w)) - @as(i32, @intCast(self.config.width + self.config.margin));
|
|
base_y = @intCast(self.config.margin);
|
|
},
|
|
.bottom_left => {
|
|
base_x = @intCast(self.config.margin);
|
|
base_y = @as(i32, @intCast(screen_h)) - @as(i32, @intCast(toast_height + self.config.margin));
|
|
},
|
|
.bottom_center => {
|
|
base_x = @as(i32, @intCast(screen_w / 2)) - @as(i32, @intCast(self.config.width / 2));
|
|
base_y = @as(i32, @intCast(screen_h)) - @as(i32, @intCast(toast_height + self.config.margin));
|
|
},
|
|
.bottom_right => {
|
|
base_x = @as(i32, @intCast(screen_w)) - @as(i32, @intCast(self.config.width + self.config.margin));
|
|
base_y = @as(i32, @intCast(screen_h)) - @as(i32, @intCast(toast_height + self.config.margin));
|
|
},
|
|
}
|
|
|
|
// Determine stack direction
|
|
const stack_down = switch (self.config.position) {
|
|
.top_left, .top_center, .top_right => true,
|
|
.bottom_left, .bottom_center, .bottom_right => false,
|
|
};
|
|
|
|
// Render visible toasts (most recent first or last based on position)
|
|
const visible_count = @min(self.count, @as(usize, self.config.max_visible));
|
|
var rendered: usize = 0;
|
|
|
|
while (rendered < visible_count) : (rendered += 1) {
|
|
const idx = if (stack_down)
|
|
rendered
|
|
else
|
|
self.count - 1 - rendered;
|
|
|
|
if (idx >= self.count) continue;
|
|
|
|
const toast = &self.toasts[idx];
|
|
const offset: i32 = @as(i32, @intCast(rendered)) * @as(i32, @intCast(toast_height + self.config.gap));
|
|
|
|
const y = if (stack_down)
|
|
base_y + offset
|
|
else
|
|
base_y - offset;
|
|
|
|
const toast_result = renderToast(ctx, toast, base_x, y, self.config);
|
|
|
|
if (toast_result.dismissed) {
|
|
toast.dismissing = true;
|
|
}
|
|
if (toast_result.action_clicked) {
|
|
result.action_clicked = toast.id;
|
|
}
|
|
|
|
result.visible_count += 1;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Get number of active toasts
|
|
pub fn getCount(self: *const Self) usize {
|
|
return self.count;
|
|
}
|
|
|
|
/// Check if a toast with given ID exists
|
|
pub fn exists(self: *const Self, id: u32) bool {
|
|
for (self.toasts[0..self.count]) |*toast| {
|
|
if (toast.id == id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Check if action was clicked for a toast
|
|
pub fn wasActionClicked(self: *Self, id: u32) bool {
|
|
for (self.toasts[0..self.count]) |*toast| {
|
|
if (toast.id == id and toast.action_clicked) {
|
|
toast.action_clicked = false;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/// Result from toast manager render
|
|
pub const ToastResult = struct {
|
|
/// Number of visible toasts
|
|
visible_count: usize,
|
|
/// ID of toast whose action was clicked (if any)
|
|
action_clicked: ?u32,
|
|
};
|
|
|
|
// =============================================================================
|
|
// Rendering Helper
|
|
// =============================================================================
|
|
|
|
const SingleToastResult = struct {
|
|
dismissed: bool,
|
|
action_clicked: bool,
|
|
};
|
|
|
|
fn renderToast(ctx: *Context, toast: *const Toast, x: i32, y: i32, config: Config) SingleToastResult {
|
|
var result = SingleToastResult{
|
|
.dismissed = false,
|
|
.action_clicked = false,
|
|
};
|
|
|
|
const colors = Colors.fromTheme(toast.toast_type);
|
|
const padding: i32 = @intCast(config.padding);
|
|
const width: u32 = config.width;
|
|
|
|
// Calculate height based on text (simplified - assume single line for now)
|
|
const height: u32 = 56;
|
|
|
|
// Check render mode for fancy features
|
|
const corner_radius: u8 = 6;
|
|
const fancy = Style.isFancy();
|
|
|
|
// Draw shadow first (behind toast) in fancy mode
|
|
if (fancy) {
|
|
ctx.pushCommand(Command.shadow(x, y, width, height, corner_radius));
|
|
}
|
|
|
|
// Draw background
|
|
if (fancy) {
|
|
ctx.pushCommand(Command.roundedRect(x, y, width, height, colors.background, corner_radius));
|
|
} else {
|
|
ctx.pushCommand(.{
|
|
.rect = .{
|
|
.x = x,
|
|
.y = y,
|
|
.w = width,
|
|
.h = height,
|
|
.color = colors.background,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Draw left accent bar
|
|
ctx.pushCommand(.{
|
|
.rect = .{
|
|
.x = x,
|
|
.y = y,
|
|
.w = 4,
|
|
.h = height,
|
|
.color = colors.icon_bg,
|
|
},
|
|
});
|
|
|
|
// Draw border
|
|
drawBorder(ctx, Rect.init(x, y, width, height), colors.border);
|
|
|
|
// Draw icon
|
|
const icon = toast.toast_type.getIcon();
|
|
ctx.pushCommand(.{
|
|
.text = .{
|
|
.x = x + padding,
|
|
.y = y + padding,
|
|
.text = icon,
|
|
.color = colors.icon_bg,
|
|
},
|
|
});
|
|
|
|
// Draw message
|
|
ctx.pushCommand(.{
|
|
.text = .{
|
|
.x = x + padding + 16,
|
|
.y = y + padding,
|
|
.text = toast.getMessage(),
|
|
.color = colors.text,
|
|
},
|
|
});
|
|
|
|
// Draw dismiss button if enabled
|
|
if (config.show_dismiss) {
|
|
const btn_x = x + @as(i32, @intCast(width)) - padding - 8;
|
|
const btn_y = y + padding;
|
|
|
|
ctx.pushCommand(.{
|
|
.text = .{
|
|
.x = btn_x,
|
|
.y = btn_y,
|
|
.text = "x",
|
|
.color = colors.text,
|
|
},
|
|
});
|
|
|
|
// Check for click on dismiss button
|
|
const mouse_x = ctx.input.mouse_x;
|
|
const mouse_y = ctx.input.mouse_y;
|
|
const btn_rect = Rect.init(btn_x - 4, btn_y - 4, 16, 16);
|
|
|
|
if (btn_rect.contains(mouse_x, mouse_y) and ctx.input.mouse_pressed) {
|
|
result.dismissed = true;
|
|
}
|
|
}
|
|
|
|
// Draw action button if present
|
|
if (toast.getAction()) |action| {
|
|
const action_x = x + @as(i32, @intCast(width)) - padding - @as(i32, @intCast(action.len * 8)) - 20;
|
|
const action_y = y + @as(i32, @intCast(height)) - padding - 12;
|
|
|
|
ctx.pushCommand(.{
|
|
.text = .{
|
|
.x = action_x,
|
|
.y = action_y,
|
|
.text = action,
|
|
.color = colors.icon_bg,
|
|
},
|
|
});
|
|
|
|
// Check for click on action
|
|
const mouse_x = ctx.input.mouse_x;
|
|
const mouse_y = ctx.input.mouse_y;
|
|
const action_rect = Rect.init(action_x - 4, action_y - 4, @intCast(action.len * 8 + 8), 20);
|
|
|
|
if (action_rect.contains(mouse_x, mouse_y) and ctx.input.mouse_pressed) {
|
|
result.action_clicked = true;
|
|
}
|
|
}
|
|
|
|
// Draw progress bar for remaining time
|
|
if (toast.duration_ms > 0) {
|
|
const remaining = toast.getRemainingMs();
|
|
const progress: f32 = @as(f32, @floatFromInt(remaining)) / @as(f32, @floatFromInt(toast.duration_ms));
|
|
|
|
const bar_width: u32 = @intFromFloat(@as(f32, @floatFromInt(width - 8)) * progress);
|
|
ctx.pushCommand(.{
|
|
.rect = .{
|
|
.x = x + 4,
|
|
.y = y + @as(i32, @intCast(height)) - 3,
|
|
.w = bar_width,
|
|
.h = 2,
|
|
.color = colors.icon_bg,
|
|
},
|
|
});
|
|
}
|
|
|
|
ctx.countWidget();
|
|
|
|
return result;
|
|
}
|
|
|
|
fn drawBorder(ctx: *Context, rect: Rect, color: Color) void {
|
|
// Top
|
|
ctx.pushCommand(.{
|
|
.rect = .{ .x = rect.x, .y = rect.y, .w = rect.w, .h = 1, .color = color },
|
|
});
|
|
// Bottom
|
|
ctx.pushCommand(.{
|
|
.rect = .{ .x = rect.x, .y = rect.y + @as(i32, @intCast(rect.h)) - 1, .w = rect.w, .h = 1, .color = color },
|
|
});
|
|
// Left
|
|
ctx.pushCommand(.{
|
|
.rect = .{ .x = rect.x, .y = rect.y, .w = 1, .h = rect.h, .color = color },
|
|
});
|
|
// Right
|
|
ctx.pushCommand(.{
|
|
.rect = .{ .x = rect.x + @as(i32, @intCast(rect.w)) - 1, .y = rect.y, .w = 1, .h = rect.h, .color = color },
|
|
});
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "Toast init" {
|
|
const toast = Toast.init(1, .info, "Test message", 3000);
|
|
|
|
try std.testing.expectEqual(@as(u32, 1), toast.id);
|
|
try std.testing.expectEqual(ToastType.info, toast.toast_type);
|
|
try std.testing.expectEqualStrings("Test message", toast.getMessage());
|
|
try std.testing.expectEqual(@as(u32, 3000), toast.duration_ms);
|
|
}
|
|
|
|
test "ToastManager basic" {
|
|
var manager = Manager.init();
|
|
|
|
try std.testing.expectEqual(@as(usize, 0), manager.getCount());
|
|
|
|
const id1 = manager.info("Info message");
|
|
try std.testing.expectEqual(@as(usize, 1), manager.getCount());
|
|
try std.testing.expect(manager.exists(id1));
|
|
|
|
const id2 = manager.success("Success!");
|
|
try std.testing.expectEqual(@as(usize, 2), manager.getCount());
|
|
|
|
_ = id2;
|
|
}
|
|
|
|
test "ToastManager dismiss" {
|
|
var manager = Manager.init();
|
|
|
|
const id = manager.info("Test");
|
|
try std.testing.expect(manager.exists(id));
|
|
|
|
manager.dismiss(id);
|
|
manager.update();
|
|
|
|
try std.testing.expect(!manager.exists(id));
|
|
}
|
|
|
|
test "ToastManager dismissAll" {
|
|
var manager = Manager.init();
|
|
|
|
_ = manager.info("One");
|
|
_ = manager.info("Two");
|
|
_ = manager.info("Three");
|
|
|
|
try std.testing.expectEqual(@as(usize, 3), manager.getCount());
|
|
|
|
manager.dismissAll();
|
|
|
|
try std.testing.expectEqual(@as(usize, 0), manager.getCount());
|
|
}
|
|
|
|
test "ToastType colors" {
|
|
const info_color = ToastType.info.getColor();
|
|
const success_color = ToastType.success.getColor();
|
|
|
|
try std.testing.expect(info_color.r != success_color.r or
|
|
info_color.g != success_color.g or
|
|
info_color.b != success_color.b);
|
|
}
|
|
|
|
test "Toast action" {
|
|
var toast = Toast.init(1, .info, "Test", 3000);
|
|
toast.setAction("Undo");
|
|
|
|
const action = toast.getAction();
|
|
try std.testing.expect(action != null);
|
|
try std.testing.expectEqualStrings("Undo", action.?);
|
|
}
|