1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 05:10:03 -04:00

merge both crates into egui_extras

This commit is contained in:
René Rössler
2022-01-09 21:56:58 +01:00
parent 7dec7054fb
commit bb5da25a9b
17 changed files with 43 additions and 49 deletions

View File

@@ -0,0 +1,34 @@
mod button;
mod popup;
pub use button::DatePickerButton;
use chrono::{Date, Datelike, Duration, NaiveDate, Utc, Weekday};
#[derive(Debug)]
struct Week {
number: u8,
days: Vec<Date<Utc>>,
}
fn month_data(year: i32, month: u32) -> Vec<Week> {
let first = Date::from_utc(NaiveDate::from_ymd(year, month, 1), Utc);
let mut start = first;
while start.weekday() != Weekday::Mon {
start = start.checked_sub_signed(Duration::days(1)).unwrap();
}
let mut weeks = vec![];
let mut week = vec![];
while start < first || start.month() == first.month() || start.weekday() != Weekday::Mon {
week.push(start);
if start.weekday() == Weekday::Sun {
weeks.push(Week {
number: start.iso_week().week() as u8,
days: week.drain(..).collect(),
});
}
start = start.checked_add_signed(Duration::days(1)).unwrap();
}
weeks
}

View File

@@ -0,0 +1,131 @@
use super::popup::DatePickerPopup;
use chrono::{Date, Utc};
use egui::{Area, Button, Frame, Key, Order, RichText, Ui, Widget};
#[derive(Default, Clone)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct DatePickerButtonState {
pub picker_visible: bool,
}
pub struct DatePickerButton<'a> {
selection: &'a mut Date<Utc>,
id_source: Option<&'a str>,
combo_boxes: bool,
arrows: bool,
calendar: bool,
calendar_week: bool,
}
impl<'a> DatePickerButton<'a> {
pub fn new(selection: &'a mut Date<Utc>) -> Self {
Self {
selection,
id_source: None,
combo_boxes: true,
arrows: true,
calendar: true,
calendar_week: true,
}
}
/// Add id source.
/// Must be set if multiple date picker buttons are in the same Ui.
pub fn id_source(mut self, id_source: &'a str) -> Self {
self.id_source = Some(id_source);
self
}
/// Show combo boxes in date picker popup. (Default: true)
pub fn combo_boxes(mut self, combo_boxes: bool) -> Self {
self.combo_boxes = combo_boxes;
self
}
/// Show arrows in date picker popup. (Default: true)
pub fn arrows(mut self, arrows: bool) -> Self {
self.arrows = arrows;
self
}
/// Show calendar in date picker popup. (Default: true)
pub fn calendar(mut self, calendar: bool) -> Self {
self.calendar = calendar;
self
}
/// Show calendar week in date picker popup. (Default: true)
pub fn calendar_week(mut self, week: bool) -> Self {
self.calendar_week = week;
self
}
}
impl<'a> Widget for DatePickerButton<'a> {
fn ui(self, ui: &mut Ui) -> egui::Response {
let id = ui.make_persistent_id(&self.id_source);
let mut button_state = ui
.memory()
.data
.get_persisted::<DatePickerButtonState>(id)
.unwrap_or_default();
//TODO: Internationalization
let mut text = RichText::new(format!("{} 📆", self.selection.format("%d.%m.%Y")));
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.bg_fill).stroke(visuals.bg_stroke);
}
let button_response = ui.add(button);
if button_response.clicked() {
button_state.picker_visible = true;
ui.memory().data.insert_persisted(id, button_state.clone());
}
if button_state.picker_visible {
let width = 333.0;
let mut pos = button_response.rect.left_bottom();
let width_with_padding =
width + ui.style().spacing.item_spacing.x + ui.style().spacing.window_padding.x;
if pos.x + width_with_padding > ui.clip_rect().right() {
pos.x = button_response.rect.right() - width_with_padding;
}
//TODO: Better positioning
let area_response = Area::new(ui.make_persistent_id(&self.id_source))
.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);
DatePickerPopup {
selection: self.selection,
button_id: id,
combo_boxes: self.combo_boxes,
arrows: self.arrows,
calendar: self.calendar,
calendar_week: self.calendar_week,
}
.draw(ui)
})
})
.response;
if !button_response.clicked()
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
{
button_state.picker_visible = false;
ui.memory().data.insert_persisted(id, button_state);
}
}
button_response
}
}

