This commit is contained in:
nora 2023-04-18 15:38:14 +02:00
parent 12163d1338
commit 550b1644cb
363 changed files with 84081 additions and 16 deletions

View file

@ -0,0 +1,153 @@
use super::popup::DatePickerPopup;
use chrono::NaiveDate;
use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget};
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct DatePickerButtonState {
pub picker_visible: bool,
}
pub struct DatePickerButton<'a> {
selection: &'a mut NaiveDate,
id_source: Option<&'a str>,
combo_boxes: bool,
arrows: bool,
calendar: bool,
calendar_week: bool,
show_icon: bool,
}
impl<'a> DatePickerButton<'a> {
pub fn new(selection: &'a mut NaiveDate) -> Self {
Self {
selection,
id_source: None,
combo_boxes: true,
arrows: true,
calendar: true,
calendar_week: true,
show_icon: 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
}
/// Show the calender icon on the button. (Default: true)
pub fn show_icon(mut self, show_icon: bool) -> Self {
self.show_icon = show_icon;
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_mut(|mem| mem.data.get_persisted::<DatePickerButtonState>(id))
.unwrap_or_default();
let mut text = if self.show_icon {
RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d")))
} else {
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.weak_bg_fill).stroke(visuals.bg_stroke);
}
let mut button_response = ui.add(button);
if button_response.clicked() {
button_state.picker_visible = true;
ui.memory_mut(|mem| mem.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 InnerResponse {
inner: saved,
response: 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)
})
.inner
});
if saved {
button_response.mark_changed();
}
if !button_response.clicked()
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
button_state.picker_visible = false;
ui.memory_mut(|mem| mem.data.insert_persisted(id, button_state));
}
}
button_response
}
}

View file

@ -0,0 +1,34 @@
mod button;
mod popup;
pub use button::DatePickerButton;
use chrono::{Datelike, Duration, NaiveDate, Weekday};
#[derive(Debug)]
struct Week {
number: u8,
days: Vec<NaiveDate>,
}
fn month_data(year: i32, month: u32) -> Vec<Week> {
let first = NaiveDate::from_ymd_opt(year, month, 1).expect("Could not create NaiveDate");
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,433 @@
use chrono::{Datelike, NaiveDate, Weekday};
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2};
use super::{button::DatePickerButtonState, month_data};
use crate::{Column, Size, StripBuilder, TableBuilder};
#[derive(Default, Clone, 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: NaiveDate =
NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("Could not create NaiveDate");
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 NaiveDate,
pub button_id: Id,
pub combo_boxes: bool,
pub arrows: bool,
pub calendar: bool,
pub calendar_week: bool,
}
impl<'a> DatePickerPopup<'a> {
/// Returns `true` if user pressed `Save` button.
pub fn draw(&mut self, ui: &mut Ui) -> bool {
let id = ui.make_persistent_id("date_picker");
let today = chrono::offset::Utc::now().date_naive();
let mut popup_state = ui
.memory_mut(|mem| mem.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_mut(|mem| mem.data.insert_persisted(id, popup_state.clone()));
}
let weeks = month_data(popup_state.year, popup_state.month);
let (mut close, mut saved) = (false, 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),
self.calendar as usize,
)
.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_mut(|mem| {
mem.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_mut(|mem| {
mem.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_mut(|mem| {
mem.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("subtract one year")
.clicked()
{
popup_state.year -= 1;
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory_mut(|mem| {
mem.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("subtract 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_mut(|mem| {
mem.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("subtract 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_mut(|mem| {
mem.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_mut(|mem| {
mem.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_mut(|mem| {
mem.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_mut(|mem| {
mem.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)
.vscroll(false)
.columns(Column::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_mut(|mem| {
mem.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 = NaiveDate::from_ymd_opt(
popup_state.year,
popup_state.month,
popup_state.day,
)
.expect("Could not create NaiveDate");
saved = true;
close = true;
}
});
});
});
});
});
if close {
popup_state.setup = false;
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state);
mem.data
.get_persisted_mut_or_default::<DatePickerButtonState>(self.button_id)
.picker_visible = false;
});
}
saved && close
}
}
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),
}
}

View file

@ -0,0 +1,265 @@
use egui::{mutex::Mutex, TextureFilter, TextureOptions};
#[cfg(feature = "svg")]
pub use usvg::FitTo;
/// 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>>,
options: TextureOptions,
}
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(),
options: Default::default(),
}
}
/// Load a (non-svg) image.
///
/// `image_bytes` should be the raw contents of an image file (`.png`, `.jpg`, …).
///
/// 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> {
Self::from_svg_bytes_with_size(debug_name, svg_bytes, FitTo::Original)
}
/// 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())
}
/// Pass in the bytes of an SVG that you've loaded
/// and the scaling option to resize the SVG with
///
/// # Errors
/// On invalid image
#[cfg(feature = "svg")]
pub fn from_svg_bytes_with_size(
debug_name: impl Into<String>,
svg_bytes: &[u8],
size: FitTo,
) -> Result<Self, String> {
Ok(Self::from_color_image(
debug_name,
load_svg_bytes_with_size(svg_bytes, size)?,
))
}
/// Set the texture filters 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::TextureOptions}};
/// # 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_options(TextureOptions::NEAREST);
/// ```
pub fn with_options(mut self, options: TextureOptions) -> Self {
self.options = options;
// If the texture has already been uploaded, this will force it to be re-uploaded with the
// updated filter.
*self.texture.lock() = None;
self
}
#[deprecated = "Use with_options instead"]
pub fn with_texture_filter(self, filter: TextureFilter) -> Self {
self.with_options(TextureOptions {
magnification: filter,
minification: filter,
})
}
/// 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.options)
})
.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> {
load_svg_bytes_with_size(svg_bytes, FitTo::Original)
}
/// Load an SVG and rasterize it into an egui image with a scaling parameter.
///
/// Requires the "svg" feature.
///
/// # Errors
/// On invalid image
#[cfg(feature = "svg")]
pub fn load_svg_bytes_with_size(
svg_bytes: &[u8],
fit_to: FitTo,
) -> Result<egui::ColorImage, String> {
let opt = usvg::Options::default();
let rtree = usvg::Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?;
let pixmap_size = rtree.size.to_screen_size();
let [w, h] = match fit_to {
FitTo::Original => [pixmap_size.width(), pixmap_size.height()],
FitTo::Size(w, h) => [w, h],
FitTo::Height(h) => [
(pixmap_size.width() as f32 * (h as f32 / pixmap_size.height() as f32)) as u32,
h,
],
FitTo::Width(w) => [
w,
(pixmap_size.height() as f32 * (w as f32 / pixmap_size.width() as f32)) as u32,
],
FitTo::Zoom(z) => [
(pixmap_size.width() as f32 * z) as u32,
(pixmap_size.height() as f32 * z) as u32,
],
};
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, fit_to, Default::default(), pixmap.as_mut())
.ok_or_else(|| "Failed to render SVG".to_owned())?;
let image = egui::ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data());
Ok(image)
}

