rust_widgets 2.1.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 60+ widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Progress dialog widget.
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::GenericSignal;
use crate::tr;
use crate::widget::capability::coercion::expect_string;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
/// Progress dialog widget.
/// Progress dialog widget.
///
/// A modal-style dialog that reports the progress of a long operation on an
/// integer range and offers a single cancel affordance. It owns no timer: the
/// caller drives it by calling [`ProgressDialog::set_value`], which is the only
/// way the displayed progress changes.
///
pub struct ProgressDialog {
    base: BaseWidget,
    title: String,
    label_text: String,
    value: i32,
    minimum: i32,
    maximum: i32,
    cancel_button_text: String,
    was_canceled: bool,
    auto_close: bool,
    auto_reset: bool,
    modal: bool,
    /// Signal emitted when the user cancels, either by pressing Escape while
    /// the dialog is enabled or by calling [`ProgressDialog::cancel`] directly.
    ///
    /// Carries no payload; the reason is not distinguishible from the signal
    /// alone. Connect through [`Widget::connection_scope`] so the slot is
    /// disconnected when the dialog is dropped.
    pub canceled: GenericSignal,
}
impl ProgressDialog {
    /// Creates a dialog with an empty title and label, a range of `0..=100`, a
    /// value of `0`, the translated "Cancel" button text, and `auto_close`,
    /// `auto_reset`, and `modal` all `true`.
    ///
    /// `geometry` is in parent-relative logical pixels; the default size hint
    /// is 350x120, which the caller is free to override.
    pub fn new(geometry: Rect) -> Self {
        Self {
            base: BaseWidget::new(WidgetKind::ProgressDialog, geometry, "ProgressDialog"),
            title: String::new(),
            label_text: String::new(),
            value: 0,
            minimum: 0,
            maximum: 100,
            cancel_button_text: tr!("common.button.cancel"),
            was_canceled: false,
            auto_close: true,
            auto_reset: true,
            modal: true,
            canceled: GenericSignal::new(),
        }
    }
    /// Returns the dialog title, drawn in the title bar. Empty by default.
    pub fn title(&self) -> &str {
        &self.title
    }
    /// Returns the label drawn above the progress bar. Empty by default.
    pub fn label_text(&self) -> &str {
        &self.label_text
    }
    /// Returns the current value, always within `minimum() ..= maximum()`.
    pub fn value(&self) -> i32 {
        self.value
    }
    /// Returns the lower bound of the progress range. Defaults to `0`.
    pub fn minimum(&self) -> i32 {
        self.minimum
    }
    /// Returns the upper bound of the progress range. Defaults to `100`; this
    /// is also the value at which `auto_close` hides the dialog.
    pub fn maximum(&self) -> i32 {
        self.maximum
    }
    /// Returns `true` once the user (or [`ProgressDialog::cancel`]) has
    /// cancelled. Cleared again by [`ProgressDialog::reset`].
    pub fn was_canceled(&self) -> bool {
        self.was_canceled
    }
    /// Returns whether reaching the maximum hides the dialog automatically.
    /// Defaults to `true`.
    pub fn auto_close(&self) -> bool {
        self.auto_close
    }
    /// Returns whether cancellation resets the value back to the minimum.
    /// Defaults to `true`.
    pub fn auto_reset(&self) -> bool {
        self.auto_reset
    }
    /// Returns the cancel button caption. Defaults to the translated
    /// `common.button.cancel` string.
    pub fn cancel_button_text(&self) -> &str {
        &self.cancel_button_text
    }
    /// Sets the title bar text and requests a redraw.
    pub fn set_title(&mut self, t: impl Into<String>) {
        self.title = t.into();
        self.base.request_redraw();
    }
    /// Sets the label shown above the progress bar and requests a redraw.
    pub fn set_label_text(&mut self, t: impl Into<String>) {
        self.label_text = t.into();
        self.base.request_redraw();
    }
    /// Sets the lower bound of the progress range.
    ///
    /// The new bound is **not** applied to the current value, so the value can
    /// temporarily sit outside the range until the next
    /// [`ProgressDialog::set_value`] call clamps it. A `min` above `max` makes
    /// [`ProgressDialog::progress_fraction`] report full progress.
    pub fn set_minimum(&mut self, min: i32) {
        self.minimum = min;
        self.base.request_redraw();
    }
    /// Sets the upper bound of the progress range. As with
    /// [`ProgressDialog::set_minimum`], the current value is not re-clamped
    /// until the next `set_value`.
    pub fn set_maximum(&mut self, max: i32) {
        self.maximum = max;
        self.base.request_redraw();
    }
    /// Sets both minimum and maximum in one call.
    /// This is a convenience writer; query bounds via `minimum()` and `maximum()`.
    pub fn set_range(&mut self, min: i32, max: i32) {
        self.minimum = min;
        self.maximum = max;
        self.base.request_redraw();
    }
    /// Toggles automatic hiding at completion; requests a redraw. See
    /// [`ProgressDialog::auto_close`].
    pub fn set_auto_close(&mut self, v: bool) {
        self.auto_close = v;
        self.base.request_redraw();
    }
    /// Toggles automatic reset on restart; requests a redraw. See
    /// [`ProgressDialog::auto_reset`].
    ///
    /// Note the flag is stored but not consulted anywhere in this type:
    /// [`ProgressDialog::cancel`] never resets the value, and
    /// [`ProgressDialog::reset`] is an explicit call. The intended consumer is a
    /// caller coordinating restarts.
    pub fn set_auto_reset(&mut self, v: bool) {
        self.auto_reset = v;
        self.base.request_redraw();
    }
    /// Sets the caption of the cancel button and requests a redraw.
    pub fn set_cancel_button_text(&mut self, t: impl Into<String>) {
        self.cancel_button_text = t.into();
        self.base.request_redraw();
    }
    /// Returns whether the dialog is modal (blocks interaction with the widgets
    /// behind it). Defaults to `true`.
    ///
    /// This is advisory: it records the caller's intent for the surrounding
    /// runtime or dialog manager, which is what actually enforces modality.
    pub fn is_modal(&self) -> bool {
        self.modal
    }
    /// Sets the modality intent. See [`ProgressDialog::is_modal`].
    pub fn set_modal(&mut self, modal: bool) {
        self.modal = modal;
        self.base.request_redraw();
    }
    /// Moves the progress value, clamped into `minimum() ..= maximum()`.
    ///
    /// Side effects: when `auto_close` is set and the clamped value reaches the
    /// maximum, the dialog hides itself. Hiding does not emit `canceled`. The
    /// value is set before hiding, so [`ProgressDialog::value`] still reports
    /// the maximum afterwards. A redraw is requested in all cases.
    pub fn set_value(&mut self, value: i32) {
        self.value = value.clamp(self.minimum, self.maximum);
        if self.auto_close && self.value >= self.maximum {
            self.hide();
        }
        self.base.request_redraw();
    }
    /// Resets the value to the minimum and clears the cancelled flag.
    ///
    /// Does not show the dialog: combine with [`Widget::show`] when reusing a
    /// hidden dialog. The `auto_reset` flag is not consulted here.
    pub fn reset(&mut self) {
        self.value = self.minimum;
        self.was_canceled = false;
    }
    /// Marks the dialog as cancelled, emits `canceled`, and hides the dialog.
    ///
    /// The signal is emitted **before** the dialog is hidden, so a slot that
    /// reads geometry still sees the visible state. Calling `cancel` twice
    /// emits the signal twice.
    pub fn cancel(&mut self) {
        self.was_canceled = true;
        self.canceled.emit();
        self.hide();
    }
    /// Returns progress as a fraction in `0.0..=1.0`, where `0.0` is the
    /// minimum and `1.0` is the maximum.
    ///
    /// An empty or inverted range (`maximum <= minimum`) reports `1.0`
    /// unconditionally, i.e. a single-valued range is treated as complete
    /// rather than as division by zero.
    pub fn progress_fraction(&self) -> f32 {
        let range = self.maximum - self.minimum;
        if range <= 0 {
            return 1.0;
        }
        (self.value - self.minimum) as f32 / range as f32
    }
}
impl Widget for ProgressDialog {
    /// Access to the shared base-widget state; all default trait behaviour
    /// delegates through this.
    fn base(&self) -> &BaseWidget {
        &self.base
    }