View File

@@ -0,0 +1,360 @@
use super::{button::DatePickerButtonState, month_data};
use crate::{GridBuilder, Padding, Size, TableBuilder};
use chrono::{Date, Datelike, NaiveDate, Utc, Weekday};
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Label, Layout, RichText, Ui};
#[derive(Default, Clone)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
struct DatePickerPopupState {
year: i32,
month: u32,
day: u32,
setup: bool,
}
impl DatePickerPopupState {
fn last_day_of_month(&self) -> u32 {
let date: Date<Utc> = Date::from_utc(NaiveDate::from_ymd(self.year, self.month, 1), Utc);
date.with_day(31)
.map(|_| 31)
.or_else(|| date.with_day(30).map(|_| 30))
.or_else(|| date.with_day(29).map(|_| 29))
.unwrap_or(28)
}
}
pub(crate) struct DatePickerPopup<'a> {
pub selection: &'a mut Date<Utc>,
pub button_id: Id,
pub combo_boxes: bool,
pub arrows: bool,
pub calendar: bool,
pub calendar_week: bool,
}
impl<'a> DatePickerPopup<'a> {
pub fn draw(&mut self, ui: &mut Ui) {
let id = ui.make_persistent_id("date_picker");
let today = chrono::offset::Utc::now().date();
let mut popup_state = ui
.memory()
.data
.get_persisted::<DatePickerPopupState>(id)
.unwrap_or_default();
if !popup_state.setup {
popup_state.year = self.selection.year();
popup_state.month = self.selection.month();
popup_state.day = self.selection.day();
popup_state.setup = true;
ui.memory().data.insert_persisted(id, popup_state.clone());
}
let weeks = month_data(popup_state.year, popup_state.month);
let mut close = false;
let height = 20.0;
GridBuilder::new(ui, Padding::new(2.0, 0.0))
.sizes(
Size::Absolute(height),
match (self.combo_boxes, self.arrows) {
(true, true) => 2,
(true, false) | (false, true) => 1,
(false, false) => 0,
},
)
.sizes(
Size::Absolute(2.0 + (height + 2.0) * weeks.len() as f32),
if self.calendar { 1 } else { 0 },
)
.size(Size::Absolute(height))
.vertical(|mut grid| {
if self.combo_boxes {
grid.grid_noclip(|builder| {
builder.sizes(Size::Remainder, 3).horizontal(|mut grid| {
grid.cell_noclip(|ui| {
ComboBox::from_id_source("date_picker_year")
.selected_text(format!("{}", popup_state.year))
.show_ui(ui, |ui| {
for year in today.year() - 5..today.year() + 10 {
if ui
.selectable_value(
&mut popup_state.year,
year,
format!("{}", year),
)
.changed()
{
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
}
}
});
});
grid.cell_noclip(|ui| {
ComboBox::from_id_source("date_picker_month")
.selected_text(format!("{}", popup_state.month))
.show_ui(ui, |ui| {
for month in 1..=12 {
if ui
.selectable_value(
&mut popup_state.month,
month,
format!("{}", month),
)
.changed()
{
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
}
}
});
});
grid.cell_noclip(|ui| {
ComboBox::from_id_source("date_picker_day")
.selected_text(format!("{}", popup_state.day))
.show_ui(ui, |ui| {
for day in 1..=popup_state.last_day_of_month() {
if ui
.selectable_value(
&mut popup_state.day,
day,
format!("{}", day),
)
.changed()
{
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
}
}
});
});
})
});
}
if self.arrows {
grid.grid(|builder| {
builder.sizes(Size::Remainder, 6).horizontal(|mut grid| {
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui
.button("<<<")
.on_hover_text("substract one year")
.clicked()
{
popup_state.year -= 1;
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui
.button("<<")
.on_hover_text("substract one month")
.clicked()
{
popup_state.month -= 1;
if popup_state.month == 0 {
popup_state.month = 12;
popup_state.year -= 1;
}
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button("<").on_hover_text("substract one day").clicked() {
popup_state.day -= 1;
if popup_state.day == 0 {
popup_state.month -= 1;
if popup_state.month == 0 {
popup_state.year -= 1;
popup_state.month = 12;
}
popup_state.day = popup_state.last_day_of_month();
}
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button(">").on_hover_text("add one day").clicked() {
popup_state.day += 1;
if popup_state.day > popup_state.last_day_of_month() {
popup_state.day = 1;
popup_state.month += 1;
if popup_state.month > 12 {
popup_state.month = 1;
popup_state.year += 1;
}
}
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button(">>").on_hover_text("add one month").clicked() {
popup_state.month += 1;
if popup_state.month > 12 {
popup_state.month = 1;
popup_state.year += 1;
}
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button(">>>").on_hover_text("add one year").clicked() {
popup_state.year += 1;
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
}
});
});
})
});
}
if self.calendar {
grid.cell(|ui| {
TableBuilder::new(ui, Padding::new(2.0, 0.0))
.scroll(false)
.columns(Size::Remainder, if self.calendar_week { 8 } else { 7 })
.header(height, |mut header| {
if self.calendar_week {
header.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::TopDown),
|ui| {
ui.add(Label::new("Week"));
},
);
});
}
//TODO: Locale
for name in ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] {
header.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::TopDown),
|ui| {
ui.add(Label::new(name));
},
);
});
}
})
.body(|mut body| {
for week in weeks {
body.row(height, |mut row| {
if self.calendar_week {
row.col(|ui| {
ui.add(Label::new(format!("{}", week.number)));
});
}
for day in week.days {
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(Align::Center),
|ui| {
//TODO: Colors from egui style
let fill_color = if popup_state.year
== day.year()
&& popup_state.month == day.month()
&& popup_state.day == day.day()
{
Color32::DARK_BLUE
} else if day.weekday() == Weekday::Sat
|| day.weekday() == Weekday::Sun
{
Color32::DARK_RED
} else {
Color32::BLACK
};
let text_color = if day == today {
Color32::RED
} else if day.month() == popup_state.month {
Color32::WHITE
} else {
Color32::from_gray(80)
};
let button = Button::new(
RichText::new(format!("{}", day.day()))
.color(text_color),
)
.fill(fill_color);
if ui.add(button).clicked() {
popup_state.year = day.year();
popup_state.month = day.month();
popup_state.day = day.day();
ui.memory().data.insert_persisted(
id,
popup_state.clone(),
);
}
},
);
});
}
});
}
});
});
}
grid.grid(|builder| {
builder.sizes(Size::Remainder, 3).horizontal(|mut grid| {
grid.empty();
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button("Abbrechen").clicked() {
close = true;
}
});
});
grid.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button("Speichern").clicked() {
*self.selection = Date::from_utc(
NaiveDate::from_ymd(
popup_state.year,
popup_state.month,
popup_state.day,
),
Utc,
);
close = true;
}
});
});
})
});
});
if close {
popup_state.setup = false;
ui.memory().data.insert_persisted(id, popup_state);
ui.memory()
.data
.get_persisted_mut_or_default::<DatePickerButtonState>(self.button_id)
.picker_visible = false;
}
}
}

