egui_timepicker 0.1.0

A time picker widget for egui
Documentation
use chrono::NaiveTime;
use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Widget};

use crate::popup::TimePickerPopup;

#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct TimePickerButtonState {
    pub picker_visible: bool,
}

pub struct TimePickerButton<'a> {
    selection: &'a mut NaiveTime,
    id_salt: Option<&'a str>,
    show_icon: bool,
    format: String,
    show_clockface: bool,
    use_12_hour_clock: bool,
    show_seconds: bool,
    use_dragvalue: bool,
}

impl<'a> TimePickerButton<'a> {
    pub fn new(selection: &'a mut NaiveTime) -> Self {
        Self {
            selection,
            id_salt: None,
            show_icon: true,
            format: "%H:%M".to_string(),
            show_clockface: true,
            use_12_hour_clock: false,
            show_seconds: false,
            use_dragvalue: false,
        }
    }

    pub fn id_salt(mut self, id_salt: &'a str) -> Self {
        self.id_salt = Some(id_salt);
        self
    }

    pub fn show_icon(mut self, show_icon: bool) -> Self {
        self.show_icon = show_icon;
        self
    }

    /// Format string for the time. See [chrono::format::strftime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for details.
    pub fn format(mut self, format: &'a str) -> Self {
        self.format = format.to_string();
        self
    }

    pub fn show_clockface(mut self, show_clockface: bool) -> Self {
        self.show_clockface = show_clockface;
        self
    }

    pub fn use_12_hour_clock(mut self, use_12_hour_clock: bool) -> Self {
        self.use_12_hour_clock = use_12_hour_clock;
        self
    }

    pub fn show_seconds(mut self, show_seconds: bool) -> Self {
        self.show_seconds = show_seconds;
        self
    }

    pub fn use_dragvalue(mut self, use_dragvalue: bool) -> Self {
        self.use_dragvalue = use_dragvalue;
        self
    }
}

impl Widget for TimePickerButton<'_> {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        let id = ui.make_persistent_id(self.id_salt);
        let mut button_state = ui
            .data_mut(|data| data.get_persisted::<TimePickerButtonState>(id))
            .unwrap_or_default();

        let mut text = if self.show_icon {
            RichText::new(format!("{} 🕒", self.selection.format(&self.format)))
        } else {
            RichText::new(self.selection.format(&self.format).to_string())
        };

        let visuals = ui.visuals().widgets.open;
        if button_state.picker_visible {
            text = text.color(visuals.text_color());
        }

        let mut button = Button::new(text);
        if button_state.picker_visible {
            button = button.fill(visuals.weak_bg_fill).stroke(visuals.bg_stroke);
        }

        let mut button_response = ui.add(button);
        if button_response.clicked() {
            button_state.picker_visible = true;
            ui.data_mut(|data| data.insert_persisted(id, button_state.clone()));
        }

        if button_state.picker_visible {
            let width = 250.;
            let mut pos = button_response.rect.left_bottom();
            let width_with_padding = width
                + ui.style().spacing.item_spacing.x
                + ui.style().spacing.window_margin.leftf()
                + ui.style().spacing.window_margin.rightf();

            if pos.x + width_with_padding > ui.clip_rect().right() {
                pos.x = button_response.rect.right() - width_with_padding;
            }

            // Check to make sure the calendar never is displayed out of window
            pos.x = pos.x.max(ui.style().spacing.window_margin.leftf());

            let InnerResponse {
                inner: saved,
                response: area_response,
            } = Area::new(ui.make_persistent_id(self.id_salt))
                .kind(egui::UiKind::Picker)
                .order(Order::Foreground)
                .fixed_pos(pos)
                .show(ui.ctx(), |ui| {
                    let frame = Frame::popup(ui.style());
                    frame
                        .show(ui, |ui| {
                            ui.set_min_width(width);
                            ui.set_max_width(width);

                            TimePickerPopup {
                                selection: self.selection,
                                button_id: id,
                                show_clockface: self.show_clockface,
                                show_seconds: self.show_seconds,
                                use_12_hour_clock: self.use_12_hour_clock,
                                use_dragvalue: self.use_dragvalue,
                            }
                            .draw(ui)
                        })
                        .inner
                });

            if saved {
                button_response.mark_changed();
            }

            // We don't want to close our popup if any other popup is open, since other popups would
            // most likely be the combo boxes in the date picker.
            let any_popup_open = ui.ctx().is_popup_open();
            if !button_response.clicked()
                && !any_popup_open
                && (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
            {
                button_state.picker_visible = false;
                ui.data_mut(|data| data.insert_persisted(id, button_state));
            }
        }

        button_response
    }
}