    /// Mutable access to the shared base-widget state.
    fn base_mut(&mut self) -> &mut BaseWidget {
        &mut self.base
    }

    fn size_hint(&self) -> Size {
        crate::core::Size::new(350, 120)
    }

    /// Reports this widget as the object that paints it.
    ///
    /// `ProgressDialog` implements `Draw`, so `Some(self)` is total and cannot be
    /// wrong.
    fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
        Some(self)
    }

    impl_widget_property_hooks!();
}

/// `ProgressDialog`'s property contract.
///
/// Read semantics are carried over unchanged from the centralised
/// `access_read_dialog.in.rs` dispatch. Both properties are read-only: the old
/// write layer had no arm for this kind.
impl WidgetProperties for ProgressDialog {
    fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
        match name {
            "title" => Ok(CapabilityValue::String(self.title().to_string())),
            "label_text" => Ok(CapabilityValue::String(self.label_text().to_string())),
            _ => base_property_get(self, name),
        }
    }

    fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
        match name {
            "title" => {
                self.set_title(expect_string(value)?);
                Ok(())
            }
            "label_text" => {
                self.set_label_text(expect_string(value)?);
                Ok(())
            }
            _ => base_property_set(self, name, value),
        }
    }

    fn property_names(&self) -> &'static [&'static str] {
        property_names_of!["title", "label_text", BASE_PROPERTY_NAMES]
    }
}
impl EventHandler for ProgressDialog {
    fn handle_event(&mut self, event: &Event) {
        self.base.handle_event(event);
        if !self.base.is_enabled() {
            return;
        }
        match event {
            Event::KeyPress { key, .. } if *key == 27 => self.cancel(),
            _ => { /* Other events are not relevant */ }
        }
    }
}
impl Draw for ProgressDialog {
    fn draw(&mut self, context: &mut RenderContext) {
        let rect = self.geometry();
        context.fill_rect(
            Rect::new(rect.x, rect.y, rect.width, rect.height),
            Color::rgb(245, 245, 245),
        );
        context.draw_rect(
            Rect::new(rect.x, rect.y, rect.width, rect.height),
            Color::rgb(160, 160, 160),
        );
        context.fill_rect(Rect::new(rect.x, rect.y, rect.width, 28), Color::rgb(0, 120, 215));
        context.draw_text(
            Point::new(rect.x + 8, rect.y + 14),
            &self.title,
            &Font::default(),
            Color::rgb(255, 255, 255),
            HorizontalAlignment::Left,
        );
        // Label
        context.draw_text(
            Point::new(rect.x + 10, rect.y + 48),
            &self.label_text,
            &Font::default(),
            Color::rgb(0, 0, 0),
            HorizontalAlignment::Left,
        );
        // Progress bar
        let bar_y = rect.y + 62;
        let bar_w = rect.width.saturating_sub(20);
        let bar_h: u32 = 20;
        context.fill_rect(Rect::new(rect.x + 10, bar_y, bar_w, bar_h), Color::rgb(220, 220, 220));
        context.draw_rect(Rect::new(rect.x + 10, bar_y, bar_w, bar_h), Color::rgb(150, 150, 150));
        let fill_w = (bar_w as f32 * self.progress_fraction()) as i32;
        if fill_w > 0 {
            context.fill_rect(
                Rect::new(rect.x + 10, bar_y, fill_w.max(0) as u32, bar_h),
                Color::rgb(6, 176, 37),
            );
        }
        // Percentage text
        let pct = (self.progress_fraction() * 100.0) as i32;
        context.draw_text(
            Point::new(rect.x + 10 + (bar_w as i32 / 2), bar_y + (bar_h as i32 / 2)),
            &format!("{pct}%"),
            &Font::default(),
            Color::rgb(0, 0, 0),
            HorizontalAlignment::Left,
        );
        // Cancel button
        let btn_y = rect.y as f32 + rect.height as f32 - 40.0;
        let btn_w = 80;
        context.fill_rect(
            Rect::new(
                rect.x + rect.width as i32 / 2 - btn_w / 2,
                btn_y as i32,
                btn_w as u32,
                28u32,
            ),
            Color::rgb(225, 225, 225),
        );
        context.draw_rect(
            Rect::new(
                rect.x + rect.width as i32 / 2 - btn_w / 2,
                btn_y as i32,
                btn_w as u32,
                28u32,
            ),
            Color::rgb(100, 100, 100),
        );
        context.draw_text(
            Point::new(rect.x + rect.width as i32 / 2, (btn_y + 14.0) as i32),
            &self.cancel_button_text,
            &Font::default(),
            Color::rgb(0, 0, 0),
            HorizontalAlignment::Left,
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::event::Event;
    use std::sync::{Arc, Mutex};

    #[test]
    fn set_value_clamps_and_auto_closes_on_max() {
        let mut dialog = ProgressDialog::new(Rect::new(0, 0, 360, 160));
        dialog.set_range(10, 20);

        dialog.set_value(5);
        assert_eq!(dialog.value(), 10);

        dialog.show();
        dialog.set_value(99);
        assert_eq!(dialog.value(), 20);
        assert!(!dialog.is_visible());
    }

    #[test]
    fn escape_key_cancels_and_emits_signal() {
        let mut dialog = ProgressDialog::new(Rect::new(0, 0, 360, 160));
        let canceled = Arc::new(Mutex::new(0usize));
        let canceled_clone = Arc::clone(&canceled);

        dialog.canceled.connect(move || {
            if let Ok(mut n) = canceled_clone.lock() {
                *n += 1;
            }
        });

        dialog.show();
        dialog.handle_event(&Event::key_press(27, 0));
        assert!(dialog.was_canceled());
        assert_eq!(*canceled.lock().expect("canceled lock"), 1);
        assert!(!dialog.is_visible());
    }
}