158
egui_extras/src/grid.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::{
layout::{CellSize, Layout, LineDirection},
sizing::Sizing,
Padding, Size,
};
use egui::Ui;
enum GridDirection {
Horizontal,
Vertical,
}
pub struct GridBuilder<'a> {
ui: &'a mut Ui,
sizing: Sizing,
padding: Padding,
}
impl<'a> GridBuilder<'a> {
/// Create new grid builder
/// After adding size hints with [Self::column]/[Self::columns] the grid can be build with [Self::horizontal]/[Self::vertical]
pub fn new(ui: &'a mut Ui, padding: Padding) -> Self {
let sizing = Sizing::new();
Self {
ui,
sizing,
padding,
}
}
/// Add size hint for column/row
pub fn size(mut self, size: Size) -> Self {
self.sizing.add_size(size);
self
}
/// Add size hint for columns/rows [count] times
pub fn sizes(mut self, size: Size, count: usize) -> Self {
for _ in 0..count {
self.sizing.add_size(size.clone());
}
self
}
/// Build horizontal grid
pub fn horizontal<F>(self, grid: F)
where
F: for<'b> FnOnce(Grid<'a, 'b>),
{
let widths = self.sizing.into_lengths(
self.ui.available_rect_before_wrap().width() - 2.0 * self.padding.outer,
self.padding.inner,
);
let mut layout = Layout::new(self.ui, self.padding.clone(), LineDirection::TopToBottom);
grid(Grid {
layout: &mut layout,
direction: GridDirection::Horizontal,
padding: self.padding.clone(),
widths,
});
}
/// Build vertical grid
pub fn vertical<F>(self, grid: F)
where
F: for<'b> FnOnce(Grid<'a, 'b>),
{
let widths = self.sizing.into_lengths(
self.ui.available_rect_before_wrap().height() - 2.0 * self.padding.outer,
self.padding.inner,
);
let mut layout = Layout::new(self.ui, self.padding.clone(), LineDirection::LeftToRight);
grid(Grid {
layout: &mut layout,
direction: GridDirection::Vertical,
padding: self.padding.clone(),
widths,
});
}
}
pub struct Grid<'a, 'b> {
layout: &'b mut Layout<'a>,
direction: GridDirection,
padding: Padding,
widths: Vec<f32>,
}
impl<'a, 'b> Grid<'a, 'b> {
fn size(&mut self) -> (CellSize, CellSize) {
match self.direction {
GridDirection::Horizontal => (
CellSize::Absolute(self.widths.remove(0)),
CellSize::Remainder,
),
GridDirection::Vertical => (
CellSize::Remainder,
CellSize::Absolute(self.widths.remove(0)),
),
}
}
/// Add empty cell
pub fn empty(&mut self) {
assert!(
!self.widths.is_empty(),
"Tried using more grid cells then available."
);
let (width, height) = self.size();
self.layout.empty(width, height);
}
pub fn _cell(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
assert!(
!self.widths.is_empty(),
"Tried using more grid cells then available."
);
let (width, height) = self.size();
self.layout.add(width, height, clip, add_contents);
}
/// Add cell, content is clipped
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
self._cell(true, add_contents);
}
/// Add cell, content is not clipped
pub fn cell_noclip(&mut self, add_contents: impl FnOnce(&mut Ui)) {
self._cell(false, add_contents);
}
pub fn _grid(&mut self, clip: bool, grid_builder: impl FnOnce(GridBuilder)) {
let padding = self.padding.clone();
self._cell(clip, |ui| {
grid_builder(GridBuilder::new(ui, padding));
});
}
/// Add grid as cell, content is clipped
pub fn grid(&mut self, grid_builder: impl FnOnce(GridBuilder)) {
self._grid(true, grid_builder)
}
/// Add grid as cell, content is not clipped
pub fn grid_noclip(&mut self, grid_builder: impl FnOnce(GridBuilder)) {
self._grid(false, grid_builder)
}
}
impl<'a, 'b> Drop for Grid<'a, 'b> {
fn drop(&mut self) {
while !self.widths.is_empty() {
self.empty();
}
}
}

