From 4dbf20d1070cfb669e77ccd62a219cb4c1802e6b Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 12 Mar 2025 11:50:18 +0100 Subject: [PATCH] Finish WidgetLayout prototype --- Cargo.lock | 2 + crates/egui/src/lib.rs | 1 + crates/egui/src/widget_layout.rs | 169 +++++++++++++++++++++--- examples/hello_world_simple/Cargo.toml | 2 + examples/hello_world_simple/src/main.rs | 33 +++++ 5 files changed, 190 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2d246045..0f917ebc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2064,7 +2064,9 @@ name = "hello_world_simple" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "env_logger", + "image", ] [[package]] diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 3aff7bc06..512caac99 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -508,6 +508,7 @@ pub use self::{ ui_builder::UiBuilder, ui_stack::*, viewport::*, + widget_layout::*, widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, diff --git a/crates/egui/src/widget_layout.rs b/crates/egui/src/widget_layout.rs index 59a76fa1a..580691ad7 100644 --- a/crates/egui/src/widget_layout.rs +++ b/crates/egui/src/widget_layout.rs @@ -1,23 +1,47 @@ -use crate::{Frame, ImageSource, Response, Sense, Ui, WidgetText}; -use emath::Vec2; +use crate::{Frame, Image, ImageSource, Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use emath::{Align2, Vec2}; use epaint::Galley; +use std::sync::Arc; -enum WidgetLayoutItem<'a> { +enum WidgetLayoutItemType<'a> { Text(WidgetText), - Image(ImageSource<'a>), + Image(Image<'a>), Custom(Vec2), Grow, } -enum SizedWidgetLayoutItem<'a> { - Text(Galley), - Image(ImageSource<'a>, Vec2), +enum SizedWidgetLayoutItemType<'a> { + Text(Arc), + Image(Image<'a>, Vec2), Custom(Vec2), Grow, } +struct Item { + align2: Align2, +} + +impl Default for Item { + fn default() -> Self { + Self { + align2: Align2::LEFT_CENTER, + } + } +} + +impl SizedWidgetLayoutItemType<'_> { + pub fn size(&self) -> Vec2 { + match self { + SizedWidgetLayoutItemType::Text(galley) => galley.size(), + SizedWidgetLayoutItemType::Image(_, size) => *size, + SizedWidgetLayoutItemType::Custom(size) => *size, + SizedWidgetLayoutItemType::Grow => Vec2::ZERO, + } + } +} + struct WidgetLayout<'a> { - items: Vec>, + items: Vec<(Item, WidgetLayoutItemType<'a>)>, gap: f32, frame: Frame, sense: Sense, @@ -27,14 +51,14 @@ impl<'a> WidgetLayout<'a> { pub fn new() -> Self { Self { items: Vec::new(), - gap: 0.0, + gap: 4.0, frame: Frame::default(), sense: Sense::hover(), } } - pub fn add(mut self, item: impl Into>) -> Self { - self.items.push(item.into()); + pub fn add(mut self, item: Item, kind: impl Into>) -> Self { + self.items.push((item, kind.into())); self } @@ -54,33 +78,143 @@ impl<'a> WidgetLayout<'a> { } pub fn show(self, ui: &mut Ui) -> Response { - let available_width = ui.available_width(); + let available_size = ui.available_size(); + let available_width = available_size.x; let mut desired_width = 0.0; let mut preferred_width = 0.0; - let mut height = 0.0; + let mut height: f32 = 0.0; let mut sized_items = Vec::new(); - let (rect, response) = ui.allocate_at_least(Vec2::new(desired_width, height), self.sense); + let mut grow_count = 0; + + for (item, kind) in self.items { + let (preferred_size, sized) = match kind { + WidgetLayoutItemType::Text(text) => { + let galley = text.into_galley(ui, None, available_width, TextStyle::Button); + ( + galley.size(), // TODO + SizedWidgetLayoutItemType::Text(galley), + ) + } + WidgetLayoutItemType::Image(image) => { + let size = + image.load_and_calc_size(ui, Vec2::min(available_size, Vec2::splat(16.0))); + let size = size.unwrap_or_default(); + (size, SizedWidgetLayoutItemType::Image(image, size)) + } + WidgetLayoutItemType::Custom(size) => { + (size, SizedWidgetLayoutItemType::Custom(size)) + } + WidgetLayoutItemType::Grow => { + grow_count += 1; + (Vec2::ZERO, SizedWidgetLayoutItemType::Grow) + } + }; + let size = sized.size(); + + desired_width += size.x; + preferred_width += preferred_size.x; + + height = height.max(size.y); + + sized_items.push((item, sized)); + } + + if sized_items.len() > 1 { + let gap_space = self.gap * (sized_items.len() as f32 - 1.0); + desired_width += gap_space; + preferred_width += gap_space; + } + + let margin = self.frame.total_margin(); + let content_size = Vec2::new(desired_width, height); + let frame_size = content_size + margin.sum(); + + let (rect, response) = ui.allocate_at_least(frame_size, self.sense); + + let content_rect = rect - margin; + ui.painter().add(self.frame.paint(content_rect)); + + let width_to_fill = content_rect.width(); + let extra_space = f32::max(width_to_fill - desired_width, 0.0); + let grow_width = f32::max(extra_space / grow_count as f32, 0.0); + + let mut cursor = content_rect.left(); + + for (item, sized) in sized_items { + let size = sized.size(); + let width = match sized { + SizedWidgetLayoutItemType::Grow => grow_width, + _ => size.x, + }; + + let frame = content_rect.with_min_x(cursor).with_max_x(cursor + width); + cursor = frame.right() + self.gap; + + let rect = item.align2.align_size_within_rect(size, frame); + + match sized { + SizedWidgetLayoutItemType::Text(galley) => { + ui.painter() + .galley(rect.min, galley, ui.visuals().text_color()); + } + SizedWidgetLayoutItemType::Image(image, _) => { + image.paint_at(ui, rect); + } + SizedWidgetLayoutItemType::Custom(_) => {} + SizedWidgetLayoutItemType::Grow => {} + } + } response } } -struct WLButton<'a> { +pub struct WLButton<'a> { wl: WidgetLayout<'a>, } impl<'a> WLButton<'a> { pub fn new(text: impl Into) -> Self { Self { - wl: WidgetLayout::new().add(text), + wl: WidgetLayout::new() + .sense(Sense::click()) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())), } } - pub fn ui(mut self, ui: &mut Ui) -> Response { + pub fn image(image: impl Into>) -> Self { + Self { + wl: WidgetLayout::new().sense(Sense::click()).add( + Item::default(), + WidgetLayoutItemType::Image(image.into().max_size(Vec2::splat(16.0))), + ), + } + } + + pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { + Self { + wl: WidgetLayout::new() + .sense(Sense::click()) + .add(Item::default(), WidgetLayoutItemType::Image(image.into())) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())), + } + } + + pub fn right_text(mut self, text: impl Into) -> Self { + self.wl = self + .wl + .add(Item::default(), WidgetLayoutItemType::Grow) + .add(Item::default(), WidgetLayoutItemType::Text(text.into())); + self + } +} + +impl<'a> Widget for WLButton<'a> { + fn ui(mut self, ui: &mut Ui) -> Response { let response = ui.ctx().read_response(ui.next_auto_id()); let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { @@ -90,6 +224,7 @@ impl<'a> WLButton<'a> { self.wl.frame = self .wl .frame + .inner_margin(ui.style().spacing.button_padding) .fill(visuals.bg_fill) .stroke(visuals.bg_stroke) .corner_radius(visuals.corner_radius); diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index db1d7906d..e8d08456b 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -20,3 +20,5 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } +egui_extras = {workspace = true, features = ["image", "all_loaders"]} +image = {workspace = true, features = ["png"]} diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 4fe49a89d..72174f051 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,6 +2,10 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; +use eframe::egui::{ + include_image, Image, Key, KeyboardShortcut, ModifierNames, Modifiers, Popup, RichText, + WLButton, Widget, +}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -17,6 +21,7 @@ fn main() -> eframe::Result { eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { + egui_extras::install_image_loaders(ctx); ui.heading("My egui Application"); ui.horizontal(|ui| { let name_label = ui.label("Your name: "); @@ -28,6 +33,34 @@ fn main() -> eframe::Result { age += 1; } ui.label(format!("Hello '{name}', age {age}")); + + if WLButton::new("WL Button").ui(ui).clicked() { + age += 1; + }; + + let source = include_image!("../../../crates/eframe/data/icon.png"); + let response = WLButton::image_and_text(source, "Hello World").ui(ui); + + Popup::menu(&response).show(|ui| { + WLButton::new("Print") + .right_text( + RichText::new( + KeyboardShortcut::new(Modifiers::COMMAND, Key::P) + .format(&ModifierNames::SYMBOLS, true), + ) + .weak(), + ) + .ui(ui); + WLButton::new("A very long button") + .right_text( + RichText::new( + KeyboardShortcut::new(Modifiers::COMMAND, Key::O) + .format(&ModifierNames::SYMBOLS, true), + ) + .weak(), + ) + .ui(ui); + }); }); }) }