1use crate::{
4 backend::{CursorShape, Window, WindowEvent, create_window},
5 error::Error,
6 render::{Canvas, Font},
7 ui::{
8 BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, Colors, KEY_ESCAPE,
9 widgets::{Widget, button::Button, text_input::TextInput},
10 },
11};
12
13const BASE_PADDING: u32 = 20;
14const BASE_INPUT_WIDTH: u32 = 300;
15
16#[derive(Debug, Clone)]
18pub enum EntryResult {
19 Text(String),
21 Cancelled,
23 Closed,
25}
26
27impl EntryResult {
28 pub fn exit_code(&self) -> i32 {
29 match self {
30 EntryResult::Text(_) => 0,
31 EntryResult::Cancelled => 1,
32 EntryResult::Closed => 1,
33 }
34 }
35}
36
37pub struct EntryBuilder {
39 title: String,
40 text: String,
41 entry_text: String,
42 hide_text: bool,
43 width: Option<u32>,
44 height: Option<u32>,
45 colors: Option<&'static Colors>,
46}
47
48impl EntryBuilder {
49 pub fn new() -> Self {
50 Self {
51 title: String::new(),
52 text: String::new(),
53 entry_text: String::new(),
54 hide_text: false,
55 width: None,
56 height: None,
57 colors: None,
58 }
59 }
60
61 pub fn title(mut self, title: &str) -> Self {
62 self.title = title.to_string();
63 self
64 }
65
66 pub fn text(mut self, text: &str) -> Self {
67 self.text = text.to_string();
68 self
69 }
70
71 pub fn entry_text(mut self, entry_text: &str) -> Self {
72 self.entry_text = entry_text.to_string();
73 self
74 }
75
76 pub fn hide_text(mut self, hide: bool) -> Self {
77 self.hide_text = hide;
78 self
79 }
80
81 pub fn colors(mut self, colors: &'static Colors) -> Self {
82 self.colors = Some(colors);
83 self
84 }
85
86 pub fn width(mut self, width: u32) -> Self {
87 self.width = Some(width);
88 self
89 }
90
91 pub fn height(mut self, height: u32) -> Self {
92 self.height = Some(height);
93 self
94 }
95
96 pub fn show(self) -> Result<EntryResult, Error> {
97 let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
98
99 let temp_font = Font::load(1.0);
101 let temp_ok = Button::new("OK", &temp_font, 1.0);
102 let temp_cancel = Button::new("Cancel", &temp_font, 1.0);
103 let temp_prompt_height = if !self.text.is_empty() {
104 temp_font
105 .render(&self.text)
106 .with_max_width(BASE_INPUT_WIDTH as f32)
107 .finish()
108 .height()
109 } else {
110 0
111 };
112 let temp_input = TextInput::new(BASE_INPUT_WIDTH);
113
114 let logical_buttons_width = temp_ok.width() + temp_cancel.width() + BASE_BUTTON_SPACING;
115 let logical_content_width = BASE_INPUT_WIDTH.max(logical_buttons_width);
116 let calc_width = logical_content_width + BASE_PADDING * 2;
117 let calc_height = BASE_PADDING * 3
118 + temp_prompt_height
119 + (if temp_prompt_height > 0 {
120 BASE_BUTTON_SPACING
121 } else {
122 0
123 })
124 + temp_input.height()
125 + BASE_BUTTON_SPACING
126 + BASE_BUTTON_HEIGHT;
127
128 drop(temp_font);
129 drop(temp_ok);
130 drop(temp_cancel);
131 drop(temp_input);
132
133 let logical_width = self.width.unwrap_or(calc_width) as u16;
135 let logical_height = self.height.unwrap_or(calc_height) as u16;
136
137 let mut window = create_window(logical_width, logical_height)?;
139 window.set_title(if self.title.is_empty() {
140 "Entry"
141 } else {
142 &self.title
143 })?;
144
145 let scale = window.scale_factor();
147
148 let physical_width = (logical_width as f32 * scale) as u32;
150 let physical_height = (logical_height as f32 * scale) as u32;
151
152 let font = Font::load(scale);
154
155 let padding = (BASE_PADDING as f32 * scale) as u32;
157 let button_spacing = (BASE_BUTTON_SPACING as f32 * scale) as u32;
158
159 let input_width = physical_width - (padding * 2);
161
162 let mut ok_button = Button::new("OK", &font, scale);
164 let mut cancel_button = Button::new("Cancel", &font, scale);
165
166 let mut input = TextInput::new(input_width)
168 .with_password(self.hide_text)
169 .with_default_text(&self.entry_text);
170 input.set_focus(true);
171
172 let prompt_canvas = if !self.text.is_empty() {
174 Some(
175 font.render(&self.text)
176 .with_color(colors.text)
177 .with_max_width((physical_width - padding * 2) as f32)
178 .finish(),
179 )
180 } else {
181 None
182 };
183 let prompt_height = prompt_canvas.as_ref().map(|c| c.height()).unwrap_or(0);
184
185 let mut y = padding as i32;
187 let prompt_y = y;
188 if prompt_height > 0 {
189 y += prompt_height as i32 + (BASE_BUTTON_SPACING as f32 * scale) as i32;
190 }
191
192 input.set_position(padding as i32, y);
194 y += input.height() as i32 + (BASE_BUTTON_SPACING as f32 * scale) as i32;
195
196 let mut button_x = physical_width as i32 - padding as i32;
198 button_x -= cancel_button.width() as i32;
199 cancel_button.set_position(button_x, y);
200 button_x -= button_spacing as i32 + ok_button.width() as i32;
201 ok_button.set_position(button_x, y);
202
203 let mut canvas = Canvas::new(physical_width, physical_height);
205
206 let draw = |canvas: &mut Canvas,
208 colors: &Colors,
209 font: &Font,
210 prompt_canvas: &Option<Canvas>,
211 input: &TextInput,
212 ok_button: &Button,
213 cancel_button: &Button,
214 padding: u32,
215 prompt_y: i32,
216 scale: f32| {
217 let width = canvas.width() as f32;
218 let height = canvas.height() as f32;
219 let radius = BASE_CORNER_RADIUS * scale;
220
221 canvas.fill_dialog_bg(
222 width,
223 height,
224 colors.window_bg,
225 colors.window_border,
226 colors.window_shadow,
227 radius,
228 );
229
230 if let Some(prompt) = prompt_canvas {
232 canvas.draw_canvas(prompt, padding as i32, prompt_y);
233 }
234
235 input.draw_to(canvas, colors, font);
237
238 ok_button.draw_to(canvas, colors, font);
240 cancel_button.draw_to(canvas, colors, font);
241 };
242
243 draw(
245 &mut canvas,
246 colors,
247 &font,
248 &prompt_canvas,
249 &input,
250 &ok_button,
251 &cancel_button,
252 padding,
253 prompt_y,
254 scale,
255 );
256 window.set_contents(&canvas)?;
257 window.show()?;
258
259 let mut window_dragging = false;
261 loop {
262 let event = window.wait_for_event()?;
263
264 match &event {
265 WindowEvent::CloseRequested => {
266 return Ok(EntryResult::Closed);
267 }
268 WindowEvent::RedrawRequested => {
269 draw(
270 &mut canvas,
271 colors,
272 &font,
273 &prompt_canvas,
274 &input,
275 &ok_button,
276 &cancel_button,
277 padding,
278 prompt_y,
279 scale,
280 );
281 window.set_contents(&canvas)?;
282 }
283 WindowEvent::CursorMove(pos) => {
284 if window_dragging {
285 let _ = window.start_drag();
286 window_dragging = false;
287 }
288
289 let cursor_x = pos.x as i32;
290 let cursor_y = pos.y as i32;
291
292 let ix = input.x();
294 let iy = input.y();
295 let iw = input.width();
296 let ih = input.height();
297
298 let over_input = cursor_x >= ix
299 && cursor_x < ix + iw as i32
300 && cursor_y >= iy
301 && cursor_y < iy + ih as i32;
302
303 let _ = window.set_cursor(if over_input {
304 CursorShape::Text
305 } else {
306 CursorShape::Default
307 });
308 }
309 WindowEvent::KeyPress(key_event) => {
310 if key_event.keysym == KEY_ESCAPE {
311 return Ok(EntryResult::Closed);
312 }
313 }
314 WindowEvent::ButtonPress(crate::backend::MouseButton::Left, _) => {
315 window_dragging = true;
316 }
317 WindowEvent::ButtonRelease(crate::backend::MouseButton::Left, _) => {
318 window_dragging = false;
319 }
320 _ => {}
321 }
322
323 let mut needs_redraw = input.process_event(&event);
325
326 if input.was_submitted() {
328 return Ok(EntryResult::Text(input.text().to_string()));
329 }
330
331 if ok_button.process_event(&event) {
333 needs_redraw = true;
334 }
335 if cancel_button.process_event(&event) {
336 needs_redraw = true;
337 }
338
339 if ok_button.was_clicked() {
340 return Ok(EntryResult::Text(input.text().to_string()));
341 }
342 if cancel_button.was_clicked() {
343 return Ok(EntryResult::Cancelled);
344 }
345
346 while let Some(event) = window.poll_for_event()? {
348 match &event {
349 WindowEvent::CloseRequested => {
350 return Ok(EntryResult::Closed);
351 }
352 _ => {
353 if input.process_event(&event) {
354 needs_redraw = true;
355 }
356 if input.was_submitted() {
357 return Ok(EntryResult::Text(input.text().to_string()));
358 }
359 if ok_button.process_event(&event) {
360 needs_redraw = true;
361 }
362 if cancel_button.process_event(&event) {
363 needs_redraw = true;
364 }
365 if ok_button.was_clicked() {
366 return Ok(EntryResult::Text(input.text().to_string()));
367 }
368 if cancel_button.was_clicked() {
369 return Ok(EntryResult::Cancelled);
370 }
371 }
372 }
373 }
374
375 if needs_redraw {
376 draw(
377 &mut canvas,
378 colors,
379 &font,
380 &prompt_canvas,
381 &input,
382 &ok_button,
383 &cancel_button,
384 padding,
385 prompt_y,
386 scale,
387 );
388 window.set_contents(&canvas)?;
389 }
390 }
391 }
392}
393
394impl Default for EntryBuilder {
395 fn default() -> Self {
396 Self::new()
397 }
398}