diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index d0b5b6980..7012d0e1b 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,12 +1,13 @@ -use crate::{Frame, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; -use emath::{Align2, Vec2}; -use epaint::Galley; +use crate::{Frame, Id, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use ahash::HashMap; +use emath::{Align2, Rect, Vec2}; +use epaint::{Color32, Galley}; use std::sync::Arc; pub enum SizedAtomicKind<'a> { Text(Arc), Image(Image<'a>, Vec2), - Custom(Vec2), + Custom(Id, Vec2), Grow, } @@ -15,7 +16,7 @@ impl SizedAtomicKind<'_> { match self { SizedAtomicKind::Text(galley) => galley.size(), SizedAtomicKind::Image(_, size) => *size, - SizedAtomicKind::Custom(size) => *size, + SizedAtomicKind::Custom(_, size) => *size, SizedAtomicKind::Grow => Vec2::ZERO, } } @@ -24,18 +25,20 @@ impl SizedAtomicKind<'_> { /// AtomicLayout pub struct WidgetLayout<'a> { pub atomics: Atomics<'a>, - gap: f32, + gap: Option, pub(crate) frame: Frame, pub(crate) sense: Sense, + fallback_text_color: Option, } impl<'a> WidgetLayout<'a> { pub fn new(atomics: impl IntoAtomics<'a>) -> Self { Self { atomics: atomics.into_atomics(), - gap: 4.0, + gap: None, frame: Frame::default(), sense: Sense::hover(), + fallback_text_color: None, } } @@ -44,8 +47,9 @@ impl<'a> WidgetLayout<'a> { self } + /// Default: `Spacing::icon_spacing` pub fn gap(mut self, gap: f32) -> Self { - self.gap = gap; + self.gap = Some(gap); self } @@ -59,7 +63,17 @@ impl<'a> WidgetLayout<'a> { self } - pub fn show(self, ui: &mut Ui) -> Response { + pub fn fallback_text_color(mut self, color: Color32) -> Self { + self.fallback_text_color = Some(color); + self + } + + pub fn show(self, ui: &mut Ui) -> AtomicLayoutResponse { + let fallback_text_color = self + .fallback_text_color + .unwrap_or_else(|| ui.style().visuals.text_color()); + let gap = self.gap.unwrap_or(ui.spacing().icon_spacing); + let available_size = ui.available_size(); let available_width = available_size.x; @@ -87,7 +101,7 @@ impl<'a> WidgetLayout<'a> { let size = size.unwrap_or_default(); (size, SizedAtomicKind::Image(image, size)) } - AtomicKind::Custom(size) => (size, SizedAtomicKind::Custom(size)), + AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)), AtomicKind::Grow => { grow_count += 1; (Vec2::ZERO, SizedAtomicKind::Grow) @@ -104,7 +118,7 @@ impl<'a> WidgetLayout<'a> { } if sized_items.len() > 1 { - let gap_space = self.gap * (sized_items.len() as f32 - 1.0); + let gap_space = gap * (sized_items.len() as f32 - 1.0); desired_width += gap_space; preferred_width += gap_space; } @@ -115,6 +129,11 @@ impl<'a> WidgetLayout<'a> { let (rect, response) = ui.allocate_at_least(frame_size, self.sense); + let mut response = AtomicLayoutResponse { + response, + custom_rects: HashMap::default(), + }; + let content_rect = rect - margin; ui.painter().add(self.frame.paint(content_rect)); @@ -132,20 +151,21 @@ impl<'a> WidgetLayout<'a> { }; let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); - cursor = frame.right() + self.gap; + cursor = frame.right() + gap; let align = Align2::CENTER_CENTER; let rect = align.align_size_within_rect(size, frame); match sized { SizedAtomicKind::Text(galley) => { - ui.painter() - .galley(rect.min, galley, ui.visuals().text_color()); + ui.painter().galley(rect.min, galley, fallback_text_color); } SizedAtomicKind::Image(image, _) => { image.paint_at(ui, rect); } - SizedAtomicKind::Custom(_) => {} + SizedAtomicKind::Custom(id, size) => { + response.custom_rects.insert(id, rect); + } SizedAtomicKind::Grow => {} } } @@ -154,6 +174,11 @@ impl<'a> WidgetLayout<'a> { } } +pub struct AtomicLayoutResponse { + pub response: Response, + pub custom_rects: HashMap, +} + // pub struct WLButton<'a> { // wl: WidgetLayout<'a>, // } @@ -217,7 +242,7 @@ impl<'a> WidgetLayout<'a> { pub enum AtomicKind<'a> { Text(WidgetText), Image(Image<'a>), - Custom(Vec2), + Custom(Id, Vec2), Grow, } @@ -247,7 +272,7 @@ impl Atomic<'_> { // } } -trait AtomicExt<'a> { +pub trait AtomicExt<'a> { fn a_size(self, size: Vec2) -> Atomic<'a>; fn a_grow(self, grow: bool) -> Atomic<'a>; } @@ -317,6 +342,21 @@ impl<'a> Atomics<'a> { pub fn iter_mut(&mut self) -> impl Iterator> { self.0.iter_mut() } + + pub fn text(&self) -> Option { + let mut string: Option = None; + for atomic in &self.0 { + if let AtomicKind::Text(text) = &atomic.kind { + if let Some(string) = &mut string { + string.push(' '); + string.push_str(text.text()); + } else { + string = Some(text.text().to_owned()); + } + } + } + string + } } impl<'a, T> IntoAtomics<'a> for T @@ -368,3 +408,20 @@ all_the_atomics!(T0, T1, T2); all_the_atomics!(T0, T1, T2, T3); all_the_atomics!(T0, T1, T2, T3, T4); all_the_atomics!(T0, T1, T2, T3, T4, T5); + +// trait AtomicWidget { +// fn show(&self, ui: &mut Ui) -> WidgetLayout; +// } + +// TODO: This conflicts with the FnOnce Widget impl, is there some way around that? +// impl Widget for T where T: AtomicWidget { +// fn ui(self, ui: &mut Ui) -> Response { +// ui.add(self) +// } +// } + +impl Widget for WidgetLayout<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.show(ui).response + } +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 50feabfac..691a71bdd 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,7 @@ use crate::{ - widgets, Align, Atomic, AtomicKind, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt, - Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, - WidgetLayout, WidgetText, WidgetType, + widgets, Align, Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius, + Frame, Image, IntoAtomics, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, + Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, }; /// Clickable button with text. @@ -184,14 +184,20 @@ impl<'a> Button<'a> { /// See also [`Self::right_text`]. #[inline] pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { - self.wl = self.wl.add(shortcut_text); + self.wl = self + .wl + .add(AtomicKind::Grow.a_grow(true)) + .add(shortcut_text); self } /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl Into>) -> Self { - self.wl = self.wl.add(right_text.into()); + self.wl = self + .wl + .add(AtomicKind::Grow.a_grow(true)) + .add(right_text.into()); self } @@ -201,10 +207,8 @@ impl<'a> Button<'a> { self.selected = selected; self } -} -impl Widget for Button<'_> { - fn ui(mut self, ui: &mut Ui) -> Response { + pub fn atomic_ui(mut self, ui: &mut Ui) -> AtomicLayoutResponse { let Button { wrap_mode, fill, @@ -241,26 +245,37 @@ impl Widget for Button<'_> { ui.style().interact(&response) }); + wl = wl.fallback_text_color(visuals.text_color()); + wl.frame = if has_frame { wl.frame .inner_margin(button_padding) - .fill(fill.unwrap_or(visuals.bg_fill)) + .fill(fill.unwrap_or(visuals.weak_bg_fill)) .stroke(stroke.unwrap_or(visuals.bg_stroke)) .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) } else { Frame::new() }; + let text = wl.atomics.text(); + let response = wl.show(ui); - // TODO: How to get text? - // response.widget_info(|| { - // if let Some(galley) = &galley { - // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - // } else { - // WidgetInfo::new(WidgetType::Button) - // } - // }); + response.response.widget_info(|| { + if let Some(text) = &text { + WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text) + } else { + WidgetInfo::new(WidgetType::Button) + } + }); + + response + } +} + +impl Widget for Button<'_> { + fn ui(mut self, ui: &mut Ui) -> Response { + self.atomic_ui(ui).response // // let space_available_for_image = if let Some(text) = &text { @@ -326,13 +341,6 @@ impl Widget for Button<'_> { // desired_size = desired_size.at_least(min_size); // // let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - // response.widget_info(|| { - // if let Some(galley) = &galley { - // WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - // } else { - // WidgetInfo::new(WidgetType::Button) - // } - // }); // // if ui.is_rect_visible(rect) { // let visuals = ui.style().interact(&response); @@ -435,7 +443,5 @@ impl Widget for Button<'_> { // ui.ctx().set_cursor_icon(cursor); // } // } - - response } } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 7bdb6c86f..b6fb55764 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,7 @@ +use crate::AtomicKind::Custom; use crate::{ - epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo, - WidgetText, WidgetType, + epaint, pos2, vec2, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, TextStyle, Ui, + Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -19,21 +20,21 @@ use crate::{ #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Checkbox<'a> { checked: &'a mut bool, - text: WidgetText, + atomics: Atomics<'a>, indeterminate: bool, } impl<'a> Checkbox<'a> { - pub fn new(checked: &'a mut bool, text: impl Into) -> Self { + pub fn new(checked: &'a mut bool, atomics: impl IntoAtomics<'a>) -> Self { Checkbox { checked, - text: text.into(), + atomics: atomics.into_atomics(), indeterminate: false, } } pub fn without_text(checked: &'a mut bool) -> Self { - Self::new(checked, WidgetText::default()) + Self::new(checked, ()) } /// Display an indeterminate state (neither checked nor unchecked) @@ -51,56 +52,46 @@ impl Widget for Checkbox<'_> { fn ui(self, ui: &mut Ui) -> Response { let Checkbox { checked, - text, + mut atomics, indeterminate, } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); + let rect_id = Id::new("checkbox"); + atomics.add_front(Custom(rect_id, Vec2::splat(icon_width))); - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); + let text = atomics.text(); - let mut desired_size = total_extra + galley.size(); - desired_size = desired_size.at_least(spacing.interact_size); + let mut response = WidgetLayout::new(atomics).sense(Sense::click()).show(ui); - (Some(galley), desired_size) - }; - - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); - - if response.clicked() { + if response.response.clicked() { *checked = !*checked; - response.mark_changed(); + response.response.mark_changed(); } - response.widget_info(|| { + response.response.widget_info(|| { if indeterminate { WidgetInfo::labeled( WidgetType::Checkbox, ui.is_enabled(), - galley.as_ref().map_or("", |x| x.text()), + text.clone().unwrap_or("".to_owned()), ) } else { WidgetInfo::selected( WidgetType::Checkbox, ui.is_enabled(), *checked, - galley.as_ref().map_or("", |x| x.text()), + text.clone().unwrap_or("".to_owned()), ) } }); - if ui.is_rect_visible(rect) { + if ui.is_rect_visible(response.response.rect) { // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = ui.style().interact(&response); + let visuals = ui.style().interact(&response.response); + let rect = response.custom_rects.get(&rect_id).unwrap().clone(); + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); ui.painter().add(epaint::RectShape::new( big_icon_rect.expand(visuals.expansion), @@ -128,15 +119,8 @@ impl Widget for Checkbox<'_> { visuals.fg_stroke, )); } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); - } } - response + response.response } }