1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

Button improvements, implement checkbox

This commit is contained in:
lucasmerlin
2025-04-09 10:28:49 +02:00
parent 471a91966b
commit 026e47d939
3 changed files with 128 additions and 81 deletions

View File

@@ -1,12 +1,13 @@
use crate::{Frame, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; use crate::{Frame, Id, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText};
use emath::{Align2, Vec2}; use ahash::HashMap;
use epaint::Galley; use emath::{Align2, Rect, Vec2};
use epaint::{Color32, Galley};
use std::sync::Arc; use std::sync::Arc;
pub enum SizedAtomicKind<'a> { pub enum SizedAtomicKind<'a> {
Text(Arc<Galley>), Text(Arc<Galley>),
Image(Image<'a>, Vec2), Image(Image<'a>, Vec2),
Custom(Vec2), Custom(Id, Vec2),
Grow, Grow,
} }
@@ -15,7 +16,7 @@ impl SizedAtomicKind<'_> {
match self { match self {
SizedAtomicKind::Text(galley) => galley.size(), SizedAtomicKind::Text(galley) => galley.size(),
SizedAtomicKind::Image(_, size) => *size, SizedAtomicKind::Image(_, size) => *size,
SizedAtomicKind::Custom(size) => *size, SizedAtomicKind::Custom(_, size) => *size,
SizedAtomicKind::Grow => Vec2::ZERO, SizedAtomicKind::Grow => Vec2::ZERO,
} }
} }
@@ -24,18 +25,20 @@ impl SizedAtomicKind<'_> {
/// AtomicLayout /// AtomicLayout
pub struct WidgetLayout<'a> { pub struct WidgetLayout<'a> {
pub atomics: Atomics<'a>, pub atomics: Atomics<'a>,
gap: f32, gap: Option<f32>,
pub(crate) frame: Frame, pub(crate) frame: Frame,
pub(crate) sense: Sense, pub(crate) sense: Sense,
fallback_text_color: Option<Color32>,
} }
impl<'a> WidgetLayout<'a> { impl<'a> WidgetLayout<'a> {
pub fn new(atomics: impl IntoAtomics<'a>) -> Self { pub fn new(atomics: impl IntoAtomics<'a>) -> Self {
Self { Self {
atomics: atomics.into_atomics(), atomics: atomics.into_atomics(),
gap: 4.0, gap: None,
frame: Frame::default(), frame: Frame::default(),
sense: Sense::hover(), sense: Sense::hover(),
fallback_text_color: None,
} }
} }
@@ -44,8 +47,9 @@ impl<'a> WidgetLayout<'a> {
self self
} }
/// Default: `Spacing::icon_spacing`
pub fn gap(mut self, gap: f32) -> Self { pub fn gap(mut self, gap: f32) -> Self {
self.gap = gap; self.gap = Some(gap);
self self
} }
@@ -59,7 +63,17 @@ impl<'a> WidgetLayout<'a> {
self 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_size = ui.available_size();
let available_width = available_size.x; let available_width = available_size.x;
@@ -87,7 +101,7 @@ impl<'a> WidgetLayout<'a> {
let size = size.unwrap_or_default(); let size = size.unwrap_or_default();
(size, SizedAtomicKind::Image(image, size)) (size, SizedAtomicKind::Image(image, size))
} }
AtomicKind::Custom(size) => (size, SizedAtomicKind::Custom(size)), AtomicKind::Custom(id, size) => (size, SizedAtomicKind::Custom(id, size)),
AtomicKind::Grow => { AtomicKind::Grow => {
grow_count += 1; grow_count += 1;
(Vec2::ZERO, SizedAtomicKind::Grow) (Vec2::ZERO, SizedAtomicKind::Grow)
@@ -104,7 +118,7 @@ impl<'a> WidgetLayout<'a> {
} }
if sized_items.len() > 1 { 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; desired_width += gap_space;
preferred_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 (rect, response) = ui.allocate_at_least(frame_size, self.sense);
let mut response = AtomicLayoutResponse {
response,
custom_rects: HashMap::default(),
};
let content_rect = rect - margin; let content_rect = rect - margin;
ui.painter().add(self.frame.paint(content_rect)); 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); 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 align = Align2::CENTER_CENTER;
let rect = align.align_size_within_rect(size, frame); let rect = align.align_size_within_rect(size, frame);
match sized { match sized {
SizedAtomicKind::Text(galley) => { SizedAtomicKind::Text(galley) => {
ui.painter() ui.painter().galley(rect.min, galley, fallback_text_color);
.galley(rect.min, galley, ui.visuals().text_color());
} }
SizedAtomicKind::Image(image, _) => { SizedAtomicKind::Image(image, _) => {
image.paint_at(ui, rect); image.paint_at(ui, rect);
} }
SizedAtomicKind::Custom(_) => {} SizedAtomicKind::Custom(id, size) => {
response.custom_rects.insert(id, rect);
}
SizedAtomicKind::Grow => {} SizedAtomicKind::Grow => {}
} }
} }
@@ -154,6 +174,11 @@ impl<'a> WidgetLayout<'a> {
} }
} }
pub struct AtomicLayoutResponse {
pub response: Response,
pub custom_rects: HashMap<Id, Rect>,
}
// pub struct WLButton<'a> { // pub struct WLButton<'a> {
// wl: WidgetLayout<'a>, // wl: WidgetLayout<'a>,
// } // }
@@ -217,7 +242,7 @@ impl<'a> WidgetLayout<'a> {
pub enum AtomicKind<'a> { pub enum AtomicKind<'a> {
Text(WidgetText), Text(WidgetText),
Image(Image<'a>), Image(Image<'a>),
Custom(Vec2), Custom(Id, Vec2),
Grow, Grow,
} }
@@ -247,7 +272,7 @@ impl Atomic<'_> {
// } // }
} }
trait AtomicExt<'a> { pub trait AtomicExt<'a> {
fn a_size(self, size: Vec2) -> Atomic<'a>; fn a_size(self, size: Vec2) -> Atomic<'a>;
fn a_grow(self, grow: bool) -> 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<Item = &mut Atomic<'a>> { pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Atomic<'a>> {
self.0.iter_mut() self.0.iter_mut()
} }
pub fn text(&self) -> Option<String> {
let mut string: Option<String> = 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 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);
all_the_atomics!(T0, T1, T2, T3, T4); all_the_atomics!(T0, T1, T2, T3, T4);
all_the_atomics!(T0, T1, T2, T3, T4, T5); 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<T> 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
}
}

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
widgets, Align, Atomic, AtomicKind, Color32, CornerRadius, Frame, Image, IntoAtomics, NumExt, widgets, Align, Atomic, AtomicExt, AtomicKind, AtomicLayoutResponse, Color32, CornerRadius,
Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, Frame, Image, IntoAtomics, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui,
WidgetLayout, WidgetText, WidgetType, Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType,
}; };
/// Clickable button with text. /// Clickable button with text.
@@ -184,14 +184,20 @@ impl<'a> Button<'a> {
/// See also [`Self::right_text`]. /// See also [`Self::right_text`].
#[inline] #[inline]
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atomic<'a>>) -> Self { pub fn shortcut_text(mut self, shortcut_text: impl Into<Atomic<'a>>) -> Self {
self.wl = self.wl.add(shortcut_text); self.wl = self
.wl
.add(AtomicKind::Grow.a_grow(true))
.add(shortcut_text);
self self
} }
/// Show some text on the right side of the button. /// Show some text on the right side of the button.
#[inline] #[inline]
pub fn right_text(mut self, right_text: impl Into<Atomic<'a>>) -> Self { pub fn right_text(mut self, right_text: impl Into<Atomic<'a>>) -> Self {
self.wl = self.wl.add(right_text.into()); self.wl = self
.wl
.add(AtomicKind::Grow.a_grow(true))
.add(right_text.into());
self self
} }
@@ -201,10 +207,8 @@ impl<'a> Button<'a> {
self.selected = selected; self.selected = selected;
self self
} }
}
impl Widget for Button<'_> { pub fn atomic_ui(mut self, ui: &mut Ui) -> AtomicLayoutResponse {
fn ui(mut self, ui: &mut Ui) -> Response {
let Button { let Button {
wrap_mode, wrap_mode,
fill, fill,
@@ -241,26 +245,37 @@ impl Widget for Button<'_> {
ui.style().interact(&response) ui.style().interact(&response)
}); });
wl = wl.fallback_text_color(visuals.text_color());
wl.frame = if has_frame { wl.frame = if has_frame {
wl.frame wl.frame
.inner_margin(button_padding) .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)) .stroke(stroke.unwrap_or(visuals.bg_stroke))
.corner_radius(corner_radius.unwrap_or(visuals.corner_radius)) .corner_radius(corner_radius.unwrap_or(visuals.corner_radius))
} else { } else {
Frame::new() Frame::new()
}; };
let text = wl.atomics.text();
let response = wl.show(ui); let response = wl.show(ui);
// TODO: How to get text? response.response.widget_info(|| {
// response.widget_info(|| { if let Some(text) = &text {
// if let Some(galley) = &galley { WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
// WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) } else {
// } else { WidgetInfo::new(WidgetType::Button)
// 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 { // 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); // desired_size = desired_size.at_least(min_size);
// //
// let (rect, mut response) = ui.allocate_at_least(desired_size, sense); // 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) { // if ui.is_rect_visible(rect) {
// let visuals = ui.style().interact(&response); // let visuals = ui.style().interact(&response);
@@ -435,7 +443,5 @@ impl Widget for Button<'_> {
// ui.ctx().set_cursor_icon(cursor); // ui.ctx().set_cursor_icon(cursor);
// } // }
// } // }
response
} }
} }

