1use crate::{Widget, draw_text_span};
19use ftui_core::geometry::Rect;
20use ftui_render::frame::Frame;
21use ftui_style::Style;
22use ftui_text::wrap::display_width;
23
24#[derive(Debug, Clone)]
26pub struct Emoji {
27 text: String,
29 fallback: Option<String>,
31 style: Style,
33 fallback_style: Style,
35}
36
37impl Emoji {
38 #[must_use]
40 pub fn new(text: impl Into<String>) -> Self {
41 Self {
42 text: text.into(),
43 fallback: None,
44 style: Style::default(),
45 fallback_style: Style::default(),
46 }
47 }
48
49 #[must_use]
51 pub fn with_fallback(mut self, fallback: impl Into<String>) -> Self {
52 self.fallback = Some(fallback.into());
53 self
54 }
55
56 #[must_use]
58 pub fn with_style(mut self, style: Style) -> Self {
59 self.style = style;
60 self
61 }
62
63 #[must_use]
65 pub fn with_fallback_style(mut self, style: Style) -> Self {
66 self.fallback_style = style;
67 self
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn text(&self) -> &str {
74 &self.text
75 }
76
77 #[inline]
79 #[must_use = "use the fallback text (if any)"]
80 pub fn fallback(&self) -> Option<&str> {
81 self.fallback.as_deref()
82 }
83
84 #[inline]
86 #[must_use]
87 pub fn width(&self) -> usize {
88 display_width(&self.text)
89 }
90
91 #[must_use]
93 pub fn effective_width(&self) -> usize {
94 match &self.fallback {
95 Some(fb) => display_width(fb),
96 None => self.width(),
97 }
98 }
99
100 #[must_use]
102 pub fn should_use_fallback(&self, use_emoji: bool) -> bool {
103 !use_emoji && self.fallback.is_some()
104 }
105}
106
107impl Widget for Emoji {
108 fn render(&self, area: Rect, frame: &mut Frame) {
109 if area.width == 0 || area.height == 0 || self.text.is_empty() {
110 return;
111 }
112
113 let deg = frame.buffer.degradation;
114 let max_x = area.right();
115
116 if deg.apply_styling() {
118 draw_text_span(frame, area.x, area.y, &self.text, self.style, max_x);
119 } else if let Some(fb) = &self.fallback {
120 draw_text_span(frame, area.x, area.y, fb, self.fallback_style, max_x);
121 } else {
122 draw_text_span(frame, area.x, area.y, &self.text, Style::default(), max_x);
123 }
124 }
125
126 fn is_essential(&self) -> bool {
127 false
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use ftui_render::frame::Frame;
135 use ftui_render::grapheme_pool::GraphemePool;
136
137 #[test]
138 fn new_emoji() {
139 let e = Emoji::new("🎉");
140 assert_eq!(e.text(), "🎉");
141 assert!(e.fallback().is_none());
142 }
143
144 #[test]
145 fn with_fallback() {
146 let e = Emoji::new("🦀").with_fallback("[crab]");
147 assert_eq!(e.fallback(), Some("[crab]"));
148 }
149
150 #[test]
151 fn width_measurement() {
152 let e = Emoji::new("🎉");
153 assert!(e.width() > 0);
155 }
156
157 #[test]
158 fn effective_width_with_fallback() {
159 let e = Emoji::new("🦀").with_fallback("[crab]");
160 assert_eq!(e.effective_width(), 6); }
162
163 #[test]
164 fn effective_width_without_fallback() {
165 let e = Emoji::new("🎉");
166 assert_eq!(e.effective_width(), e.width());
167 }
168
169 #[test]
170 fn render_basic() {
171 let e = Emoji::new("A");
172 let mut pool = GraphemePool::new();
173 let mut frame = Frame::new(10, 1, &mut pool);
174 let area = Rect::new(0, 0, 10, 1);
175 e.render(area, &mut frame);
176
177 let cell = frame.buffer.get(0, 0).unwrap();
178 assert_eq!(cell.content.as_char(), Some('A'));
179 }
180
181 #[test]
182 fn render_zero_area() {
183 let e = Emoji::new("🎉");
184 let mut pool = GraphemePool::new();
185 let mut frame = Frame::new(10, 1, &mut pool);
186 e.render(Rect::new(0, 0, 0, 0), &mut frame); }
188
189 #[test]
190 fn render_empty_text() {
191 let e = Emoji::new("");
192 let mut pool = GraphemePool::new();
193 let mut frame = Frame::new(10, 1, &mut pool);
194 e.render(Rect::new(0, 0, 10, 1), &mut frame); }
196
197 #[test]
198 fn is_not_essential() {
199 let e = Emoji::new("🎉");
200 assert!(!e.is_essential());
201 }
202
203 #[test]
204 fn multi_char_emoji() {
205 let e = Emoji::new("👩💻");
206 assert!(e.width() > 0);
207 }
208
209 #[test]
210 fn text_as_emoji() {
211 let e = Emoji::new("OK");
213 assert_eq!(e.width(), 2);
214 }
215
216 #[test]
217 fn should_use_fallback_logic() {
218 let e = Emoji::new("🎉").with_fallback("(party)");
219 assert!(e.should_use_fallback(false));
220 assert!(!e.should_use_fallback(true));
221 }
222
223 #[test]
224 fn should_not_use_fallback_without_setting() {
225 let e = Emoji::new("🎉");
226 assert!(!e.should_use_fallback(false));
227 }
228}