164
egui_extras/src/layout.rs Normal file
View File

@@ -0,0 +1,164 @@
use crate::Padding;
use egui::{Pos2, Rect, Response, Rgba, Sense, Ui, Vec2};
pub(crate) enum CellSize {
/// Absolute size in points
Absolute(f32),
/// Take all available space
Remainder,
}
pub(crate) enum LineDirection {
/// Cells go from top to bottom
LeftToRight,
/// Cells go from left to right
TopToBottom,
}
/// Positions cells in [LineDirection] and starts a new line on [Layout::end_line]
pub struct Layout<'l> {
ui: &'l mut Ui,
padding: Padding,
direction: LineDirection,
rect: Rect,
pos: Pos2,
max: Pos2,
}
impl<'l> Layout<'l> {
pub(crate) fn new(ui: &'l mut Ui, padding: Padding, direction: LineDirection) -> Self {
let mut rect = ui.available_rect_before_wrap();
rect.set_left(rect.left() + padding.outer + padding.inner);
rect.set_top(rect.top() + padding.outer + padding.inner);
rect.set_width(rect.width() - 2.0 * padding.outer);
rect.set_height(rect.height() - 2.0 * padding.outer);
let pos = rect.left_top();
Self {
ui,
padding,
rect,
pos,
max: pos,
direction,
}
}
pub fn current_y(&self) -> f32 {
self.rect.top()
}
fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
Rect {
min: self.pos,
max: Pos2 {
x: match width {
CellSize::Absolute(width) => self.pos.x + width,
CellSize::Remainder => self.rect.right(),
},
y: match height {
CellSize::Absolute(height) => self.pos.y + height,
CellSize::Remainder => self.rect.bottom(),
},
},
}
}
fn set_pos(&mut self, rect: Rect) {
match self.direction {
LineDirection::LeftToRight => {
self.pos.y = rect.bottom() + self.padding.inner;
}
LineDirection::TopToBottom => {
self.pos.x = rect.right() + self.padding.inner;
}
}
self.max.x = self.max.x.max(rect.right() + self.padding.inner);
self.max.y = self.max.y.max(rect.bottom() + self.padding.inner);
}
pub(crate) fn empty(&mut self, width: CellSize, height: CellSize) {
self.set_pos(self.cell_rect(&width, &height));
}
pub(crate) fn add(
&mut self,
width: CellSize,
height: CellSize,
clip: bool,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let rect = self.cell_rect(&width, &height);
self.cell(rect, clip, add_contents);
self.set_pos(rect);
self.ui.allocate_rect(rect, Sense::click())
}
pub(crate) fn add_striped(
&mut self,
width: CellSize,
height: CellSize,
clip: bool,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let mut rect = self.cell_rect(&width, &height);
*rect.top_mut() -= self.padding.inner;
*rect.left_mut() -= self.padding.inner;
let text_color: Rgba = self.ui.visuals().text_color().into();
self.ui
.painter()
.rect_filled(rect, 0.0, text_color.multiply(0.2));
self.add(width, height, clip, add_contents)
}
/// only needed for layouts with multiple lines, like Table
pub fn end_line(&mut self) {
match self.direction {
LineDirection::LeftToRight => {
self.pos.x = self.max.x;
self.pos.y = self.rect.top();
}
LineDirection::TopToBottom => {
self.pos.y = self.max.y;
self.pos.x = self.rect.left();
}
}
}
/// Set the rect so that the scrollview knows about our size
fn set_rect(&mut self) {
let mut rect = self.rect;
rect.set_right(self.max.x);
rect.set_bottom(self.max.y);
self.ui
.allocate_rect(rect, Sense::focusable_noninteractive());
}
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
let mut child_ui = self.ui.child_ui(rect, *self.ui.layout());
if clip {
let mut clip_rect = child_ui.clip_rect();
clip_rect.min = clip_rect
.min
.max(rect.min - Vec2::new(self.padding.inner, self.padding.inner));
clip_rect.max = clip_rect
.max
.min(rect.max + Vec2::new(self.padding.inner, self.padding.inner));
child_ui.set_clip_rect(clip_rect);
}
add_contents(&mut child_ui)
}
}
impl<'a> Drop for Layout<'a> {
fn drop(&mut self) {
self.set_rect()
}
}