View File

@@ -1,6 +1,7 @@
use crate::AtomicKind::Custom;
use crate::{ use crate::{
epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo, epaint, pos2, vec2, Atomics, Id, IntoAtomics, NumExt, Response, Sense, Shape, TextStyle, Ui,
WidgetText, WidgetType, Vec2, Widget, WidgetInfo, WidgetLayout, WidgetText, WidgetType,
}; };
// TODO(emilk): allow checkbox without a text label // 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);`"] #[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct Checkbox<'a> { pub struct Checkbox<'a> {
checked: &'a mut bool, checked: &'a mut bool,
text: WidgetText, atomics: Atomics<'a>,
indeterminate: bool, indeterminate: bool,
} }
impl<'a> Checkbox<'a> { impl<'a> Checkbox<'a> {
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self { pub fn new(checked: &'a mut bool, atomics: impl IntoAtomics<'a>) -> Self {
Checkbox { Checkbox {
checked, checked,
text: text.into(), atomics: atomics.into_atomics(),
indeterminate: false, indeterminate: false,
} }
} }
pub fn without_text(checked: &'a mut bool) -> Self { 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) /// Display an indeterminate state (neither checked nor unchecked)
@@ -51,56 +52,46 @@ impl Widget for Checkbox<'_> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Checkbox { let Checkbox {
checked, checked,
text, mut atomics,
indeterminate, indeterminate,
} = self; } = self;
let spacing = &ui.spacing(); let spacing = &ui.spacing();
let icon_width = spacing.icon_width; let icon_width = spacing.icon_width;
let icon_spacing = spacing.icon_spacing;
let (galley, mut desired_size) = if text.is_empty() { let rect_id = Id::new("checkbox");
(None, vec2(icon_width, 0.0)) atomics.add_front(Custom(rect_id, Vec2::splat(icon_width)));
} else {
let total_extra = vec2(icon_width + icon_spacing, 0.0);
let wrap_width = ui.available_width() - total_extra.x; let text = atomics.text();
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
let mut desired_size = total_extra + galley.size(); let mut response = WidgetLayout::new(atomics).sense(Sense::click()).show(ui);
desired_size = desired_size.at_least(spacing.interact_size);
(Some(galley), desired_size) if response.response.clicked() {
};
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() {
*checked = !*checked; *checked = !*checked;
response.mark_changed(); response.response.mark_changed();
} }
response.widget_info(|| { response.response.widget_info(|| {
if indeterminate { if indeterminate {
WidgetInfo::labeled( WidgetInfo::labeled(
WidgetType::Checkbox, WidgetType::Checkbox,
ui.is_enabled(), ui.is_enabled(),
galley.as_ref().map_or("", |x| x.text()), text.clone().unwrap_or("".to_owned()),
) )
} else { } else {
WidgetInfo::selected( WidgetInfo::selected(
WidgetType::Checkbox, WidgetType::Checkbox,
ui.is_enabled(), ui.is_enabled(),
*checked, *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_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); let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape::new( ui.painter().add(epaint::RectShape::new(
big_icon_rect.expand(visuals.expansion), big_icon_rect.expand(visuals.expansion),
@@ -128,15 +119,8 @@ impl Widget for Checkbox<'_> {
visuals.fg_stroke, 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
} }
} }