View file

@ -0,0 +1,171 @@
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,
pub(crate) cursor: Pos2,
/// Keeps track of the max used position,
/// so we know how much space we used.
max: Pos2,
cell_layout: egui::Layout,
}
impl<'l> StripLayout<'l> {
pub(crate) fn new(ui: &'l mut Ui, direction: CellDirection, 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,
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));
}
/// This is the innermost part of [`crate::Table`] and [`crate::Strip`].
///
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
pub(crate) fn add(
&mut self,
clip: bool,
striped: bool,
width: CellSize,
height: CellSize,
add_cell_contents: impl FnOnce(&mut Ui),
) -> (Rect, Response) {
let max_rect = self.cell_rect(&width, &height);
if striped {
// Make sure we don't have a gap in the stripe background:
let stripe_rect = max_rect.expand2(0.5 * self.ui.spacing().item_spacing);
self.ui
.painter()
.rect_filled(stripe_rect, 0.0, self.ui.visuals().faint_bg_color);
}
let used_rect = self.cell(clip, max_rect, add_cell_contents);
self.set_pos(max_rect);
let allocation_rect = if clip {
max_rect
} else {
max_rect.union(used_rect)
};
let response = self.ui.allocate_rect(allocation_rect, Sense::hover());
(used_rect, response)
}
/// 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, clip: bool, rect: Rect, add_cell_contents: impl FnOnce(&mut Ui)) -> Rect {
let mut child_ui = self.ui.child_ui(rect, self.cell_layout);
if 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_cell_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())
}
}

View file

@ -0,0 +1,55 @@
//! 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)]
#![forbid(unsafe_code)]
#[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;

View 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]);
}

View file

@ -0,0 +1,197 @@
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: false,
}
}
/// Should we clip the contents of each cell? Default: `false`.
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.cell_layout);
strip(Strip {
layout: &mut layout,
direction: CellDirection::Horizontal,
clip: self.clip,
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.cell_layout);
strip(Strip {
layout: &mut layout,
direction: CellDirection::Vertical,
clip: self.clip,
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,
clip: bool,
sizes: Vec<f32>,
size_index: usize,
}
impl<'a, 'b> Strip<'a, 'b> {
#[cfg_attr(debug_assertions, track_caller)]
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.
#[cfg_attr(debug_assertions, track_caller)]
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
let (width, height) = self.next_cell_size();
let striped = false;
self.layout
.add(self.clip, striped, width, height, add_contents);
}
/// Add an empty cell.
#[cfg_attr(debug_assertions, track_caller)]
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.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();
}
}
}

File diff suppressed because it is too large Load diff