13
egui_extras/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
mod datepicker;
mod grid;
mod layout;
mod padding;
mod sizing;
mod table;
pub use datepicker::DatePickerButton;
pub use grid::*;
pub(crate) use layout::Layout;
pub use padding::Padding;
pub use sizing::Size;
pub use table::*;

View File

@@ -0,0 +1,29 @@
/// Configure padding of grid or table
/// TODO: Use padding settings of egui/should we extend egui padding settings for table?
#[derive(Clone, Debug)]
pub struct Padding {
pub(crate) inner: f32,
pub(crate) outer: f32,
}
impl Padding {
pub fn new(inner: f32, outer: f32) -> Self {
Self { inner, outer }
}
pub fn inner(mut self, inner: f32) -> Self {
self.inner = inner;
self
}
pub fn outer(mut self, outer: f32) -> Self {
self.outer = outer;
self
}
}
impl Default for Padding {
fn default() -> Self {
Self::new(5.0, 10.0)
}
}

85
egui_extras/src/sizing.rs Normal file
View File

@@ -0,0 +1,85 @@
/// Size hint for table column/grid cell
#[derive(Clone, Debug)]
pub enum Size {
/// Absolute size in points
Absolute(f32),
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
Relative(f32),
/// [`Size::Relative`] with a minimum size in points
RelativeMinimum {
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
relative: f32,
/// Absolute minimum size in points
minimum: f32,
},
/// Multiple remainders each get the same space
Remainder,
/// [`Size::Remainder`] with a minimum size in points
RemainderMinimum(f32),
}
pub struct Sizing {
sizes: Vec<Size>,
}
impl Sizing {
pub fn new() -> Self {
Self { sizes: vec![] }
}
pub fn add_size(&mut self, size: Size) {
self.sizes.push(size);
}
pub fn into_lengths(self, length: f32, inner_padding: f32) -> Vec<f32> {
let mut remainders = 0;
let sum_non_remainder = self
.sizes
.iter()
.map(|size| match size {
Size::Absolute(absolute) => *absolute,
Size::Relative(relative) => {
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
length * relative
}
Size::RelativeMinimum { relative, minimum } => {
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
minimum.max(length * relative)
}
Size::Remainder | Size::RemainderMinimum(..) => {
remainders += 1;
0.0
}
})
.sum::<f32>()
+ inner_padding * (self.sizes.len() + 1) as f32;
let avg_remainder_length = if remainders == 0 {
0.0
} else {
let mut remainder_length = length - sum_non_remainder;
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
self.sizes.iter().for_each(|size| {
if let Size::RemainderMinimum(minimum) = size {
if *minimum > avg_remainder_length {
remainder_length -= minimum - avg_remainder_length;
}
}
});
0.0f32.max(remainder_length / remainders as f32)
};
self.sizes
.into_iter()
.map(|size| match size {
Size::Absolute(absolute) => absolute,
Size::Relative(relative) => length * relative,
Size::RelativeMinimum { relative, minimum } => minimum.max(length * relative),
Size::Remainder => avg_remainder_length,
Size::RemainderMinimum(minimum) => minimum.max(avg_remainder_length),
})
.collect()
}
}

