1use dampen_core::parser::error::ParseError;
7use iced::{
8 Alignment, Color, Element, Length,
9 widget::{button, column, container, text},
10};
11use std::time::Instant;
12
13#[derive(Debug, Clone)]
15pub struct ErrorOverlay {
16 pub error: Option<ParseError>,
18
19 pub visible: bool,
21
22 pub timestamp: Instant,
24}
25
26impl ErrorOverlay {
27 pub fn new() -> Self {
29 Self {
30 error: None,
31 visible: false,
32 timestamp: Instant::now(),
33 }
34 }
35
36 pub fn show(&mut self, error: ParseError) {
41 self.error = Some(error);
42 self.visible = true;
43 self.timestamp = Instant::now();
44 }
45
46 pub fn hide(&mut self) {
48 self.visible = false;
49 }
50
51 pub fn is_visible(&self) -> bool {
53 self.visible
54 }
55
56 pub fn render<'a, Message: Clone + 'a>(&'a self, on_dismiss: Message) -> Element<'a, Message> {
73 if !self.visible {
74 return container(text("")).into();
75 }
76
77 let error = match &self.error {
78 Some(e) => e,
79 None => return container(text("")).into(),
80 };
81
82 let title = text("Hot-Reload Error")
84 .size(24)
85 .style(|_theme| text::Style {
86 color: Some(Color::WHITE),
87 });
88
89 let message = text(&error.message).size(16).style(|_theme| text::Style {
91 color: Some(Color::WHITE),
92 });
93
94 let location = text(format!(
96 "at line {}, column {}",
97 error.span.line, error.span.column
98 ))
99 .size(14)
100 .style(|_theme| text::Style {
101 color: Some(Color::from_rgb(0.9, 0.9, 0.9)),
102 });
103
104 let suggestion_widget = if let Some(ref suggestion) = error.suggestion {
106 let label = text(format!("💡 {}", suggestion))
107 .size(14)
108 .style(|_theme| text::Style {
109 color: Some(Color::from_rgb(1.0, 1.0, 0.6)),
110 });
111 Some(label)
112 } else {
113 None
114 };
115
116 let dismiss_btn = button(text("Dismiss (Esc)").size(14).style(|_theme| text::Style {
118 color: Some(Color::BLACK),
119 }))
120 .on_press(on_dismiss)
121 .padding(10);
122
123 let mut content = column![title, message, location]
125 .spacing(12)
126 .align_x(Alignment::Start);
127
128 if let Some(suggestion) = suggestion_widget {
129 content = content.push(suggestion);
130 }
131
132 content = content.push(dismiss_btn);
133
134 container(content)
136 .width(Length::Fill)
137 .height(Length::Fill)
138 .padding(40)
139 .style(|_theme| container::Style {
140 background: Some(Color::from_rgb(0.8, 0.2, 0.2).into()),
141 text_color: Some(Color::WHITE),
142 ..Default::default()
143 })
144 .into()
145 }
146}
147
148impl Default for ErrorOverlay {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use dampen_core::ir::span::Span;
158 use dampen_core::parser::error::ParseErrorKind;
159
160 #[derive(Debug, Clone)]
161 enum TestMessage {
162 Dismiss,
163 }
164
165 #[test]
166 fn test_new_overlay_is_hidden() {
167 let overlay = ErrorOverlay::new();
168 assert!(!overlay.is_visible());
169 assert!(overlay.error.is_none());
170 }
171
172 #[test]
173 fn test_show_makes_overlay_visible() {
174 let mut overlay = ErrorOverlay::new();
175 let error = ParseError {
176 kind: ParseErrorKind::XmlSyntax,
177 message: "Test error".to_string(),
178 span: Span {
179 start: 0,
180 end: 5,
181 line: 1,
182 column: 5,
183 },
184 suggestion: None,
185 };
186
187 overlay.show(error.clone());
188 assert!(overlay.is_visible());
189 assert_eq!(overlay.error, Some(error));
190 }
191
192 #[test]
193 fn test_hide_makes_overlay_invisible() {
194 let mut overlay = ErrorOverlay::new();
195 let error = ParseError {
196 kind: ParseErrorKind::XmlSyntax,
197 message: "Test error".to_string(),
198 span: Span {
199 start: 0,
200 end: 5,
201 line: 1,
202 column: 5,
203 },
204 suggestion: None,
205 };
206
207 overlay.show(error);
208 overlay.hide();
209 assert!(!overlay.is_visible());
210 assert!(overlay.error.is_some());
212 }
213
214 #[test]
215 fn test_render_returns_empty_when_hidden() {
216 let overlay = ErrorOverlay::new();
217 let element = overlay.render(TestMessage::Dismiss);
218 drop(element);
221 }
222
223 #[test]
224 fn test_render_with_visible_error() {
225 let mut overlay = ErrorOverlay::new();
226 let error = ParseError {
227 kind: ParseErrorKind::UnknownWidget,
228 message: "Unknown widget 'foo'".to_string(),
229 span: Span {
230 start: 50,
231 end: 53,
232 line: 10,
233 column: 15,
234 },
235 suggestion: Some("Did you mean 'button'?".to_string()),
236 };
237
238 overlay.show(error);
239 let element = overlay.render(TestMessage::Dismiss);
240 drop(element);
242 }
243
244 #[test]
245 fn test_render_without_suggestion() {
246 let mut overlay = ErrorOverlay::new();
247 let error = ParseError {
248 kind: ParseErrorKind::InvalidValue,
249 message: "Invalid value".to_string(),
250 span: Span {
251 start: 100,
252 end: 105,
253 line: 5,
254 column: 20,
255 },
256 suggestion: None,
257 };
258
259 overlay.show(error);
260 let element = overlay.render(TestMessage::Dismiss);
261 drop(element);
263 }
264
265 #[test]
266 fn test_timestamp_updated_on_show() {
267 let mut overlay = ErrorOverlay::new();
268 let initial_timestamp = overlay.timestamp;
269
270 std::thread::sleep(std::time::Duration::from_millis(1));
272
273 let error = ParseError {
274 kind: ParseErrorKind::XmlSyntax,
275 message: "Test".to_string(),
276 span: Span {
277 start: 0,
278 end: 1,
279 line: 1,
280 column: 1,
281 },
282 suggestion: None,
283 };
284
285 overlay.show(error);
286 assert!(overlay.timestamp > initial_timestamp);
287 }
288}