use std::sync::Arc;
use unicode_width::UnicodeWidthStr;
use crate::core::element::Element;
use crate::style::{Align, BorderStyle, Length, Padding, Rect, RichText, Span, Style};
use crate::widgets::frame::EdgeDecoration;
use crate::widgets::text::Overflow;
use crate::widgets::{Frame, Text};
pub(crate) const COPY_GLYPH: &str = "⧉";
pub(crate) fn copy_zone_with_right_padding(rect: Rect, right_padding: u16) -> Rect {
let glyph_width = UnicodeWidthStr::width(COPY_GLYPH).max(1) as u16;
if rect.w <= glyph_width + 1 + right_padding || rect.h == 0 {
return Rect {
x: rect.x,
y: rect.y,
w: 0,
h: 0,
};
}
Rect {
x: rect.x + rect.w.saturating_sub(glyph_width + 1 + right_padding) as i16,
y: rect.y,
w: glyph_width,
h: 1,
}
}
#[derive(Clone)]
pub struct Toast {
pub message: Arc<str>,
pub duration: f64,
pub dismiss_on_click: bool,
pub copyable: bool,
pub copy_affordance: ToastCopyAffordance,
pub(crate) title: Option<Arc<str>>,
pub(crate) title_prefix: Option<RichText>,
pub(crate) title_suffix: Option<RichText>,
pub(crate) title_alignment: Align,
pub(crate) title_style: Style,
pub(crate) message_style: Style,
pub(crate) frame_style: Style,
pub(crate) border: bool,
pub(crate) border_style: BorderStyle,
pub(crate) header_padding: Padding,
pub(crate) padding: Padding,
pub(crate) decorations: Vec<EdgeDecoration>,
pub(crate) width: Length,
pub(crate) height: Length,
pub(crate) max_width: Option<Length>,
pub(crate) wrap: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ToastCopyAffordance {
None,
BorderGlyph,
}
impl Toast {
pub fn new(message: impl Into<Arc<str>>) -> Self {
Self {
message: message.into(),
duration: 3.0,
dismiss_on_click: true,
copyable: false,
copy_affordance: ToastCopyAffordance::BorderGlyph,
title: None,
title_prefix: None,
title_suffix: None,
title_alignment: Align::Start,
title_style: Style::default(),
message_style: Style::default(),
frame_style: Style::default(),
border: true,
border_style: BorderStyle::Rounded,
header_padding: Padding::default(),
padding: Padding::default(),
decorations: Vec::new(),
width: Length::Auto,
height: Length::Auto,
max_width: None,
wrap: true,
}
}
pub fn duration(mut self, secs: f64) -> Self {
self.duration = secs;
self
}
pub fn title(mut self, title: Option<impl Into<Arc<str>>>) -> Self {
self.title = title.map(Into::into);
self
}
pub fn title_prefix(mut self, prefix: impl Into<RichText>) -> Self {
self.title_prefix = Some(prefix.into());
self
}
pub fn title_suffix(mut self, suffix: impl Into<RichText>) -> Self {
self.title_suffix = Some(suffix.into());
self
}
pub fn title_alignment(mut self, align: Align) -> Self {
self.title_alignment = align;
self
}
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
pub fn message_style(mut self, style: Style) -> Self {
self.message_style = style;
self
}
pub fn frame_style(mut self, style: Style) -> Self {
self.frame_style = style;
self
}
pub fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn border_style(mut self, border_style: BorderStyle) -> Self {
self.border_style = border_style;
self
}
pub fn header_padding(mut self, padding: impl Into<Padding>) -> Self {
self.header_padding = padding.into();
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
pub fn decoration(mut self, decoration: EdgeDecoration) -> Self {
self.decorations.push(decoration);
self
}
pub fn decorations(mut self, decorations: Vec<EdgeDecoration>) -> Self {
self.decorations = decorations;
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn max_width(mut self, width: Length) -> Self {
self.max_width = Some(width);
self
}
pub fn dismiss_on_click(mut self, dismiss: bool) -> Self {
self.dismiss_on_click = dismiss;
self
}
pub fn copyable(mut self, copyable: bool) -> Self {
self.copyable = copyable;
self
}
pub fn copy_affordance(mut self, affordance: ToastCopyAffordance) -> Self {
self.copy_affordance = affordance;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn into_element(self) -> Element {
let max_width = self.max_width;
let mut message_style = self.message_style;
if message_style.bg.is_none() {
message_style.bg = self.frame_style.bg;
}
let overflow = if self.wrap {
Overflow::Wrap
} else {
Overflow::Auto
};
let render_copy_affordance = self.copyable
&& self.border
&& matches!(self.copy_affordance, ToastCopyAffordance::BorderGlyph);
let title_alignment = if render_copy_affordance {
Align::End
} else {
self.title_alignment
};
let mut frame = Frame::new()
.title_style(self.title_style)
.title_alignment(title_alignment)
.border(self.border)
.border_style(self.border_style)
.header_padding(self.header_padding)
.padding(self.padding)
.style(self.frame_style)
.child(
Text::new(self.message.clone())
.style(message_style)
.overflow(overflow),
)
.width(self.width)
.height(self.height);
if let Some(title) = self.title {
frame = frame.title(title);
}
if let Some(prefix) = self.title_prefix.clone() {
frame = frame.title_prefix(prefix);
}
let title_suffix = if render_copy_affordance {
let mut suffix = self.title_suffix.clone().unwrap_or_default();
if !suffix.is_empty() {
suffix.spans.push(Span::new(" "));
}
suffix.spans.push(Span::new(COPY_GLYPH));
Some(suffix)
} else {
self.title_suffix.clone()
};
if let Some(suffix) = title_suffix {
frame = frame.title_suffix(suffix);
}
if !self.decorations.is_empty() {
frame = frame.decorations(self.decorations.clone());
}
let mut element: Element = frame.into();
if let Some(max_width) = max_width {
element = element.max_width(max_width);
}
element
}
}
impl From<Toast> for Element {
fn from(toast: Toast) -> Self {
toast.into_element()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::element::ElementKind;
#[test]
fn copyable_defaults_to_false() {
let toast = Toast::new("message");
assert!(!toast.copyable);
assert_eq!(toast.copy_affordance, ToastCopyAffordance::BorderGlyph);
}
#[test]
fn copy_zone_tracks_top_right_title_suffix_cell() {
let zone = copy_zone_with_right_padding(
Rect {
x: 10,
y: 5,
w: 20,
h: 3,
},
0,
);
assert_eq!(zone.x, 28);
assert_eq!(zone.y, 5);
assert_eq!(zone.w, 1);
assert_eq!(zone.h, 1);
assert!(zone.contains(28, 5));
assert!(!zone.contains(29, 5));
assert!(!zone.contains(28, 6));
}
#[test]
fn copy_zone_accounts_for_header_right_padding() {
let zone = copy_zone_with_right_padding(
Rect {
x: 10,
y: 5,
w: 20,
h: 3,
},
2,
);
assert_eq!(zone.x, 26);
assert_eq!(zone.y, 5);
assert_eq!(zone.w, 1);
assert_eq!(zone.h, 1);
}
#[test]
fn copyable_borderless_toast_does_not_render_hidden_copy_suffix() {
let element = Toast::new("message")
.copyable(true)
.border(false)
.into_element();
let ElementKind::Frame(frame) = element.kind else {
panic!("toast should lower to a frame");
};
assert!(frame.props.title_suffix.is_none());
}
#[test]
fn copy_affordance_none_does_not_render_copy_suffix() {
let element = Toast::new("message")
.copyable(true)
.copy_affordance(ToastCopyAffordance::None)
.into_element();
let ElementKind::Frame(frame) = element.kind else {
panic!("toast should lower to a frame");
};
assert!(frame.props.title_suffix.is_none());
}
}