mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 13:50:04 -04:00
Move all crates into a crates directory (#1940)
This commit is contained in:
136
crates/egui_extras/src/datepicker/button.rs
Normal file
136
crates/egui_extras/src/datepicker/button.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use super::popup::DatePickerPopup;
|
||||
use chrono::{Date, Utc};
|
||||
use egui::{Area, Button, Frame, Key, Order, RichText, Ui, Widget};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", 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();
|
||||
|
||||
let mut text = RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d")));
|
||||
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_margin.left
|
||||
+ ui.style().spacing.window_margin.right;
|
||||
if pos.x + width_with_padding > ui.clip_rect().right() {
|
||||
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.left);
|
||||
|
||||
//TODO(elwerene): 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
|
||||
}
|
||||
}
|
||||
34
crates/egui_extras/src/datepicker/mod.rs
Normal file
34
crates/egui_extras/src/datepicker/mod.rs
Normal 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
|
||||
}
|
||||
414
crates/egui_extras/src/datepicker/popup.rs
Normal file
414
crates/egui_extras/src/datepicker/popup.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use super::{button::DatePickerButtonState, month_data};
|
||||
use crate::{Size, StripBuilder, TableBuilder};
|
||||
use chrono::{Date, Datelike, NaiveDate, Utc, Weekday};
|
||||
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", 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;
|
||||
let spacing = 2.0;
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
||||
StripBuilder::new(ui)
|
||||
.clip(false)
|
||||
.sizes(
|
||||
Size::exact(height),
|
||||
match (self.combo_boxes, self.arrows) {
|
||||
(true, true) => 2,
|
||||
(true, false) | (false, true) => 1,
|
||||
(false, false) => 0,
|
||||
},
|
||||
)
|
||||
.sizes(
|
||||
Size::exact((spacing + height) * (weeks.len() + 1) as f32),
|
||||
if self.calendar { 1 } else { 0 },
|
||||
)
|
||||
.size(Size::exact(height))
|
||||
.vertical(|mut strip| {
|
||||
if self.combo_boxes {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ComboBox::from_id_source("date_picker_year")
|
||||
.selected_text(popup_state.year.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for year in today.year() - 5..today.year() + 10 {
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut popup_state.year,
|
||||
year,
|
||||
year.to_string(),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
popup_state.day = popup_state
|
||||
.day
|
||||
.min(popup_state.last_day_of_month());
|
||||
ui.memory()
|
||||
.data
|
||||
.insert_persisted(id, popup_state.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ComboBox::from_id_source("date_picker_month")
|
||||
.selected_text(month_name(popup_state.month))
|
||||
.show_ui(ui, |ui| {
|
||||
for month in 1..=12 {
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut popup_state.month,
|
||||
month,
|
||||
month_name(month),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
popup_state.day = popup_state
|
||||
.day
|
||||
.min(popup_state.last_day_of_month());
|
||||
ui.memory()
|
||||
.data
|
||||
.insert_persisted(id, popup_state.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ComboBox::from_id_source("date_picker_day")
|
||||
.selected_text(popup_state.day.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for day in 1..=popup_state.last_day_of_month() {
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut popup_state.day,
|
||||
day,
|
||||
day.to_string(),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
ui.memory()
|
||||
.data
|
||||
.insert_persisted(id, popup_state.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if self.arrows {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 6).horizontal(|mut strip| {
|
||||
strip.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.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 {
|
||||
strip.cell(|ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0);
|
||||
TableBuilder::new(ui)
|
||||
.scroll(false)
|
||||
.clip(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.label("Week");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//TODO(elwerene): Locale
|
||||
for name in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
|
||||
header.col(|ui| {
|
||||
ui.with_layout(
|
||||
Layout::centered_and_justified(Direction::TopDown),
|
||||
|ui| {
|
||||
ui.label(name);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.body(|mut body| {
|
||||
for week in weeks {
|
||||
body.row(height, |mut row| {
|
||||
if self.calendar_week {
|
||||
row.col(|ui| {
|
||||
ui.label(week.number.to_string());
|
||||
});
|
||||
}
|
||||
for day in week.days {
|
||||
row.col(|ui| {
|
||||
ui.with_layout(
|
||||
Layout::top_down_justified(Align::Center),
|
||||
|ui| {
|
||||
let fill_color = if popup_state.year
|
||||
== day.year()
|
||||
&& popup_state.month == day.month()
|
||||
&& popup_state.day == day.day()
|
||||
{
|
||||
ui.visuals().selection.bg_fill
|
||||
} else if day.weekday() == Weekday::Sat
|
||||
|| day.weekday() == Weekday::Sun
|
||||
{
|
||||
if ui.visuals().dark_mode {
|
||||
Color32::DARK_RED
|
||||
} else {
|
||||
Color32::LIGHT_RED
|
||||
}
|
||||
} else {
|
||||
ui.visuals().extreme_bg_color
|
||||
};
|
||||
|
||||
let mut text_color = ui
|
||||
.visuals()
|
||||
.widgets
|
||||
.inactive
|
||||
.text_color();
|
||||
|
||||
if day.month() != popup_state.month {
|
||||
text_color =
|
||||
text_color.linear_multiply(0.5);
|
||||
};
|
||||
|
||||
let button_response = ui.add(
|
||||
Button::new(
|
||||
RichText::new(
|
||||
day.day().to_string(),
|
||||
)
|
||||
.color(text_color),
|
||||
)
|
||||
.fill(fill_color),
|
||||
);
|
||||
|
||||
if day == today {
|
||||
// Encircle today's date
|
||||
let stroke = ui
|
||||
.visuals()
|
||||
.widgets
|
||||
.inactive
|
||||
.fg_stroke;
|
||||
ui.painter().circle_stroke(
|
||||
button_response.rect.center(),
|
||||
8.0,
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
|
||||
if button_response.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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
||||
if ui.button("Cancel").clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
||||
if ui.button("Save").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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn month_name(i: u32) -> &'static str {
|
||||
match i {
|
||||
1 => "January",
|
||||
2 => "February",
|
||||
3 => "March",
|
||||
4 => "April",
|
||||
5 => "May",
|
||||
6 => "June",
|
||||
7 => "July",
|
||||
8 => "August",
|
||||
9 => "September",
|
||||
10 => "October",
|
||||
11 => "November",
|
||||
12 => "December",
|
||||
_ => panic!("Unknown month: {}", i),
|
||||
}
|
||||
}
|
||||
219
crates/egui_extras/src/image.rs
Normal file
219
crates/egui_extras/src/image.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use egui::mutex::Mutex;
|
||||
use egui::TextureFilter;
|
||||
|
||||
/// An image to be shown in egui.
|
||||
///
|
||||
/// Load once, and save somewhere in your app state.
|
||||
///
|
||||
/// Use the `svg` and `image` features to enable more constructors.
|
||||
pub struct RetainedImage {
|
||||
debug_name: String,
|
||||
size: [usize; 2],
|
||||
/// Cleared once [`Self::texture`] has been loaded.
|
||||
image: Mutex<egui::ColorImage>,
|
||||
/// Lazily loaded when we have an egui context.
|
||||
texture: Mutex<Option<egui::TextureHandle>>,
|
||||
filter: TextureFilter,
|
||||
}
|
||||
|
||||
impl RetainedImage {
|
||||
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
|
||||
Self {
|
||||
debug_name: debug_name.into(),
|
||||
size: image.size,
|
||||
image: Mutex::new(image),
|
||||
texture: Default::default(),
|
||||
filter: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
///
|
||||
/// Requires the "image" feature. You must also opt-in to the image formats you need
|
||||
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image or unsupported image format.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn from_image_bytes(
|
||||
debug_name: impl Into<String>,
|
||||
image_bytes: &[u8],
|
||||
) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_image_bytes(image_bytes)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Pass in the bytes of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_bytes(debug_name: impl Into<String>, svg_bytes: &[u8]) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_svg_bytes(svg_bytes)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Pass in the str of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_str(debug_name: impl Into<String>, svg_str: &str) -> Result<Self, String> {
|
||||
Self::from_svg_bytes(debug_name, svg_str.as_bytes())
|
||||
}
|
||||
|
||||
/// Set the texture filter to use for the image.
|
||||
///
|
||||
/// **Note:** If the texture has already been uploaded to the GPU, this will require
|
||||
/// re-uploading the texture with the updated filter.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui_extras::RetainedImage;
|
||||
/// # use egui::{Color32, epaint::{ColorImage, textures::TextureFilter}};
|
||||
/// # let pixels = vec![Color32::BLACK];
|
||||
/// # let color_image = ColorImage {
|
||||
/// # size: [1, 1],
|
||||
/// # pixels,
|
||||
/// # };
|
||||
/// #
|
||||
/// // Upload a pixel art image without it getting blurry when resized
|
||||
/// let image = RetainedImage::from_color_image("my_image", color_image)
|
||||
/// .with_texture_filter(TextureFilter::Nearest);
|
||||
/// ```
|
||||
pub fn with_texture_filter(mut self, filter: TextureFilter) -> Self {
|
||||
self.filter = filter;
|
||||
|
||||
// If the texture has already been uploaded, this will force it to be re-uploaded with the
|
||||
// updated filter.
|
||||
*self.texture.lock() = None;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The width of the image.
|
||||
pub fn width(&self) -> usize {
|
||||
self.size[0]
|
||||
}
|
||||
|
||||
/// The height of the image.
|
||||
pub fn height(&self) -> usize {
|
||||
self.size[1]
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size_vec2(&self) -> egui::Vec2 {
|
||||
let [w, h] = self.size();
|
||||
egui::vec2(w as f32, h as f32)
|
||||
}
|
||||
|
||||
/// The debug name of the image, e.g. the file name.
|
||||
pub fn debug_name(&self) -> &str {
|
||||
&self.debug_name
|
||||
}
|
||||
|
||||
/// The texture if for this image.
|
||||
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
|
||||
self.texture
|
||||
.lock()
|
||||
.get_or_insert_with(|| {
|
||||
let image: &mut ColorImage = &mut self.image.lock();
|
||||
let image = std::mem::take(image);
|
||||
ctx.load_texture(&self.debug_name, image, self.filter)
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
/// Show the image with the given maximum size.
|
||||
pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
|
||||
let mut desired_size = self.size_vec2();
|
||||
desired_size *= (max_size.x / desired_size.x).min(1.0);
|
||||
desired_size *= (max_size.y / desired_size.y).min(1.0);
|
||||
self.show_size(ui, desired_size)
|
||||
}
|
||||
|
||||
/// Show the image with the original size (one image pixel = one gui point).
|
||||
pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2())
|
||||
}
|
||||
|
||||
/// Show the image with the given scale factor (1.0 = original size).
|
||||
pub fn show_scaled(&self, ui: &mut egui::Ui, scale: f32) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2() * scale)
|
||||
}
|
||||
|
||||
/// Show the image with the given size.
|
||||
pub fn show_size(&self, ui: &mut egui::Ui, desired_size: egui::Vec2) -> egui::Response {
|
||||
// We need to convert the SVG to a texture to display it:
|
||||
// Future improvement: tell backend to do mip-mapping of the image to
|
||||
// make it look smoother when downsized.
|
||||
ui.image(self.texture_id(ui.ctx()), desired_size)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use egui::ColorImage;
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
///
|
||||
/// Requires the "image" feature. You must also opt-in to the image formats you need
|
||||
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image or unsupported image format.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?;
|
||||
let size = [image.width() as _, image.height() as _];
|
||||
let image_buffer = image.to_rgba8();
|
||||
let pixels = image_buffer.as_flat_samples();
|
||||
Ok(egui::ColorImage::from_rgba_unmultiplied(
|
||||
size,
|
||||
pixels.as_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Load an SVG and rasterize it into an egui image.
|
||||
///
|
||||
/// Requires the "svg" feature.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
let mut opt = usvg::Options::default();
|
||||
opt.fontdb.load_system_fonts();
|
||||
|
||||
let rtree = usvg::Tree::from_data(svg_bytes, &opt.to_ref()).map_err(|err| err.to_string())?;
|
||||
|
||||
let pixmap_size = rtree.svg_node().size.to_screen_size();
|
||||
let [w, h] = [pixmap_size.width(), pixmap_size.height()];
|
||||
|
||||
let mut pixmap = tiny_skia::Pixmap::new(w, h)
|
||||
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?;
|
||||
|
||||
resvg::render(
|
||||
&rtree,
|
||||
usvg::FitTo::Original,
|
||||
Default::default(),
|
||||
pixmap.as_mut(),
|
||||
)
|
||||
.ok_or_else(|| "Failed to render SVG".to_owned())?;
|
||||
|
||||
let image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[pixmap.width() as _, pixmap.height() as _],
|
||||
pixmap.data(),
|
||||
);
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
169
crates/egui_extras/src/layout.rs
Normal file
169
crates/egui_extras/src/layout.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use egui::{Pos2, Rect, Response, Sense, Ui};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum CellSize {
|
||||
/// Absolute size in points
|
||||
Absolute(f32),
|
||||
|
||||
/// Take all available space
|
||||
Remainder,
|
||||
}
|
||||
|
||||
/// Cells are positioned in two dimensions, cells go in one direction and form lines.
|
||||
///
|
||||
/// In a strip there's only one line which goes in the direction of the strip:
|
||||
///
|
||||
/// In a horizontal strip, a [`StripLayout`] with horizontal [`CellDirection`] is used.
|
||||
/// Its cells go from left to right inside this [`StripLayout`].
|
||||
///
|
||||
/// In a table there's a [`StripLayout`] for each table row with a horizontal [`CellDirection`].
|
||||
/// Its cells go from left to right. And the lines go from top to bottom.
|
||||
pub(crate) enum CellDirection {
|
||||
/// Cells go from left to right.
|
||||
Horizontal,
|
||||
|
||||
/// Cells go from top to bottom.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
/// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`]
|
||||
pub struct StripLayout<'l> {
|
||||
pub(crate) ui: &'l mut Ui,
|
||||
direction: CellDirection,
|
||||
pub(crate) rect: Rect,
|
||||
cursor: Pos2,
|
||||
max: Pos2,
|
||||
pub(crate) clip: bool,
|
||||
cell_layout: egui::Layout,
|
||||
}
|
||||
|
||||
impl<'l> StripLayout<'l> {
|
||||
pub(crate) fn new(
|
||||
ui: &'l mut Ui,
|
||||
direction: CellDirection,
|
||||
clip: bool,
|
||||
cell_layout: egui::Layout,
|
||||
) -> Self {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let pos = rect.left_top();
|
||||
|
||||
Self {
|
||||
ui,
|
||||
direction,
|
||||
rect,
|
||||
cursor: pos,
|
||||
max: pos,
|
||||
clip,
|
||||
cell_layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
|
||||
Rect {
|
||||
min: self.cursor,
|
||||
max: Pos2 {
|
||||
x: match width {
|
||||
CellSize::Absolute(width) => self.cursor.x + width,
|
||||
CellSize::Remainder => self.rect.right(),
|
||||
},
|
||||
y: match height {
|
||||
CellSize::Absolute(height) => self.cursor.y + height,
|
||||
CellSize::Remainder => self.rect.bottom(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_pos(&mut self, rect: Rect) {
|
||||
self.max.x = self.max.x.max(rect.right());
|
||||
self.max.y = self.max.y.max(rect.bottom());
|
||||
|
||||
match self.direction {
|
||||
CellDirection::Horizontal => {
|
||||
self.cursor.x = rect.right() + self.ui.spacing().item_spacing.x;
|
||||
}
|
||||
CellDirection::Vertical => {
|
||||
self.cursor.y = rect.bottom() + self.ui.spacing().item_spacing.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
let rect = self.cell_rect(&width, &height);
|
||||
let used_rect = self.cell(rect, add_contents);
|
||||
self.set_pos(rect);
|
||||
self.ui.allocate_rect(rect.union(used_rect), Sense::hover())
|
||||
}
|
||||
|
||||
pub(crate) fn add_striped(
|
||||
&mut self,
|
||||
width: CellSize,
|
||||
height: CellSize,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
let rect = self.cell_rect(&width, &height);
|
||||
|
||||
// Make sure we don't have a gap in the stripe background:
|
||||
let rect = rect.expand2(0.5 * self.ui.spacing().item_spacing);
|
||||
|
||||
self.ui
|
||||
.painter()
|
||||
.rect_filled(rect, 0.0, self.ui.visuals().faint_bg_color);
|
||||
|
||||
self.add(width, height, add_contents)
|
||||
}
|
||||
|
||||
/// only needed for layouts with multiple lines, like [`Table`](crate::Table).
|
||||
pub fn end_line(&mut self) {
|
||||
match self.direction {
|
||||
CellDirection::Horizontal => {
|
||||
self.cursor.y = self.max.y + self.ui.spacing().item_spacing.y;
|
||||
self.cursor.x = self.rect.left();
|
||||
}
|
||||
CellDirection::Vertical => {
|
||||
self.cursor.x = self.max.x + self.ui.spacing().item_spacing.x;
|
||||
self.cursor.y = self.rect.top();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip a lot of space.
|
||||
pub(crate) fn skip_space(&mut self, delta: egui::Vec2) {
|
||||
let before = self.cursor;
|
||||
self.cursor += delta;
|
||||
let rect = Rect::from_two_pos(before, self.cursor);
|
||||
self.ui.allocate_rect(rect, Sense::hover());
|
||||
}
|
||||
|
||||
fn cell(&mut self, rect: Rect, add_contents: impl FnOnce(&mut Ui)) -> Rect {
|
||||
let mut child_ui = self.ui.child_ui(rect, self.cell_layout);
|
||||
|
||||
if self.clip {
|
||||
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
|
||||
let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
|
||||
let clip_rect = rect.expand2(margin);
|
||||
child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect()));
|
||||
}
|
||||
|
||||
add_contents(&mut child_ui);
|
||||
child_ui.min_rect()
|
||||
}
|
||||
|
||||
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
|
||||
pub fn allocate_rect(&mut self) -> Response {
|
||||
let mut rect = self.rect;
|
||||
rect.set_right(self.max.x);
|
||||
rect.set_bottom(self.max.y);
|
||||
|
||||
self.ui.allocate_rect(rect, Sense::hover())
|
||||
}
|
||||
}
|
||||
54
crates/egui_extras/src/lib.rs
Normal file
54
crates/egui_extras/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui).
|
||||
//!
|
||||
//! This crate are for experimental features, and features that require big dependencies that does not belong in `egui`.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod datepicker;
|
||||
|
||||
pub mod image;
|
||||
mod layout;
|
||||
mod sizing;
|
||||
mod strip;
|
||||
mod table;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
pub use crate::datepicker::DatePickerButton;
|
||||
|
||||
pub use crate::image::RetainedImage;
|
||||
pub(crate) use crate::layout::StripLayout;
|
||||
pub use crate::sizing::Size;
|
||||
pub use crate::strip::*;
|
||||
pub use crate::table::*;
|
||||
|
||||
/// Log an error with either `tracing` or `eprintln`
|
||||
macro_rules! log_err {
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!($fmt, $($arg)*);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!(
|
||||
concat!("egui_extras: ", $fmt), $($arg)*
|
||||
);
|
||||
}};
|
||||
}
|
||||
pub(crate) use log_err;
|
||||
|
||||
/// Panic in debug builds, log otherwise.
|
||||
macro_rules! log_or_panic {
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
if cfg!(debug_assertions) {
|
||||
panic!($fmt, $($arg)*);
|
||||
} else {
|
||||
$crate::log_err!($fmt, $($arg)*);
|
||||
}
|
||||
}};
|
||||
}
|
||||
pub(crate) use log_or_panic;
|
||||
177
crates/egui_extras/src/sizing.rs
Normal file
177
crates/egui_extras/src/sizing.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
/// Size hint for table column/strip cell.
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
pub enum Size {
|
||||
/// Absolute size in points, with a given range of allowed sizes to resize within.
|
||||
Absolute { initial: f32, range: (f32, f32) },
|
||||
|
||||
/// Relative size relative to all available space.
|
||||
Relative { fraction: f32, range: (f32, f32) },
|
||||
|
||||
/// Multiple remainders each get the same space.
|
||||
Remainder { range: (f32, f32) },
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// Exactly this big, with no room for resize.
|
||||
pub fn exact(points: f32) -> Self {
|
||||
Self::Absolute {
|
||||
initial: points,
|
||||
range: (points, points),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initially this big, but can resize.
|
||||
pub fn initial(points: f32) -> Self {
|
||||
Self::Absolute {
|
||||
initial: points,
|
||||
range: (0.0, f32::INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`.
|
||||
pub fn relative(fraction: f32) -> Self {
|
||||
egui::egui_assert!(0.0 <= fraction && fraction <= 1.0);
|
||||
Self::Relative {
|
||||
fraction,
|
||||
range: (0.0, f32::INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiple remainders each get the same space.
|
||||
pub fn remainder() -> Self {
|
||||
Self::Remainder {
|
||||
range: (0.0, f32::INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Won't shrink below this size (in points).
|
||||
pub fn at_least(mut self, minimum: f32) -> Self {
|
||||
match &mut self {
|
||||
Self::Absolute { range, .. }
|
||||
| Self::Relative { range, .. }
|
||||
| Self::Remainder { range, .. } => {
|
||||
range.0 = minimum;
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Won't grow above this size (in points).
|
||||
pub fn at_most(mut self, maximum: f32) -> Self {
|
||||
match &mut self {
|
||||
Self::Absolute { range, .. }
|
||||
| Self::Relative { range, .. }
|
||||
| Self::Remainder { range, .. } => {
|
||||
range.1 = maximum;
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table).
|
||||
pub fn range(self) -> (f32, f32) {
|
||||
match self {
|
||||
Self::Absolute { range, .. }
|
||||
| Self::Relative { range, .. }
|
||||
| Self::Remainder { range, .. } => range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Sizing {
|
||||
pub(crate) sizes: Vec<Size>,
|
||||
}
|
||||
|
||||
impl Sizing {
|
||||
pub fn add(&mut self, size: Size) {
|
||||
self.sizes.push(size);
|
||||
}
|
||||
|
||||
pub fn to_lengths(&self, length: f32, spacing: f32) -> Vec<f32> {
|
||||
if self.sizes.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut remainders = 0;
|
||||
let sum_non_remainder = self
|
||||
.sizes
|
||||
.iter()
|
||||
.map(|&size| match size {
|
||||
Size::Absolute { initial, .. } => initial,
|
||||
Size::Relative {
|
||||
fraction,
|
||||
range: (min, max),
|
||||
} => {
|
||||
assert!(0.0 <= fraction && fraction <= 1.0);
|
||||
(length * fraction).clamp(min, max)
|
||||
}
|
||||
Size::Remainder { .. } => {
|
||||
remainders += 1;
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.sum::<f32>()
|
||||
+ spacing * (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::Remainder { range: (min, _max) } = size {
|
||||
if avg_remainder_length < min {
|
||||
remainder_length -= min;
|
||||
remainders -= 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
if remainders > 0 {
|
||||
0.0f32.max(remainder_length / remainders as f32)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
self.sizes
|
||||
.iter()
|
||||
.map(|&size| match size {
|
||||
Size::Absolute { initial, .. } => initial,
|
||||
Size::Relative {
|
||||
fraction,
|
||||
range: (min, max),
|
||||
} => (length * fraction).clamp(min, max),
|
||||
Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Size>> for Sizing {
|
||||
fn from(sizes: Vec<Size>) -> Self {
|
||||
Self { sizes }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sizing() {
|
||||
let sizing: Sizing = vec![].into();
|
||||
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]);
|
||||
|
||||
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
|
||||
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
||||
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![20.0, 10.0]);
|
||||
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![20.0, 0.0]);
|
||||
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![20.0, 0.0]);
|
||||
assert_eq!(sizing.to_lengths(20.0, 10.0), vec![20.0, 0.0]);
|
||||
assert_eq!(sizing.to_lengths(30.0, 10.0), vec![20.0, 0.0]);
|
||||
assert_eq!(sizing.to_lengths(40.0, 10.0), vec![20.0, 10.0]);
|
||||
assert_eq!(sizing.to_lengths(110.0, 10.0), vec![50.0, 50.0]);
|
||||
|
||||
let sizing: Sizing = vec![Size::relative(0.5).at_least(10.0), Size::exact(10.0)].into();
|
||||
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 10.0]);
|
||||
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![15.0, 10.0]);
|
||||
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![10.0, 10.0]);
|
||||
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![10.0, 10.0]);
|
||||
}
|
||||
199
crates/egui_extras/src/strip.rs
Normal file
199
crates/egui_extras/src/strip.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use crate::{
|
||||
layout::{CellDirection, CellSize, StripLayout},
|
||||
sizing::Sizing,
|
||||
Size,
|
||||
};
|
||||
use egui::{Response, Ui};
|
||||
|
||||
/// Builder for creating a new [`Strip`].
|
||||
///
|
||||
/// This can be used to do dynamic layouts.
|
||||
///
|
||||
/// In contrast to normal egui behavior, strip cells do *not* grow with its children!
|
||||
///
|
||||
/// First use [`Self::size`] and [`Self::sizes`] to allocate space for the rows or columns will follow.
|
||||
/// Then build the strip with [`Self::horizontal`]/[`Self::vertical`], and add 'cells'
|
||||
/// to it using [`Strip::cell`]. The number of cells MUST match the number of pre-allocated sizes.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// use egui_extras::{StripBuilder, Size};
|
||||
/// StripBuilder::new(ui)
|
||||
/// .size(Size::remainder().at_least(100.0)) // top cell
|
||||
/// .size(Size::exact(40.0)) // bottom cell
|
||||
/// .vertical(|mut strip| {
|
||||
/// // Add the top 'cell'
|
||||
/// strip.cell(|ui| {
|
||||
/// ui.label("Fixed");
|
||||
/// });
|
||||
/// // We add a nested strip in the bottom cell:
|
||||
/// strip.strip(|builder| {
|
||||
/// builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
/// strip.cell(|ui| {
|
||||
/// ui.label("Top Left");
|
||||
/// });
|
||||
/// strip.cell(|ui| {
|
||||
/// ui.label("Top Right");
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct StripBuilder<'a> {
|
||||
ui: &'a mut Ui,
|
||||
sizing: Sizing,
|
||||
clip: bool,
|
||||
cell_layout: egui::Layout,
|
||||
}
|
||||
|
||||
impl<'a> StripBuilder<'a> {
|
||||
/// Create new strip builder.
|
||||
pub fn new(ui: &'a mut Ui) -> Self {
|
||||
let cell_layout = *ui.layout();
|
||||
Self {
|
||||
ui,
|
||||
sizing: Default::default(),
|
||||
cell_layout,
|
||||
clip: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Should we clip the contents of each cell? Default: `true`.
|
||||
pub fn clip(mut self, clip: bool) -> Self {
|
||||
self.clip = clip;
|
||||
self
|
||||
}
|
||||
|
||||
/// What layout should we use for the individual cells?
|
||||
pub fn cell_layout(mut self, cell_layout: egui::Layout) -> Self {
|
||||
self.cell_layout = cell_layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocate space for for one column/row.
|
||||
pub fn size(mut self, size: Size) -> Self {
|
||||
self.sizing.add(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocate space for for several columns/rows at once.
|
||||
pub fn sizes(mut self, size: Size, count: usize) -> Self {
|
||||
for _ in 0..count {
|
||||
self.sizing.add(size);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Build horizontal strip: Cells are positions from left to right.
|
||||
/// Takes the available horizontal width, so there can't be anything right of the strip or the container will grow slowly!
|
||||
///
|
||||
/// Returns a [`egui::Response`] for hover events.
|
||||
pub fn horizontal<F>(self, strip: F) -> Response
|
||||
where
|
||||
F: for<'b> FnOnce(Strip<'a, 'b>),
|
||||
{
|
||||
let widths = self.sizing.to_lengths(
|
||||
self.ui.available_rect_before_wrap().width(),
|
||||
self.ui.spacing().item_spacing.x,
|
||||
);
|
||||
let mut layout = StripLayout::new(
|
||||
self.ui,
|
||||
CellDirection::Horizontal,
|
||||
self.clip,
|
||||
self.cell_layout,
|
||||
);
|
||||
strip(Strip {
|
||||
layout: &mut layout,
|
||||
direction: CellDirection::Horizontal,
|
||||
sizes: widths,
|
||||
size_index: 0,
|
||||
});
|
||||
layout.allocate_rect()
|
||||
}
|
||||
|
||||
/// Build vertical strip: Cells are positions from top to bottom.
|
||||
/// Takes the full available vertical height, so there can't be anything below of the strip or the container will grow slowly!
|
||||
///
|
||||
/// Returns a [`egui::Response`] for hover events.
|
||||
pub fn vertical<F>(self, strip: F) -> Response
|
||||
where
|
||||
F: for<'b> FnOnce(Strip<'a, 'b>),
|
||||
{
|
||||
let heights = self.sizing.to_lengths(
|
||||
self.ui.available_rect_before_wrap().height(),
|
||||
self.ui.spacing().item_spacing.y,
|
||||
);
|
||||
let mut layout = StripLayout::new(
|
||||
self.ui,
|
||||
CellDirection::Vertical,
|
||||
self.clip,
|
||||
self.cell_layout,
|
||||
);
|
||||
strip(Strip {
|
||||
layout: &mut layout,
|
||||
direction: CellDirection::Vertical,
|
||||
sizes: heights,
|
||||
size_index: 0,
|
||||
});
|
||||
layout.allocate_rect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Strip of cells which go in one direction. Each cell has a fixed size.
|
||||
/// In contrast to normal egui behavior, strip cells do *not* grow with its children!
|
||||
pub struct Strip<'a, 'b> {
|
||||
layout: &'b mut StripLayout<'a>,
|
||||
direction: CellDirection,
|
||||
sizes: Vec<f32>,
|
||||
size_index: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Strip<'a, 'b> {
|
||||
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
|
||||
let size = if let Some(size) = self.sizes.get(self.size_index) {
|
||||
self.size_index += 1;
|
||||
*size
|
||||
} else {
|
||||
crate::log_or_panic!(
|
||||
"Added more `Strip` cells than were pre-allocated ({} pre-allocated)",
|
||||
self.sizes.len()
|
||||
);
|
||||
8.0 // anything will look wrong, so pick something that is obviously wrong
|
||||
};
|
||||
|
||||
match self.direction {
|
||||
CellDirection::Horizontal => (CellSize::Absolute(size), CellSize::Remainder),
|
||||
CellDirection::Vertical => (CellSize::Remainder, CellSize::Absolute(size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add cell contents.
|
||||
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
|
||||
let (width, height) = self.next_cell_size();
|
||||
self.layout.add(width, height, add_contents);
|
||||
}
|
||||
|
||||
/// Add an empty cell.
|
||||
pub fn empty(&mut self) {
|
||||
let (width, height) = self.next_cell_size();
|
||||
self.layout.empty(width, height);
|
||||
}
|
||||
|
||||
/// Add a strip as cell.
|
||||
pub fn strip(&mut self, strip_builder: impl FnOnce(StripBuilder<'_>)) {
|
||||
let clip = self.layout.clip;
|
||||
self.cell(|ui| {
|
||||
strip_builder(StripBuilder::new(ui).clip(clip));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Drop for Strip<'a, 'b> {
|
||||
fn drop(&mut self) {
|
||||
while self.size_index < self.sizes.len() {
|
||||
self.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
634
crates/egui_extras/src/table.rs
Normal file
634
crates/egui_extras/src/table.rs
Normal file
@@ -0,0 +1,634 @@
|
||||
//! 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 |
|
||||
//! Takes all available height, so if you want something below the table, put it in a strip.
|
||||
|
||||
use crate::{
|
||||
layout::{CellDirection, CellSize},
|
||||
sizing::Sizing,
|
||||
Size, StripLayout,
|
||||
};
|
||||
|
||||
use egui::{Rect, Response, Ui, Vec2};
|
||||
|
||||
/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
|
||||
///
|
||||
/// Cell widths are precalculated so we can have tables like this:
|
||||
///
|
||||
/// | fixed size | all available space/minimum | 30% of available width | fixed size |
|
||||
///
|
||||
/// In contrast to normal egui behavior, columns/rows do *not* grow with its children!
|
||||
/// Takes all available height, so if you want something below the table, put it in a strip.
|
||||
///
|
||||
/// You must pre-allocate all columns with [`Self::column`]/[`Self::columns`].
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// use egui_extras::{TableBuilder, Size};
|
||||
/// TableBuilder::new(ui)
|
||||
/// .column(Size::remainder().at_least(100.0))
|
||||
/// .column(Size::exact(40.0))
|
||||
/// .header(20.0, |mut header| {
|
||||
/// header.col(|ui| {
|
||||
/// ui.heading("Growing");
|
||||
/// });
|
||||
/// header.col(|ui| {
|
||||
/// ui.heading("Fixed");
|
||||
/// });
|
||||
/// })
|
||||
/// .body(|mut body| {
|
||||
/// body.row(30.0, |mut row| {
|
||||
/// row.col(|ui| {
|
||||
/// ui.label("first row growing cell");
|
||||
/// });
|
||||
/// row.col(|ui| {
|
||||
/// ui.button("action");
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct TableBuilder<'a> {
|
||||
ui: &'a mut Ui,
|
||||
sizing: Sizing,
|
||||
scroll: bool,
|
||||
striped: bool,
|
||||
resizable: bool,
|
||||
clip: bool,
|
||||
stick_to_bottom: bool,
|
||||
cell_layout: egui::Layout,
|
||||
}
|
||||
|
||||
impl<'a> TableBuilder<'a> {
|
||||
pub fn new(ui: &'a mut Ui) -> Self {
|
||||
let cell_layout = *ui.layout();
|
||||
Self {
|
||||
ui,
|
||||
sizing: Default::default(),
|
||||
scroll: true,
|
||||
striped: false,
|
||||
resizable: false,
|
||||
clip: true,
|
||||
stick_to_bottom: false,
|
||||
cell_layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Make the columns resizable by dragging.
|
||||
///
|
||||
/// If the _last_ column is [`Size::Remainder`], then it won't be resizable
|
||||
/// (and instead use up the remainder).
|
||||
///
|
||||
/// Default is `false`.
|
||||
///
|
||||
/// If you have multiple [`Table`]:s in the same [`Ui`]
|
||||
/// you will need to give them unique id:s with [`Ui::push_id`].
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resizable = resizable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Should we clip the contents of each cell? Default: `true`.
|
||||
pub fn clip(mut self, clip: bool) -> Self {
|
||||
self.clip = clip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Should the scroll handle stick to the bottom position even as the content size changes
|
||||
/// dynamically? The scroll handle remains stuck until manually changed, and will become stuck
|
||||
/// once again when repositioned to the bottom. Default: `false`.
|
||||
pub fn stick_to_bottom(mut self, stick: bool) -> Self {
|
||||
self.stick_to_bottom = stick;
|
||||
self
|
||||
}
|
||||
|
||||
/// What layout should we use for the individual cells?
|
||||
pub fn cell_layout(mut self, cell_layout: egui::Layout) -> Self {
|
||||
self.cell_layout = cell_layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocate space for one column.
|
||||
pub fn column(mut self, width: Size) -> Self {
|
||||
self.sizing.add(width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocate space for several columns at once.
|
||||
pub fn columns(mut self, size: Size, count: usize) -> Self {
|
||||
for _ in 0..count {
|
||||
self.sizing.add(size);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn available_width(&self) -> f32 {
|
||||
self.ui.available_rect_before_wrap().width()
|
||||
- if self.scroll {
|
||||
self.ui.spacing().item_spacing.x + self.ui.spacing().scroll_bar_width
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 available_width = self.available_width();
|
||||
|
||||
let Self {
|
||||
ui,
|
||||
sizing,
|
||||
scroll,
|
||||
striped,
|
||||
resizable,
|
||||
clip,
|
||||
stick_to_bottom,
|
||||
cell_layout,
|
||||
} = self;
|
||||
|
||||
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
|
||||
|
||||
let default_widths = sizing.to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||
let widths = read_persisted_widths(ui, default_widths, resize_id);
|
||||
|
||||
let table_top = ui.cursor().top();
|
||||
|
||||
{
|
||||
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, clip, cell_layout);
|
||||
header(TableRow {
|
||||
layout: &mut layout,
|
||||
widths: &widths,
|
||||
width_index: 0,
|
||||
striped: false,
|
||||
height,
|
||||
});
|
||||
layout.allocate_rect();
|
||||
}
|
||||
|
||||
Table {
|
||||
ui,
|
||||
table_top,
|
||||
resize_id,
|
||||
sizing,
|
||||
available_width,
|
||||
widths,
|
||||
scroll,
|
||||
striped,
|
||||
clip,
|
||||
stick_to_bottom,
|
||||
cell_layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create table body without a header row
|
||||
pub fn body<F>(self, body: F)
|
||||
where
|
||||
F: for<'b> FnOnce(TableBody<'b>),
|
||||
{
|
||||
let available_width = self.available_width();
|
||||
|
||||
let Self {
|
||||
ui,
|
||||
sizing,
|
||||
scroll,
|
||||
striped,
|
||||
resizable,
|
||||
clip,
|
||||
stick_to_bottom,
|
||||
cell_layout,
|
||||
} = self;
|
||||
|
||||
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
|
||||
|
||||
let default_widths = sizing.to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||
let widths = read_persisted_widths(ui, default_widths, resize_id);
|
||||
|
||||
let table_top = ui.cursor().top();
|
||||
|
||||
Table {
|
||||
ui,
|
||||
table_top,
|
||||
resize_id,
|
||||
sizing,
|
||||
available_width,
|
||||
widths,
|
||||
scroll,
|
||||
striped,
|
||||
clip,
|
||||
stick_to_bottom,
|
||||
cell_layout,
|
||||
}
|
||||
.body(body);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_persisted_widths(
|
||||
ui: &egui::Ui,
|
||||
default_widths: Vec<f32>,
|
||||
resize_id: Option<egui::Id>,
|
||||
) -> Vec<f32> {
|
||||
if let Some(resize_id) = resize_id {
|
||||
let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
|
||||
ui.ctx().check_for_id_clash(resize_id, rect, "Table");
|
||||
if let Some(persisted) = ui.data().get_persisted::<Vec<f32>>(resize_id) {
|
||||
// make sure that the stored widths aren't out-dated
|
||||
if persisted.len() == default_widths.len() {
|
||||
return persisted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default_widths
|
||||
}
|
||||
|
||||
/// Table struct which can construct a [`TableBody`].
|
||||
///
|
||||
/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
|
||||
pub struct Table<'a> {
|
||||
ui: &'a mut Ui,
|
||||
table_top: f32,
|
||||
resize_id: Option<egui::Id>,
|
||||
sizing: Sizing,
|
||||
available_width: f32,
|
||||
widths: Vec<f32>,
|
||||
scroll: bool,
|
||||
striped: bool,
|
||||
clip: bool,
|
||||
stick_to_bottom: bool,
|
||||
cell_layout: egui::Layout,
|
||||
}
|
||||
|
||||
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 Table {
|
||||
ui,
|
||||
table_top,
|
||||
resize_id,
|
||||
sizing,
|
||||
mut available_width,
|
||||
widths,
|
||||
scroll,
|
||||
striped,
|
||||
clip,
|
||||
stick_to_bottom,
|
||||
cell_layout,
|
||||
} = self;
|
||||
|
||||
let avail_rect = ui.available_rect_before_wrap();
|
||||
|
||||
let mut new_widths = widths.clone();
|
||||
|
||||
let scroll_area = egui::ScrollArea::new([false, scroll])
|
||||
.auto_shrink([true; 2])
|
||||
.stick_to_bottom(stick_to_bottom);
|
||||
|
||||
scroll_area.show(ui, move |ui| {
|
||||
let layout = StripLayout::new(ui, CellDirection::Horizontal, clip, cell_layout);
|
||||
|
||||
body(TableBody {
|
||||
layout,
|
||||
widths,
|
||||
striped,
|
||||
row_nr: 0,
|
||||
start_y: avail_rect.top(),
|
||||
end_y: avail_rect.bottom(),
|
||||
});
|
||||
});
|
||||
|
||||
let bottom = ui.min_rect().bottom();
|
||||
|
||||
// TODO(emilk): fix frame-delay by interacting before laying out (but painting later).
|
||||
if let Some(resize_id) = resize_id {
|
||||
let spacing_x = ui.spacing().item_spacing.x;
|
||||
let mut x = avail_rect.left() - spacing_x * 0.5;
|
||||
for (i, width) in new_widths.iter_mut().enumerate() {
|
||||
x += *width + spacing_x;
|
||||
|
||||
// If the last column is Size::Remainder, then let it fill the remainder!
|
||||
let last_column = i + 1 == sizing.sizes.len();
|
||||
if last_column {
|
||||
if let Size::Remainder { range: (min, max) } = sizing.sizes[i] {
|
||||
let eps = 0.1; // just to avoid some rounding errors.
|
||||
*width = (available_width - eps).clamp(min, max);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let resize_id = ui.id().with("__panel_resize").with(i);
|
||||
|
||||
let mut p0 = egui::pos2(x, table_top);
|
||||
let mut p1 = egui::pos2(x, bottom);
|
||||
let line_rect = egui::Rect::from_min_max(p0, p1)
|
||||
.expand(ui.style().interaction.resize_grab_radius_side);
|
||||
let mouse_over_resize_line = ui.rect_contains_pointer(line_rect);
|
||||
|
||||
if ui.input().pointer.any_pressed()
|
||||
&& ui.input().pointer.any_down()
|
||||
&& mouse_over_resize_line
|
||||
{
|
||||
ui.memory().set_dragged_id(resize_id);
|
||||
}
|
||||
let is_resizing = ui.memory().is_being_dragged(resize_id);
|
||||
if is_resizing {
|
||||
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
|
||||
let new_width = *width + pointer.x - x;
|
||||
let (min, max) = sizing.sizes[i].range();
|
||||
let new_width = new_width.clamp(min, max);
|
||||
let x = x - *width + new_width;
|
||||
p0.x = x;
|
||||
p1.x = x;
|
||||
|
||||
*width = new_width;
|
||||
}
|
||||
}
|
||||
|
||||
let dragging_something_else =
|
||||
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
|
||||
let resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||
|
||||
if resize_hover || is_resizing {
|
||||
ui.output().cursor_icon = egui::CursorIcon::ResizeColumn;
|
||||
}
|
||||
|
||||
let stroke = if is_resizing {
|
||||
ui.style().visuals.widgets.active.bg_stroke
|
||||
} else if resize_hover {
|
||||
ui.style().visuals.widgets.hovered.bg_stroke
|
||||
} else {
|
||||
// ui.visuals().widgets.inactive.bg_stroke
|
||||
ui.visuals().widgets.noninteractive.bg_stroke
|
||||
};
|
||||
ui.painter().line_segment([p0, p1], stroke);
|
||||
|
||||
available_width -= *width + spacing_x;
|
||||
}
|
||||
|
||||
ui.data().insert_persisted(resize_id, new_widths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The body of a table.
|
||||
/// Is created by calling `body` on a [`Table`] (after adding a header row) or [`TableBuilder`] (without a header row).
|
||||
pub struct TableBody<'a> {
|
||||
layout: StripLayout<'a>,
|
||||
widths: Vec<f32>,
|
||||
striped: bool,
|
||||
row_nr: usize,
|
||||
start_y: f32,
|
||||
end_y: f32,
|
||||
}
|
||||
|
||||
impl<'a> TableBody<'a> {
|
||||
fn scroll_offset_y(&self) -> f32 {
|
||||
self.start_y - self.layout.rect.top()
|
||||
}
|
||||
|
||||
/// Return a vector containing all column widths for this table body.
|
||||
///
|
||||
/// This is primarily meant for use with [`TableBody::heterogeneous_rows`] in cases where row
|
||||
/// heights are expected to according to the width of one or more cells -- for example, if text
|
||||
/// is wrapped rather than clipped within the cell.
|
||||
pub fn widths(&self) -> &[f32] {
|
||||
&self.widths
|
||||
}
|
||||
|
||||
/// Add a single row with the given height.
|
||||
///
|
||||
/// If you have many thousands of row it can be more performant to instead use [`Self::rows`] or [`Self::heterogeneous_rows`].
|
||||
pub fn row(&mut self, height: f32, row: impl FnOnce(TableRow<'a, '_>)) {
|
||||
row(TableRow {
|
||||
layout: &mut self.layout,
|
||||
widths: &self.widths,
|
||||
width_index: 0,
|
||||
striped: self.striped && self.row_nr % 2 == 0,
|
||||
height,
|
||||
});
|
||||
|
||||
self.row_nr += 1;
|
||||
}
|
||||
|
||||
/// Add many rows with same height.
|
||||
///
|
||||
/// Is a lot more performant than adding each individual row as non visible rows must not be rendered.
|
||||
///
|
||||
/// If you need many rows with different heights, use [`Self::heterogeneous_rows`] instead.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// use egui_extras::{TableBuilder, Size};
|
||||
/// TableBuilder::new(ui)
|
||||
/// .column(Size::remainder().at_least(100.0))
|
||||
/// .body(|mut body| {
|
||||
/// let row_height = 18.0;
|
||||
/// let num_rows = 10_000;
|
||||
/// body.rows(row_height, num_rows, |row_index, mut row| {
|
||||
/// row.col(|ui| {
|
||||
/// ui.label("First column");
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn rows(
|
||||
mut self,
|
||||
row_height_sans_spacing: f32,
|
||||
total_rows: usize,
|
||||
mut row: impl FnMut(usize, TableRow<'_, '_>),
|
||||
) {
|
||||
let spacing = self.layout.ui.spacing().item_spacing;
|
||||
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
|
||||
|
||||
let scroll_offset_y = self
|
||||
.scroll_offset_y()
|
||||
.min(total_rows as f32 * row_height_with_spacing);
|
||||
let max_height = self.end_y - self.start_y;
|
||||
let mut min_row = 0;
|
||||
|
||||
if scroll_offset_y > 0.0 {
|
||||
min_row = (scroll_offset_y / row_height_with_spacing).floor() as usize;
|
||||
self.add_buffer(min_row as f32 * row_height_with_spacing);
|
||||
}
|
||||
|
||||
let max_row =
|
||||
((scroll_offset_y + max_height) / row_height_with_spacing).ceil() as usize + 1;
|
||||
let max_row = max_row.min(total_rows);
|
||||
|
||||
for idx in min_row..max_row {
|
||||
row(
|
||||
idx,
|
||||
TableRow {
|
||||
layout: &mut self.layout,
|
||||
widths: &self.widths,
|
||||
width_index: 0,
|
||||
striped: self.striped && idx % 2 == 0,
|
||||
height: row_height_sans_spacing,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if total_rows - max_row > 0 {
|
||||
let skip_height = (total_rows - max_row) as f32 * row_height_with_spacing;
|
||||
self.add_buffer(skip_height - spacing.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add rows with varying heights.
|
||||
///
|
||||
/// This takes a very slight performance hit compared to [`TableBody::rows`] due to the need to
|
||||
/// iterate over all row heights in to calculate the virtual table height above and below the
|
||||
/// visible region, but it is many orders of magnitude more performant than adding individual
|
||||
/// heterogenously-sized rows using [`TableBody::row`] at the cost of the additional complexity
|
||||
/// that comes with pre-calculating row heights and representing them as an iterator.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// use egui_extras::{TableBuilder, Size};
|
||||
/// TableBuilder::new(ui)
|
||||
/// .column(Size::remainder().at_least(100.0))
|
||||
/// .body(|mut body| {
|
||||
/// let row_heights: Vec<f32> = vec![60.0, 18.0, 31.0, 240.0];
|
||||
/// body.heterogeneous_rows(row_heights.into_iter(), |row_index, mut row| {
|
||||
/// let thick = row_index % 6 == 0;
|
||||
/// row.col(|ui| {
|
||||
/// ui.centered_and_justified(|ui| {
|
||||
/// ui.label(row_index.to_string());
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn heterogeneous_rows(
|
||||
mut self,
|
||||
heights: impl Iterator<Item = f32>,
|
||||
mut populate_row: impl FnMut(usize, TableRow<'_, '_>),
|
||||
) {
|
||||
let spacing = self.layout.ui.spacing().item_spacing;
|
||||
let mut enumerated_heights = heights.enumerate();
|
||||
|
||||
let max_height = self.end_y - self.start_y;
|
||||
let scroll_offset_y = self.scroll_offset_y() as f64;
|
||||
|
||||
let mut cursor_y: f64 = 0.0;
|
||||
|
||||
// Skip the invisible rows, and populate the first non-virtual row.
|
||||
for (row_index, row_height) in &mut enumerated_heights {
|
||||
let old_cursor_y = cursor_y;
|
||||
cursor_y += (row_height + spacing.y) as f64;
|
||||
if cursor_y >= scroll_offset_y {
|
||||
// This row is visible:
|
||||
self.add_buffer(old_cursor_y as f32);
|
||||
let tr = TableRow {
|
||||
layout: &mut self.layout,
|
||||
widths: &self.widths,
|
||||
width_index: 0,
|
||||
striped: self.striped && row_index % 2 == 0,
|
||||
height: row_height,
|
||||
};
|
||||
populate_row(row_index, tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// populate visible rows:
|
||||
for (row_index, row_height) in &mut enumerated_heights {
|
||||
let tr = TableRow {
|
||||
layout: &mut self.layout,
|
||||
widths: &self.widths,
|
||||
width_index: 0,
|
||||
striped: self.striped && row_index % 2 == 0,
|
||||
height: row_height,
|
||||
};
|
||||
populate_row(row_index, tr);
|
||||
cursor_y += (row_height + spacing.y) as f64;
|
||||
|
||||
if cursor_y > scroll_offset_y + max_height as f64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate height below the visible table range:
|
||||
let mut height_below_visible: f64 = 0.0;
|
||||
for (_, height) in enumerated_heights {
|
||||
height_below_visible += height as f64;
|
||||
}
|
||||
if height_below_visible > 0.0 {
|
||||
// we need to add a buffer to allow the table to
|
||||
// accurately calculate the scrollbar position
|
||||
self.add_buffer(height_below_visible as f32);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a table row buffer of the given height to represent the non-visible portion of the
|
||||
// table.
|
||||
fn add_buffer(&mut self, height: f32) {
|
||||
self.layout.skip_space(egui::vec2(0.0, height));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for TableBody<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.layout.allocate_rect();
|
||||
}
|
||||
}
|
||||
|
||||
/// The row of a table.
|
||||
/// Is created by [`TableRow`] for each created [`TableBody::row`] or each visible row in rows created by calling [`TableBody::rows`].
|
||||
pub struct TableRow<'a, 'b> {
|
||||
layout: &'b mut StripLayout<'a>,
|
||||
widths: &'b [f32],
|
||||
width_index: usize,
|
||||
striped: bool,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl<'a, 'b> TableRow<'a, 'b> {
|
||||
/// Add the contents of a column.
|
||||
pub fn col(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
|
||||
let width = if let Some(width) = self.widths.get(self.width_index) {
|
||||
self.width_index += 1;
|
||||
*width
|
||||
} else {
|
||||
crate::log_or_panic!(
|
||||
"Added more `Table` columns than were pre-allocated ({} pre-allocated)",
|
||||
self.widths.len()
|
||||
);
|
||||
8.0 // anything will look wrong, so pick something that is obviously wrong
|
||||
};
|
||||
|
||||
let width = CellSize::Absolute(width);
|
||||
let height = CellSize::Absolute(self.height);
|
||||
|
||||
if self.striped {
|
||||
self.layout.add_striped(width, height, add_contents)
|
||||
} else {
|
||||
self.layout.add(width, height, add_contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Drop for TableRow<'a, 'b> {
|
||||
fn drop(&mut self) {
|
||||
self.layout.end_line();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user