From 3a2d437bd71641862ab49274de57ec59837474bd Mon Sep 17 00:00:00 2001 From: Deuracell Date: Tue, 24 Mar 2026 12:50:20 +0000 Subject: [PATCH] Add `DatePickerButton::reverse_years/year_scroll_to` (#7978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Two new builder methods on `DatePickerButton`: - **`reverse_years(bool)`** — lists years in descending order (newest first). Useful when users are more likely to pick recent years. - **`year_scroll_to(i32)`** — scrolls the year dropdown to a specific year when it first opens, centred in the list. Defaults to the currently selected year, so the picker no longer opens at the top of a 110-item list. ## Why The year `ComboBox` currently always renders in ascending order and opens scrolled to the top. For a range spanning e.g. 1925–2035, the current year is buried near the bottom. Users have to scroll past ~100 entries every time they open the picker. ## Notes - Both options are purely additive builder methods — no breaking changes. - `year_scroll_needed` is a persisted state flag that is set on popup open and cleared after the first scroll, so the user can freely scroll the list after that. - Existing behaviour is unchanged when neither method is called. Co-authored-by: Simon Deurell --- crates/egui_extras/src/datepicker/button.rs | 22 +++++++++++- crates/egui_extras/src/datepicker/popup.rs | 38 +++++++++++++++------ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 98aceefe2..d6f69fe77 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -21,6 +21,8 @@ pub struct DatePickerButton<'a> { format: String, highlight_weekends: bool, start_end_years: Option>, + reverse_years: bool, + year_scroll_to: Option, } impl<'a> DatePickerButton<'a> { @@ -36,6 +38,8 @@ impl<'a> DatePickerButton<'a> { format: "%Y-%m-%d".to_owned(), highlight_weekends: true, start_end_years: None, + reverse_years: false, + year_scroll_to: None, } } @@ -115,6 +119,21 @@ impl<'a> DatePickerButton<'a> { self.start_end_years = Some(start_end_years); self } + + /// List years in descending order in the year dropdown. (Default: false) + #[inline] + pub fn reverse_years(mut self, reverse_years: bool) -> Self { + self.reverse_years = reverse_years; + self + } + + /// Scroll the year dropdown to this year when the picker first opens. + /// Defaults to the currently selected year. + #[inline] + pub fn year_scroll_to(mut self, year: i32) -> Self { + self.year_scroll_to = Some(year); + self + } } impl Widget for DatePickerButton<'_> { @@ -154,7 +173,6 @@ impl Widget for DatePickerButton<'_> { 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()); //TODO(elwerene): Better positioning @@ -182,6 +200,8 @@ impl Widget for DatePickerButton<'_> { calendar_week: self.calendar_week, highlight_weekends: self.highlight_weekends, start_end_years: self.start_end_years, + reverse_years: self.reverse_years, + year_scroll_to: self.year_scroll_to, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index d353307b3..1c24ca81d 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -13,6 +13,7 @@ struct DatePickerPopupState { month: u32, day: u32, setup: bool, + year_scroll_needed: bool, } impl DatePickerPopupState { @@ -36,6 +37,8 @@ pub(crate) struct DatePickerPopup<'a> { pub calendar_week: bool, pub highlight_weekends: bool, pub start_end_years: Option>, + pub reverse_years: bool, + pub year_scroll_to: Option, } impl DatePickerPopup<'_> { @@ -51,6 +54,7 @@ impl DatePickerPopup<'_> { popup_state.month = self.selection.month(); popup_state.day = self.selection.day(); popup_state.setup = true; + popup_state.year_scroll_needed = true; ui.data_mut(|data| data.insert_persisted(id, popup_state.clone())); } @@ -60,7 +64,7 @@ impl DatePickerPopup<'_> { let spacing = 2.0; ui.spacing_mut().item_spacing = Vec2::splat(spacing); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); StripBuilder::new(ui) .clip(false) @@ -89,15 +93,30 @@ impl DatePickerPopup<'_> { Some(range) => (*range.start(), *range.end()), None => (today.year() - 100, today.year() + 10), }; - for year in start_year..=end_year { - if ui - .selectable_value( - &mut popup_state.year, - year, - year.to_string(), - ) - .changed() + let scroll_to_year = + self.year_scroll_to.unwrap_or(popup_state.year); + let years: Vec = if self.reverse_years { + (start_year..=end_year).rev().collect() + } else { + (start_year..=end_year).collect() + }; + for year in years { + let resp = ui.selectable_value( + &mut popup_state.year, + year, + year.to_string(), + ); + if popup_state.year_scroll_needed + && year == scroll_to_year { + resp.scroll_to_me(Some(Align::Center)); + popup_state.year_scroll_needed = false; + ui.memory_mut(|mem| { + mem.data + .insert_persisted(id, popup_state.clone()); + }); + } + if resp.changed() { popup_state.day = popup_state .day .min(popup_state.last_day_of_month()); @@ -349,7 +368,6 @@ impl DatePickerPopup<'_> { ); if day == today { - // Encircle today's date let stroke = ui .visuals() .widgets