280
egui_extras/src/table.rs Normal file
View File

@@ -0,0 +1,280 @@
/// Table view with (optional) fixed header and scrolling body.
/// Cell widths are precalculated with given size hints so we can have tables like this:
/// | fixed size | all available space/minimum | 30% of available width | fixed size |
use crate::{
layout::{CellSize, LineDirection},
sizing::Sizing,
Layout, Padding, Size,
};
use egui::{Response, Ui};
use std::cmp;
pub struct TableBuilder<'a> {
ui: &'a mut Ui,
padding: Padding,
sizing: Sizing,
scroll: bool,
striped: bool,
}
impl<'a> TableBuilder<'a> {
pub fn new(ui: &'a mut Ui, padding: Padding) -> Self {
let sizing = Sizing::new();
Self {
ui,
padding,
sizing,
scroll: true,
striped: false,
}
}
/// Enable scrollview in body (default: true)
pub fn scroll(mut self, scroll: bool) -> Self {
self.scroll = scroll;
self
}
/// Enable striped row background (default: false)
pub fn striped(mut self, striped: bool) -> Self {
self.striped = striped;
self
}
/// Add size hint for column
pub fn column(mut self, width: Size) -> Self {
self.sizing.add_size(width);
self
}
/// Add size hint for column [count] times
pub fn columns(mut self, size: Size, count: usize) -> Self {
for _ in 0..count {
self.sizing.add_size(size.clone());
}
self
}
/// Create a header row which always stays visible and at the top
pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
let widths = self.sizing.into_lengths(
self.ui.available_rect_before_wrap().width() - 2.0 * self.padding.outer,
self.padding.inner,
);
let ui = self.ui;
{
let mut layout = Layout::new(ui, self.padding.clone(), LineDirection::TopToBottom);
{
let row = TableRow {
layout: &mut layout,
widths: widths.clone(),
striped: false,
height,
clicked: false,
};
header(row);
}
}
Table {
ui,
padding: self.padding,
widths,
scroll: self.scroll,
striped: self.striped,
}
}
/// Create table body without a header row
pub fn body<F>(self, body: F)
where
F: for<'b> FnOnce(TableBody<'b>),
{
let widths = self.sizing.into_lengths(
self.ui.available_rect_before_wrap().width() - 2.0 * self.padding.outer,
self.padding.inner,
);
Table {
ui: self.ui,
padding: self.padding,
widths,
scroll: self.scroll,
striped: self.striped,
}
.body(body)
}
}
pub struct Table<'a> {
ui: &'a mut Ui,
padding: Padding,
widths: Vec<f32>,
scroll: bool,
striped: bool,
}
impl<'a> Table<'a> {
/// Create table body after adding a header row
pub fn body<F>(self, body: F)
where
F: for<'b> FnOnce(TableBody<'b>),
{
let padding = self.padding;
let ui = self.ui;
let widths = self.widths;
let striped = self.striped;
let start_y = ui.available_rect_before_wrap().top();
let end_y = ui.available_rect_before_wrap().bottom();
egui::ScrollArea::new([false, self.scroll]).show(ui, move |ui| {
let layout = Layout::new(ui, padding, LineDirection::TopToBottom);
body(TableBody {
layout,
widths,
striped,
odd: true,
start_y,
end_y,
});
});
}
}
pub struct TableBody<'a> {
layout: Layout<'a>,
widths: Vec<f32>,
striped: bool,
odd: bool,
start_y: f32,
end_y: f32,
}
impl<'a> TableBody<'a> {
/// Add rows with same height
/// Is a lot more performant than adding each individual row as non visible rows must not be rendered
pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow)) {
let delta = self.layout.current_y() - self.start_y;
let mut start = 0;
if delta < 0.0 {
start = (-delta / height).floor() as usize;
let skip_height = start as f32 * height;
TableRow {
layout: &mut self.layout,
widths: self.widths.clone(),
striped: self.striped && self.odd,
height: skip_height,
clicked: false,
}
.col(|_| ()); // advances the cursor
}
let max_height = self.end_y - self.start_y;
let count = (max_height / height).ceil() as usize;
let end = cmp::min(start + count, rows);
if start % 2 != 0 {
self.odd = false;
}
for idx in start..end {
row(
idx,
TableRow {
layout: &mut self.layout,
widths: self.widths.clone(),
striped: self.striped && self.odd,
height,
clicked: false,
},
);
self.odd = !self.odd;
}
if rows - end > 0 {
let skip_height = (rows - end) as f32 * height;
TableRow {
layout: &mut self.layout,
widths: self.widths.clone(),
striped: self.striped && self.odd,
height: skip_height,
clicked: false,
}
.col(|_| ()); // advances the cursor
}
}
/// Add row with individual height
pub fn row(&mut self, height: f32, row: impl FnOnce(TableRow<'a, '_>)) {
row(TableRow {
layout: &mut self.layout,
widths: self.widths.clone(),
striped: self.striped && self.odd,
height,
clicked: false,
});
self.odd = !self.odd;
}
}
pub struct TableRow<'a, 'b> {
layout: &'b mut Layout<'a>,
widths: Vec<f32>,
striped: bool,
height: f32,
clicked: bool,
}
impl<'a, 'b> TableRow<'a, 'b> {
/// Check if row was clicked
pub fn clicked(&self) -> bool {
self.clicked
}
fn _col(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) -> Response {
assert!(
!self.widths.is_empty(),
"Tried using more table columns then available."
);
let width = CellSize::Absolute(self.widths.remove(0));
let height = CellSize::Absolute(self.height);
let response;
if self.striped {
response = self.layout.add_striped(width, height, clip, add_contents);
} else {
response = self.layout.add(width, height, clip, add_contents);
}
if response.clicked() {
self.clicked = true;
}
response
}
/// Add column, content is clipped
pub fn col(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
self._col(true, add_contents)
}
/// Add column, content is not clipped
pub fn col_noclip(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
self._col(false, add_contents)
}
}
impl<'a, 'b> Drop for TableRow<'a, 'b> {
fn drop(&mut self) {
self.layout.end_line();
}
}