1use crate::render::Cell;
6use crate::style::Color;
7use crate::widget::traits::{RenderContext, View, WidgetProps};
8use crate::{impl_props_builders, impl_styled_view};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum ToastLevel {
13 #[default]
15 Info,
16 Success,
18 Warning,
20 Error,
22}
23
24impl ToastLevel {
25 pub fn icon(&self) -> char {
27 match self {
28 ToastLevel::Info => 'ℹ',
29 ToastLevel::Success => '✓',
30 ToastLevel::Warning => '⚠',
31 ToastLevel::Error => '✗',
32 }
33 }
34
35 pub fn color(&self) -> Color {
37 match self {
38 ToastLevel::Info => Color::CYAN,
39 ToastLevel::Success => Color::GREEN,
40 ToastLevel::Warning => Color::YELLOW,
41 ToastLevel::Error => Color::RED,
42 }
43 }
44
45 pub fn bg_color(&self) -> Color {
47 match self {
48 ToastLevel::Info => Color::rgb(0, 40, 60),
49 ToastLevel::Success => Color::rgb(0, 40, 0),
50 ToastLevel::Warning => Color::rgb(60, 40, 0),
51 ToastLevel::Error => Color::rgb(60, 0, 0),
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum ToastPosition {
59 TopLeft,
61 TopCenter,
63 #[default]
65 TopRight,
66 BottomLeft,
68 BottomCenter,
70 BottomRight,
72}
73
74pub struct Toast {
86 message: String,
87 level: ToastLevel,
88 position: ToastPosition,
89 width: Option<u16>,
90 show_icon: bool,
91 show_border: bool,
92 props: WidgetProps,
93}
94
95impl Toast {
96 pub fn new(message: impl Into<String>) -> Self {
98 Self {
99 message: message.into(),
100 level: ToastLevel::default(),
101 position: ToastPosition::default(),
102 width: None,
103 show_icon: true,
104 show_border: true,
105 props: WidgetProps::new(),
106 }
107 }
108
109 pub fn info(message: impl Into<String>) -> Self {
111 Self::new(message).level(ToastLevel::Info)
112 }
113
114 pub fn success(message: impl Into<String>) -> Self {
116 Self::new(message).level(ToastLevel::Success)
117 }
118
119 pub fn warning(message: impl Into<String>) -> Self {
121 Self::new(message).level(ToastLevel::Warning)
122 }
123
124 pub fn error(message: impl Into<String>) -> Self {
126 Self::new(message).level(ToastLevel::Error)
127 }
128
129 pub fn level(mut self, level: ToastLevel) -> Self {
131 self.level = level;
132 self
133 }
134
135 pub fn position(mut self, position: ToastPosition) -> Self {
137 self.position = position;
138 self
139 }
140
141 pub fn width(mut self, width: u16) -> Self {
143 self.width = Some(width);
144 self
145 }
146
147 pub fn show_icon(mut self, show: bool) -> Self {
149 self.show_icon = show;
150 self
151 }
152
153 pub fn show_border(mut self, show: bool) -> Self {
155 self.show_border = show;
156 self
157 }
158
159 fn calculate_size(&self, max_width: u16) -> (u16, u16) {
161 let icon_width = if self.show_icon { 2 } else { 0 };
162 let border_width = if self.show_border { 2 } else { 0 };
163 let padding = 2; let content_width = crate::utils::unicode::display_width(&self.message) as u16 + icon_width;
166 let total_width = self.width.unwrap_or(content_width + border_width + padding);
167 let width = total_width.min(max_width);
168
169 let inner_width = width.saturating_sub(border_width + padding + icon_width);
171 let msg_cols = crate::utils::unicode::display_width(&self.message) as u16;
172 let lines = if inner_width > 0 {
173 msg_cols.saturating_add(inner_width - 1) / inner_width
174 } else {
175 1
176 };
177 let height = lines + if self.show_border { 2 } else { 0 };
178
179 (width, height.max(if self.show_border { 3 } else { 1 }))
180 }
181
182 fn calculate_position(
184 &self,
185 area_width: u16,
186 area_height: u16,
187 toast_width: u16,
188 toast_height: u16,
189 ) -> (u16, u16) {
190 let margin = 1u16;
191
192 let x = match self.position {
193 ToastPosition::TopLeft | ToastPosition::BottomLeft => margin,
194 ToastPosition::TopCenter | ToastPosition::BottomCenter => {
195 area_width.saturating_sub(toast_width) / 2
196 }
197 ToastPosition::TopRight | ToastPosition::BottomRight => {
198 area_width.saturating_sub(toast_width + margin)
199 }
200 };
201
202 let y = match self.position {
203 ToastPosition::TopLeft | ToastPosition::TopCenter | ToastPosition::TopRight => margin,
204 ToastPosition::BottomLeft
205 | ToastPosition::BottomCenter
206 | ToastPosition::BottomRight => area_height.saturating_sub(toast_height + margin),
207 };
208
209 (x, y)
210 }
211}
212
213impl View for Toast {
214 crate::impl_view_meta!("Toast");
215
216 fn render(&self, ctx: &mut RenderContext) {
217 let area = ctx.area;
218 if area.width < 5 || area.height < 3 {
219 return;
220 }
221
222 let (toast_width, toast_height) = self.calculate_size(area.width);
223 let (x, y) = self.calculate_position(area.width, area.height, toast_width, toast_height);
224
225 let color = self.level.color();
226 let bg = self.level.bg_color();
227
228 if self.show_border {
230 let mut top_left = Cell::new('╭');
232 top_left.fg = Some(color);
233 top_left.bg = Some(bg);
234 ctx.buffer.set(area.x + x, area.y + y, top_left);
235
236 for i in 1..toast_width.saturating_sub(1) {
237 let mut cell = Cell::new('─');
238 cell.fg = Some(color);
239 cell.bg = Some(bg);
240 ctx.buffer.set(area.x + x + i, area.y + y, cell);
241 }
242
243 let mut top_right = Cell::new('╮');
244 top_right.fg = Some(color);
245 top_right.bg = Some(bg);
246 ctx.buffer
247 .set(area.x + x + toast_width - 1, area.y + y, top_right);
248
249 let mut bottom_left = Cell::new('╰');
251 bottom_left.fg = Some(color);
252 bottom_left.bg = Some(bg);
253 ctx.buffer
254 .set(area.x + x, area.y + y + toast_height - 1, bottom_left);
255
256 for i in 1..toast_width.saturating_sub(1) {
257 let mut cell = Cell::new('─');
258 cell.fg = Some(color);
259 cell.bg = Some(bg);
260 ctx.buffer
261 .set(area.x + x + i, area.y + y + toast_height - 1, cell);
262 }
263
264 let mut bottom_right = Cell::new('╯');
265 bottom_right.fg = Some(color);
266 bottom_right.bg = Some(bg);
267 ctx.buffer.set(
268 area.x + x + toast_width - 1,
269 area.y + y + toast_height - 1,
270 bottom_right,
271 );
272
273 for row in 1..toast_height.saturating_sub(1) {
275 let mut left = Cell::new('│');
276 left.fg = Some(color);
277 left.bg = Some(bg);
278 ctx.buffer.set(area.x + x, area.y + y + row, left);
279
280 let mut right = Cell::new('│');
281 right.fg = Some(color);
282 right.bg = Some(bg);
283 ctx.buffer
284 .set(area.x + x + toast_width - 1, area.y + y + row, right);
285
286 for col in 1..toast_width.saturating_sub(1) {
288 let mut fill = Cell::new(' ');
289 fill.bg = Some(bg);
290 ctx.buffer.set(area.x + x + col, area.y + y + row, fill);
291 }
292 }
293 }
294
295 let content_x = x + if self.show_border { 2 } else { 0 };
297 let content_y = y + if self.show_border { 1 } else { 0 };
298
299 if self.show_icon {
301 let mut icon_cell = Cell::new(self.level.icon());
302 icon_cell.fg = Some(color);
303 icon_cell.bg = Some(bg);
304 ctx.buffer
305 .set(area.x + content_x, area.y + content_y, icon_cell);
306 }
307
308 let msg_x = content_x + if self.show_icon { 2 } else { 0 };
310 let max_text_width = toast_width
311 .saturating_sub(if self.show_border { 1 } else { 0 })
312 .saturating_sub(msg_x - x);
313 ctx.draw_text_clipped(
314 area.x + msg_x,
315 area.y + content_y,
316 &self.message,
317 Color::WHITE,
318 max_text_width,
319 );
320 }
321}
322
323impl_styled_view!(Toast);
324impl_props_builders!(Toast);
325
326pub fn toast(message: impl Into<String>) -> Toast {
328 Toast::new(message)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::layout::Rect;
335 use crate::render::Buffer;
336
337 #[test]
338 fn test_toast_new() {
339 let t = Toast::new("Test message");
340 assert_eq!(t.message, "Test message");
341 assert_eq!(t.level, ToastLevel::Info);
342 }
343
344 #[test]
345 fn test_toast_levels() {
346 let info = Toast::info("Info");
347 assert_eq!(info.level, ToastLevel::Info);
348
349 let success = Toast::success("Success");
350 assert_eq!(success.level, ToastLevel::Success);
351
352 let warning = Toast::warning("Warning");
353 assert_eq!(warning.level, ToastLevel::Warning);
354
355 let error = Toast::error("Error");
356 assert_eq!(error.level, ToastLevel::Error);
357 }
358
359 #[test]
360 fn test_toast_position() {
361 let t = Toast::new("Test").position(ToastPosition::BottomLeft);
362 assert_eq!(t.position, ToastPosition::BottomLeft);
363 }
364
365 #[test]
366 fn test_toast_level_icon() {
367 assert_eq!(ToastLevel::Info.icon(), 'ℹ');
368 assert_eq!(ToastLevel::Success.icon(), '✓');
369 assert_eq!(ToastLevel::Warning.icon(), '⚠');
370 assert_eq!(ToastLevel::Error.icon(), '✗');
371 }
372
373 #[test]
374 fn test_toast_level_color() {
375 assert_eq!(ToastLevel::Info.color(), Color::CYAN);
376 assert_eq!(ToastLevel::Success.color(), Color::GREEN);
377 assert_eq!(ToastLevel::Warning.color(), Color::YELLOW);
378 assert_eq!(ToastLevel::Error.color(), Color::RED);
379 }
380
381 #[test]
382 fn test_toast_render() {
383 let t = Toast::new("Hello World")
384 .level(ToastLevel::Success)
385 .position(ToastPosition::TopRight);
386
387 let mut buffer = Buffer::new(40, 10);
388 let area = Rect::new(0, 0, 40, 10);
389 let mut ctx = RenderContext::new(&mut buffer, area);
390
391 t.render(&mut ctx);
392 }
393
394 #[test]
395 fn test_toast_no_border() {
396 let t = Toast::new("No border").show_border(false);
397
398 let mut buffer = Buffer::new(30, 5);
399 let area = Rect::new(0, 0, 30, 5);
400 let mut ctx = RenderContext::new(&mut buffer, area);
401
402 t.render(&mut ctx);
403 }
404
405 #[test]
406 fn test_toast_no_icon() {
407 let t = Toast::new("No icon").show_icon(false);
408
409 let mut buffer = Buffer::new(30, 5);
410 let area = Rect::new(0, 0, 30, 5);
411 let mut ctx = RenderContext::new(&mut buffer, area);
412
413 t.render(&mut ctx);
414 }
415
416 #[test]
417 fn test_toast_helper() {
418 let t = toast("Quick toast");
419 assert_eq!(t.message, "Quick toast");
420 }
421
422 #[test]
423 fn test_toast_all_positions() {
424 let positions = [
425 ToastPosition::TopLeft,
426 ToastPosition::TopCenter,
427 ToastPosition::TopRight,
428 ToastPosition::BottomLeft,
429 ToastPosition::BottomCenter,
430 ToastPosition::BottomRight,
431 ];
432
433 for pos in positions {
434 let t = Toast::new("Test").position(pos);
435 let mut buffer = Buffer::new(40, 20);
436 let area = Rect::new(0, 0, 40, 20);
437 let mut ctx = RenderContext::new(&mut buffer, area);
438 t.render(&mut ctx);
439 }
440 }
441}