Skip to main content

zenity_rs/ui/
entry.rs

1//! Entry dialog implementation for text input.
2
3use 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/// Entry dialog result.
17#[derive(Debug, Clone)]
18pub enum EntryResult {
19    /// User entered text and clicked OK.
20    Text(String),
21    /// User cancelled the dialog.
22    Cancelled,
23    /// Dialog was closed.
24    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
37/// Entry dialog builder.
38pub 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        // First pass: calculate LOGICAL dimensions using scale 1.0
100        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        // Use custom dimensions if provided, otherwise use calculated defaults
134        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        // Create window with LOGICAL dimensions
138        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        // Get the actual scale factor from the window (compositor scale)
146        let scale = window.scale_factor();
147
148        // Calculate physical dimensions from logical dimensions
149        let physical_width = (logical_width as f32 * scale) as u32;
150        let physical_height = (logical_height as f32 * scale) as u32;
151
152        // Now create everything at PHYSICAL scale
153        let font = Font::load(scale);
154
155        // Scale dimensions for physical rendering
156        let padding = (BASE_PADDING as f32 * scale) as u32;
157        let button_spacing = (BASE_BUTTON_SPACING as f32 * scale) as u32;
158
159        // Input should fill available width
160        let input_width = physical_width - (padding * 2);
161
162        // Create buttons at physical scale
163        let mut ok_button = Button::new("OK", &font, scale);
164        let mut cancel_button = Button::new("Cancel", &font, scale);
165
166        // Create text input at physical scale
167        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        // Render prompt text at physical scale (wrapped to fit)
173        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        // Position elements in physical coordinates
186        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 position
193        input.set_position(padding as i32, y);
194        y += input.height() as i32 + (BASE_BUTTON_SPACING as f32 * scale) as i32;
195
196        // Button positions (right-aligned)
197        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        // Create canvas at PHYSICAL dimensions
204        let mut canvas = Canvas::new(physical_width, physical_height);
205
206        // Draw function
207        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            // Draw prompt
231            if let Some(prompt) = prompt_canvas {
232                canvas.draw_canvas(prompt, padding as i32, prompt_y);
233            }
234
235            // Draw input
236            input.draw_to(canvas, colors, font);
237
238            // Draw buttons
239            ok_button.draw_to(canvas, colors, font);
240            cancel_button.draw_to(canvas, colors, font);
241        };
242
243        // Initial draw
244        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        // Event loop
260        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                    // Check if cursor is over the input field
293                    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            // Process input events
324            let mut needs_redraw = input.process_event(&event);
325
326            // Check for Enter key submission
327            if input.was_submitted() {
328                return Ok(EntryResult::Text(input.text().to_string()));
329            }
330
331            // Process button events
332            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            // Batch process pending events
347            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}