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 #[must_use]
72 pub fn text(&self) -> &str {
73 &self.text
74 }
75
76 #[must_use]
78 pub fn fallback(&self) -> Option<&str> {
79 self.fallback.as_deref()
80 }
81
82 #[must_use]
84 pub fn width(&self) -> usize {
85 display_width(&self.text)
86 }
87
88 #[must_use]
90 pub fn effective_width(&self) -> usize {
91 match &self.fallback {
92 Some(fb) => display_width(fb),
93 None => self.width(),
94 }
95 }
96
97 #[must_use]
99 pub fn should_use_fallback(&self, use_emoji: bool) -> bool {
100 !use_emoji && self.fallback.is_some()
101 }
102}
103
104impl Widget for Emoji {
105 fn render(&self, area: Rect, frame: &mut Frame) {
106 if area.width == 0 || area.height == 0 || self.text.is_empty() {
107 return;
108 }
109
110 let deg = frame.buffer.degradation;
111 let max_x = area.right();
112
113 if deg.apply_styling() {
115 draw_text_span(frame, area.x, area.y, &self.text, self.style, max_x);
116 } else if let Some(fb) = &self.fallback {
117 draw_text_span(frame, area.x, area.y, fb, self.fallback_style, max_x);
118 } else {
119 draw_text_span(frame, area.x, area.y, &self.text, Style::default(), max_x);
120 }
121 }
122
123 fn is_essential(&self) -> bool {
124 false
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use ftui_render::frame::Frame;
132 use ftui_render::grapheme_pool::GraphemePool;
133
134 #[test]
135 fn new_emoji() {
136 let e = Emoji::new("🎉");
137 assert_eq!(e.text(), "🎉");
138 assert!(e.fallback().is_none());
139 }
140
141 #[test]
142 fn with_fallback() {
143 let e = Emoji::new("🦀").with_fallback("[crab]");
144 assert_eq!(e.fallback(), Some("[crab]"));
145 }
146
147 #[test]
148 fn width_measurement() {
149 let e = Emoji::new("🎉");
150 assert!(e.width() > 0);
152 }
153
154 #[test]
155 fn effective_width_with_fallback() {
156 let e = Emoji::new("🦀").with_fallback("[crab]");
157 assert_eq!(e.effective_width(), 6); }
159
160 #[test]
161 fn effective_width_without_fallback() {
162 let e = Emoji::new("🎉");
163 assert_eq!(e.effective_width(), e.width());
164 }
165
166 #[test]
167 fn render_basic() {
168 let e = Emoji::new("A");
169 let mut pool = GraphemePool::new();
170 let mut frame = Frame::new(10, 1, &mut pool);
171 let area = Rect::new(0, 0, 10, 1);
172 e.render(area, &mut frame);
173
174 let cell = frame.buffer.get(0, 0).unwrap();
175 assert_eq!(cell.content.as_char(), Some('A'));
176 }
177
178 #[test]
179 fn render_zero_area() {
180 let e = Emoji::new("🎉");
181 let mut pool = GraphemePool::new();
182 let mut frame = Frame::new(10, 1, &mut pool);
183 e.render(Rect::new(0, 0, 0, 0), &mut frame); }
185
186 #[test]
187 fn render_empty_text() {
188 let e = Emoji::new("");
189 let mut pool = GraphemePool::new();
190 let mut frame = Frame::new(10, 1, &mut pool);
191 e.render(Rect::new(0, 0, 10, 1), &mut frame); }
193
194 #[test]
195 fn is_not_essential() {
196 let e = Emoji::new("🎉");
197 assert!(!e.is_essential());
198 }
199
200 #[test]
201 fn multi_char_emoji() {
202 let e = Emoji::new("👩💻");
203 assert!(e.width() > 0);
204 }
205
206 #[test]
207 fn text_as_emoji() {
208 let e = Emoji::new("OK");
210 assert_eq!(e.width(), 2);
211 }
212
213 #[test]
214 fn should_use_fallback_logic() {
215 let e = Emoji::new("🎉").with_fallback("(party)");
216 assert!(e.should_use_fallback(false));
217 assert!(!e.should_use_fallback(true));
218 }
219
220 #[test]
221 fn should_not_use_fallback_without_setting() {
222 let e = Emoji::new("🎉");
223 assert!(!e.should_use_fallback(false));
224 }
225}