Dynamic sized strips, tables, and date picker (#963)
This commit is contained in:
parent
95ff7ec000
commit
1d32670cf3
19 changed files with 1658 additions and 36 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -4,9 +4,9 @@ version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ab_glyph"
|
name = "ab_glyph"
|
||||||
version = "0.2.13"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61caed9aec6daeee1ea38ccf5fb225e4f96c1eeead1b4a5c267324a63cf02326"
|
checksum = "d54a65e0d4f66f8536c98cb3ca81ca33b7e2ca43442465507a3a62291ec0d9e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph_rasterizer",
|
"ab_glyph_rasterizer",
|
||||||
"owned_ttf_parser",
|
"owned_ttf_parser",
|
||||||
|
@ -423,9 +423,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-expr"
|
name = "cfg-expr"
|
||||||
version = "0.10.1"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd"
|
checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
@ -1080,9 +1080,11 @@ dependencies = [
|
||||||
name = "egui_extras"
|
name = "egui_extras"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"egui",
|
"egui",
|
||||||
"image",
|
"image",
|
||||||
"resvg",
|
"resvg",
|
||||||
|
"serde",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"usvg",
|
"usvg",
|
||||||
]
|
]
|
||||||
|
@ -1164,9 +1166,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-map"
|
name = "enum-map"
|
||||||
version = "2.0.2"
|
version = "2.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7848397e7221a27d81cb7f07498d563f09b23fcd52ce9f74a6a110ed28f7cd4f"
|
checksum = "82605a2a3d13a9661b07ba27f39d00496aa347c9c236b1a3b8201c1b6d761408"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enum-map-derive",
|
"enum-map-derive",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1305,14 +1307,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontdb"
|
name = "fontdb"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db856ee8cca3b9f23dd11c13bf3d4854b663ae86ed0c4a627a354431fc265f66"
|
checksum = "122fa73a5566372f9df09768a16e8e3dad7ad18abe07835f1f0b71f84078ba4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fontconfig-parser",
|
"fontconfig-parser",
|
||||||
"log",
|
"log",
|
||||||
"memmap2 0.5.3",
|
"memmap2 0.5.3",
|
||||||
"ttf-parser 0.15.0",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1425,9 +1427,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
|
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2297,11 +2299,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef05f2882a8b3e7acc10c153ade2631f7bfc8ce00d2bf3fb8f4e9d2ae6ea5c3"
|
checksum = "4fb1e509cfe7a12db2a90bfa057dfcdbc55a347f5da677c506b53dd099cfec9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ttf-parser 0.14.0",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2450,9 +2452,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.17.3"
|
version = "0.17.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e8f1882177b17c98ec33a51f5910ecbf4db92ca0def706781a1f8d0c661f393"
|
checksum = "02cd7d51cea7e2fa6bbcb8af5fbcad15b871451bfc2d20ed72dff2f4ae072a84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
@ -2490,9 +2492,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dada8c9981fcf32929c3c0f0cd796a9284aca335565227ed88c83babb1d43dc"
|
checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -2775,7 +2777,7 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"ttf-parser 0.15.0",
|
"ttf-parser",
|
||||||
"unicode-bidi-mirroring",
|
"unicode-bidi-mirroring",
|
||||||
"unicode-ccc",
|
"unicode-ccc",
|
||||||
"unicode-general-category",
|
"unicode-general-category",
|
||||||
|
@ -2836,9 +2838,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7"
|
checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
|
@ -3337,12 +3339,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ttf-parser"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
@ -3488,7 +3484,7 @@ dependencies = [
|
||||||
"simplecss",
|
"simplecss",
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"svgtypes",
|
"svgtypes",
|
||||||
"ttf-parser 0.15.0",
|
"ttf-parser",
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-script",
|
"unicode-script",
|
||||||
"unicode-vo",
|
"unicode-vo",
|
||||||
|
|
|
@ -636,7 +636,7 @@ impl Visuals {
|
||||||
widgets: Widgets::default(),
|
widgets: Widgets::default(),
|
||||||
selection: Selection::default(),
|
selection: Selection::default(),
|
||||||
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
||||||
faint_bg_color: Color32::from_gray(24),
|
faint_bg_color: Color32::from_gray(35),
|
||||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||||
code_bg_color: Color32::from_gray(64),
|
code_bg_color: Color32::from_gray(64),
|
||||||
window_rounding: Rounding::same(6.0),
|
window_rounding: Rounding::same(6.0),
|
||||||
|
@ -658,7 +658,7 @@ impl Visuals {
|
||||||
widgets: Widgets::light(),
|
widgets: Widgets::light(),
|
||||||
selection: Selection::light(),
|
selection: Selection::light(),
|
||||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||||
faint_bg_color: Color32::from_gray(245),
|
faint_bg_color: Color32::from_gray(242),
|
||||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||||
code_bg_color: Color32::from_gray(230),
|
code_bg_color: Color32::from_gray(230),
|
||||||
window_shadow: Shadow::big_light(),
|
window_shadow: Shadow::big_light(),
|
||||||
|
|
|
@ -20,15 +20,21 @@ all-features = true
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["chrono"]
|
default = ["datetime"]
|
||||||
|
|
||||||
# Enable additional checks if debug assertions are enabled (debug builds).
|
# Enable additional checks if debug assertions are enabled (debug builds).
|
||||||
extra_debug_asserts = ["egui/extra_debug_asserts"]
|
extra_debug_asserts = ["egui/extra_debug_asserts"]
|
||||||
# Always enable additional checks.
|
# Always enable additional checks.
|
||||||
extra_asserts = ["egui/extra_asserts"]
|
extra_asserts = ["egui/extra_asserts"]
|
||||||
|
|
||||||
|
datetime = ["egui_extras/chrono", "chrono"]
|
||||||
http = ["egui_extras", "ehttp", "image", "poll-promise"]
|
http = ["egui_extras", "ehttp", "image", "poll-promise"]
|
||||||
persistence = ["egui/persistence", "epi/persistence", "serde"]
|
persistence = [
|
||||||
|
"egui/persistence",
|
||||||
|
"epi/persistence",
|
||||||
|
"egui_extras/persistence",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
serialize = ["egui/serialize", "serde"]
|
serialize = ["egui/serialize", "serde"]
|
||||||
syntax_highlighting = ["syntect"]
|
syntax_highlighting = ["syntect"]
|
||||||
|
|
||||||
|
@ -45,6 +51,7 @@ unicode_names2 = { version = "0.5.0", default-features = false }
|
||||||
# feature "http":
|
# feature "http":
|
||||||
egui_extras = { version = "0.17.0", path = "../egui_extras", optional = true, features = [
|
egui_extras = { version = "0.17.0", path = "../egui_extras", optional = true, features = [
|
||||||
"image",
|
"image",
|
||||||
|
"datepicker",
|
||||||
] }
|
] }
|
||||||
ehttp = { version = "0.2.0", optional = true }
|
ehttp = { version = "0.2.0", optional = true }
|
||||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||||
|
@ -64,7 +71,6 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.3", default-features = false }
|
criterion = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "benchmark"
|
name = "benchmark"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
|
@ -29,6 +29,8 @@ impl Default for Demos {
|
||||||
Box::new(super::plot_demo::PlotDemo::default()),
|
Box::new(super::plot_demo::PlotDemo::default()),
|
||||||
Box::new(super::scrolling::Scrolling::default()),
|
Box::new(super::scrolling::Scrolling::default()),
|
||||||
Box::new(super::sliders::Sliders::default()),
|
Box::new(super::sliders::Sliders::default()),
|
||||||
|
Box::new(super::strip_demo::StripDemo::default()),
|
||||||
|
Box::new(super::table_demo::TableDemo::default()),
|
||||||
Box::new(super::text_edit::TextEdit::default()),
|
Box::new(super::text_edit::TextEdit::default()),
|
||||||
Box::new(super::widget_gallery::WidgetGallery::default()),
|
Box::new(super::widget_gallery::WidgetGallery::default()),
|
||||||
Box::new(super::window_options::WindowOptions::default()),
|
Box::new(super::window_options::WindowOptions::default()),
|
||||||
|
|
|
@ -21,6 +21,8 @@ pub mod password;
|
||||||
pub mod plot_demo;
|
pub mod plot_demo;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
pub mod sliders;
|
pub mod sliders;
|
||||||
|
pub mod strip_demo;
|
||||||
|
pub mod table_demo;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
pub mod text_edit;
|
pub mod text_edit;
|
||||||
pub mod toggle_switch;
|
pub mod toggle_switch;
|
||||||
|
|
110
egui_demo_lib/src/apps/demo/strip_demo.rs
Normal file
110
egui_demo_lib/src/apps/demo/strip_demo.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use egui::Color32;
|
||||||
|
use egui_extras::{Size, StripBuilder};
|
||||||
|
|
||||||
|
/// Shows off a table with dynamic layout
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct StripDemo {}
|
||||||
|
|
||||||
|
impl super::Demo for StripDemo {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"▣ Strip Demo"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||||
|
egui::Window::new(self.name())
|
||||||
|
.open(open)
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(400.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
use super::View as _;
|
||||||
|
self.ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for StripDemo {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.size(Size::Absolute(50.0))
|
||||||
|
.size(Size::Remainder)
|
||||||
|
.size(Size::RelativeMinimum {
|
||||||
|
relative: 0.5,
|
||||||
|
minimum: 60.0,
|
||||||
|
})
|
||||||
|
.size(Size::Absolute(10.0))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.painter()
|
||||||
|
.rect_filled(ui.available_rect_before_wrap(), 0.0, Color32::BLUE);
|
||||||
|
ui.label("Full width and 50px height");
|
||||||
|
});
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder.sizes(Size::Remainder, 2).horizontal(|mut strip| {
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::RED,
|
||||||
|
);
|
||||||
|
ui.label("remaining height and 50% of the width");
|
||||||
|
});
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder.sizes(Size::Remainder, 3).vertical(|mut strip| {
|
||||||
|
strip.empty();
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::YELLOW,
|
||||||
|
);
|
||||||
|
ui.label("one third of the box left of me but same width");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder
|
||||||
|
.size(Size::Remainder)
|
||||||
|
.size(Size::Absolute(60.0))
|
||||||
|
.size(Size::Remainder)
|
||||||
|
.size(Size::Absolute(70.0))
|
||||||
|
.horizontal(|mut strip| {
|
||||||
|
strip.empty();
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder
|
||||||
|
.size(Size::Remainder)
|
||||||
|
.size(Size::Absolute(60.0))
|
||||||
|
.size(Size::Remainder)
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.empty();
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::GOLD,
|
||||||
|
);
|
||||||
|
ui.label("60x60");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
strip.empty();
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
0.0,
|
||||||
|
Color32::GREEN,
|
||||||
|
);
|
||||||
|
ui.label("height: half the available - at least 60px, width: 70px");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add(crate::__egui_github_link_file!());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
109
egui_demo_lib/src/apps/demo/table_demo.rs
Normal file
109
egui_demo_lib/src/apps/demo/table_demo.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use egui_extras::{Size, StripBuilder, TableBuilder};
|
||||||
|
|
||||||
|
/// Shows off a table with dynamic layout
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TableDemo {
|
||||||
|
virtual_scroll: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Demo for TableDemo {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"☰ Table Demo"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||||
|
egui::Window::new(self.name())
|
||||||
|
.open(open)
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(400.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
use super::View as _;
|
||||||
|
self.ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for TableDemo {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.checkbox(&mut self.virtual_scroll, "Virtual scroll demo");
|
||||||
|
|
||||||
|
// Leave room for the source code link after the table demo:
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.size(Size::Remainder) // for the table
|
||||||
|
.size(Size::Absolute(10.0)) // for the source code link
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.cell_clip(|ui| {
|
||||||
|
self.table_ui(ui);
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add(crate::__egui_github_link_file!());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableDemo {
|
||||||
|
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.striped(true)
|
||||||
|
.column(Size::Absolute(120.0))
|
||||||
|
.column(Size::RemainderMinimum(180.0))
|
||||||
|
.column(Size::Absolute(100.0))
|
||||||
|
.header(20.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.heading("Left");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.heading("Middle");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.heading("Right");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
if self.virtual_scroll {
|
||||||
|
body.rows(20.0, 100_000, |index, mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(index.to_string());
|
||||||
|
});
|
||||||
|
row.col_clip(|ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new("virtual scroll, easily with thousands of rows!")
|
||||||
|
.wrap(false),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(index.to_string());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for i in 0..100 {
|
||||||
|
let height = match i % 8 {
|
||||||
|
0 => 25.0,
|
||||||
|
4 => 30.0,
|
||||||
|
_ => 20.0,
|
||||||
|
};
|
||||||
|
body.row(height, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(i.to_string());
|
||||||
|
});
|
||||||
|
row.col_clip(|ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(
|
||||||
|
format!("Normal scroll, each row can have a different height. Height: {}", height),
|
||||||
|
)
|
||||||
|
.wrap(false),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(i.to_string());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
#[cfg(feature = "datetime")]
|
||||||
|
mod serde_date_format;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
enum Enum {
|
enum Enum {
|
||||||
|
@ -17,6 +20,9 @@ pub struct WidgetGallery {
|
||||||
string: String,
|
string: String,
|
||||||
color: egui::Color32,
|
color: egui::Color32,
|
||||||
animate_progress_bar: bool,
|
animate_progress_bar: bool,
|
||||||
|
#[cfg(feature = "datetime")]
|
||||||
|
#[serde(with = "serde_date_format")]
|
||||||
|
date: chrono::Date<chrono::Utc>,
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
texture: Option<egui::TextureHandle>,
|
texture: Option<egui::TextureHandle>,
|
||||||
}
|
}
|
||||||
|
@ -32,6 +38,8 @@ impl Default for WidgetGallery {
|
||||||
string: Default::default(),
|
string: Default::default(),
|
||||||
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
||||||
animate_progress_bar: false,
|
animate_progress_bar: false,
|
||||||
|
#[cfg(feature = "datetime")]
|
||||||
|
date: chrono::offset::Utc::now().date(),
|
||||||
texture: None,
|
texture: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +110,7 @@ impl WidgetGallery {
|
||||||
string,
|
string,
|
||||||
color,
|
color,
|
||||||
animate_progress_bar,
|
animate_progress_bar,
|
||||||
|
date,
|
||||||
texture,
|
texture,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -201,6 +210,13 @@ impl WidgetGallery {
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
#[cfg(feature = "datetime")]
|
||||||
|
{
|
||||||
|
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));
|
||||||
|
ui.add(egui_extras::DatePickerButton::new(date));
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
|
||||||
ui.add(doc_link_label("Separator", "separator"));
|
ui.add(doc_link_label("Separator", "separator"));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
use chrono::{Date, NaiveDate, Utc};
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
const FORMAT: &str = "%Y-%m-%d";
|
||||||
|
|
||||||
|
pub fn serialize<S>(date: &Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s = date.format(FORMAT).to_string();
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Date<Utc>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
NaiveDate::parse_from_str(&s, FORMAT)
|
||||||
|
.map(|naive_date| Date::from_utc(naive_date, Utc))
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
|
@ -100,7 +100,7 @@ fn test_egui_zero_window_size() {
|
||||||
|
|
||||||
/// Time of day as seconds since midnight. Used for clock in demo app.
|
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||||
pub(crate) fn seconds_since_midnight() -> Option<f64> {
|
pub(crate) fn seconds_since_midnight() -> Option<f64> {
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "datetime")]
|
||||||
{
|
{
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
let time = chrono::Local::now().time();
|
let time = chrono::Local::now().time();
|
||||||
|
@ -108,6 +108,6 @@ pub(crate) fn seconds_since_midnight() -> Option<f64> {
|
||||||
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64);
|
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64);
|
||||||
Some(seconds_since_midnight)
|
Some(seconds_since_midnight)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "chrono"))]
|
#[cfg(not(feature = "datetime"))]
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui_extras"
|
name = "egui_extras"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = [
|
||||||
|
"Dominik Rössler <dominik@freshx.de>",
|
||||||
|
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||||
|
"René Rössler <rene@freshx.de>",
|
||||||
|
]
|
||||||
description = "Extra functionality and widgets for the egui GUI library"
|
description = "Extra functionality and widgets for the egui GUI library"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
rust-version = "1.56"
|
||||||
|
@ -25,12 +29,20 @@ default = []
|
||||||
# Support loading svg images
|
# Support loading svg images
|
||||||
svg = ["resvg", "tiny-skia", "usvg"]
|
svg = ["resvg", "tiny-skia", "usvg"]
|
||||||
|
|
||||||
|
# Datepicker widget
|
||||||
|
datepicker = ["chrono"]
|
||||||
|
|
||||||
|
# Persistence
|
||||||
|
persistence = ["serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.17.0", path = "../egui", default-features = false }
|
egui = { version = "0.17.0", path = "../egui", default-features = false }
|
||||||
|
|
||||||
# Optional dependencies:
|
# Optional dependencies:
|
||||||
|
|
||||||
|
# Date operations needed for datepicker widget
|
||||||
|
chrono = { version = "0.4", optional = true }
|
||||||
|
|
||||||
# Add support for loading images with the `image` crate.
|
# Add support for loading images with the `image` crate.
|
||||||
# You also need to ALSO opt-in to the image formats you want to support, like so:
|
# You also need to ALSO opt-in to the image formats you want to support, like so:
|
||||||
# image = { version = "0.24", features = ["jpeg", "png"] }
|
# image = { version = "0.24", features = ["jpeg", "png"] }
|
||||||
|
@ -40,3 +52,6 @@ image = { version = "0.24", optional = true, default-features = false }
|
||||||
resvg = { version = "0.22", optional = true }
|
resvg = { version = "0.22", optional = true }
|
||||||
tiny-skia = { version = "0.6", optional = true }
|
tiny-skia = { version = "0.6", optional = true }
|
||||||
usvg = { version = "0.22", optional = true }
|
usvg = { version = "0.22", optional = true }
|
||||||
|
|
||||||
|
# feature "persistence":
|
||||||
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
34
egui_extras/src/datepicker.rs
Normal file
34
egui_extras/src/datepicker.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
|
||||||
|
}
|
132
egui_extras/src/datepicker/button.rs
Normal file
132
egui_extras/src/datepicker/button.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use super::popup::DatePickerPopup;
|
||||||
|
use chrono::{Date, Utc};
|
||||||
|
use egui::{Area, Button, Frame, Key, Order, RichText, Ui, Widget};
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub(crate) struct DatePickerButtonState {
|
||||||
|
pub picker_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DatePickerButton<'a> {
|
||||||
|
selection: &'a mut Date<Utc>,
|
||||||
|
id_source: Option<&'a str>,
|
||||||
|
combo_boxes: bool,
|
||||||
|
arrows: bool,
|
||||||
|
calendar: bool,
|
||||||
|
calendar_week: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DatePickerButton<'a> {
|
||||||
|
pub fn new(selection: &'a mut Date<Utc>) -> Self {
|
||||||
|
Self {
|
||||||
|
selection,
|
||||||
|
id_source: None,
|
||||||
|
combo_boxes: true,
|
||||||
|
arrows: true,
|
||||||
|
calendar: true,
|
||||||
|
calendar_week: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add id source.
|
||||||
|
/// Must be set if multiple date picker buttons are in the same Ui.
|
||||||
|
pub fn id_source(mut self, id_source: &'a str) -> Self {
|
||||||
|
self.id_source = Some(id_source);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show combo boxes in date picker popup. (Default: true)
|
||||||
|
pub fn combo_boxes(mut self, combo_boxes: bool) -> Self {
|
||||||
|
self.combo_boxes = combo_boxes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show arrows in date picker popup. (Default: true)
|
||||||
|
pub fn arrows(mut self, arrows: bool) -> Self {
|
||||||
|
self.arrows = arrows;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show calendar in date picker popup. (Default: true)
|
||||||
|
pub fn calendar(mut self, calendar: bool) -> Self {
|
||||||
|
self.calendar = calendar;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show calendar week in date picker popup. (Default: true)
|
||||||
|
pub fn calendar_week(mut self, week: bool) -> Self {
|
||||||
|
self.calendar_week = week;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for DatePickerButton<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> egui::Response {
|
||||||
|
let id = ui.make_persistent_id(&self.id_source);
|
||||||
|
let mut button_state = ui
|
||||||
|
.memory()
|
||||||
|
.data
|
||||||
|
.get_persisted::<DatePickerButtonState>(id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
//TODO: Better positioning
|
||||||
|
|
||||||
|
let area_response = Area::new(ui.make_persistent_id(&self.id_source))
|
||||||
|
.order(Order::Foreground)
|
||||||
|
.fixed_pos(pos)
|
||||||
|
.show(ui.ctx(), |ui| {
|
||||||
|
let frame = Frame::popup(ui.style());
|
||||||
|
frame.show(ui, |ui| {
|
||||||
|
ui.set_min_width(width);
|
||||||
|
ui.set_max_width(width);
|
||||||
|
|
||||||
|
DatePickerPopup {
|
||||||
|
selection: self.selection,
|
||||||
|
button_id: id,
|
||||||
|
combo_boxes: self.combo_boxes,
|
||||||
|
arrows: self.arrows,
|
||||||
|
calendar: self.calendar,
|
||||||
|
calendar_week: self.calendar_week,
|
||||||
|
}
|
||||||
|
.draw(ui);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.response;
|
||||||
|
|
||||||
|
if !button_response.clicked()
|
||||||
|
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
|
||||||
|
{
|
||||||
|
button_state.picker_visible = false;
|
||||||
|
ui.memory().data.insert_persisted(id, button_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button_response
|
||||||
|
}
|
||||||
|
}
|
363
egui_extras/src/datepicker/popup.rs
Normal file
363
egui_extras/src/datepicker/popup.rs
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
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, Label, Layout, RichText, Ui, Vec2};
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
struct DatePickerPopupState {
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
day: u32,
|
||||||
|
setup: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatePickerPopupState {
|
||||||
|
fn last_day_of_month(&self) -> u32 {
|
||||||
|
let date: Date<Utc> = Date::from_utc(NaiveDate::from_ymd(self.year, self.month, 1), Utc);
|
||||||
|
date.with_day(31)
|
||||||
|
.map(|_| 31)
|
||||||
|
.or_else(|| date.with_day(30).map(|_| 30))
|
||||||
|
.or_else(|| date.with_day(29).map(|_| 29))
|
||||||
|
.unwrap_or(28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DatePickerPopup<'a> {
|
||||||
|
pub selection: &'a mut Date<Utc>,
|
||||||
|
pub button_id: Id,
|
||||||
|
pub combo_boxes: bool,
|
||||||
|
pub arrows: bool,
|
||||||
|
pub calendar: bool,
|
||||||
|
pub calendar_week: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DatePickerPopup<'a> {
|
||||||
|
pub fn draw(&mut self, ui: &mut Ui) {
|
||||||
|
let id = ui.make_persistent_id("date_picker");
|
||||||
|
let today = chrono::offset::Utc::now().date();
|
||||||
|
let mut popup_state = ui
|
||||||
|
.memory()
|
||||||
|
.data
|
||||||
|
.get_persisted::<DatePickerPopupState>(id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !popup_state.setup {
|
||||||
|
popup_state.year = self.selection.year();
|
||||||
|
popup_state.month = self.selection.month();
|
||||||
|
popup_state.day = self.selection.day();
|
||||||
|
popup_state.setup = true;
|
||||||
|
ui.memory().data.insert_persisted(id, popup_state.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let weeks = month_data(popup_state.year, popup_state.month);
|
||||||
|
let mut close = false;
|
||||||
|
let height = 20.0;
|
||||||
|
let spacing = 2.0;
|
||||||
|
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.sizes(
|
||||||
|
Size::Absolute(height),
|
||||||
|
match (self.combo_boxes, self.arrows) {
|
||||||
|
(true, true) => 2,
|
||||||
|
(true, false) | (false, true) => 1,
|
||||||
|
(false, false) => 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.sizes(
|
||||||
|
Size::Absolute((spacing + height) * (weeks.len() + 1) as f32),
|
||||||
|
if self.calendar { 1 } else { 0 },
|
||||||
|
)
|
||||||
|
.size(Size::Absolute(height))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
if self.combo_boxes {
|
||||||
|
strip.strip_clip(|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()
|
||||||
|
{
|
||||||
|
ui.memory()
|
||||||
|
.data
|
||||||
|
.insert_persisted(id, popup_state.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ComboBox::from_id_source("date_picker_month")
|
||||||
|
.selected_text(popup_state.month.to_string())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for month in 1..=12 {
|
||||||
|
if ui
|
||||||
|
.selectable_value(
|
||||||
|
&mut popup_state.month,
|
||||||
|
month,
|
||||||
|
month.to_string(),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
.columns(Size::Remainder, if self.calendar_week { 8 } else { 7 })
|
||||||
|
.header(height, |mut header| {
|
||||||
|
if self.calendar_week {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.with_layout(
|
||||||
|
Layout::centered_and_justified(Direction::TopDown),
|
||||||
|
|ui| {
|
||||||
|
ui.add(Label::new("Week"));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Locale
|
||||||
|
for name in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.with_layout(
|
||||||
|
Layout::centered_and_justified(Direction::TopDown),
|
||||||
|
|ui| {
|
||||||
|
ui.add(Label::new(name));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
for week in weeks {
|
||||||
|
body.row(height, |mut row| {
|
||||||
|
if self.calendar_week {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(week.number.to_string()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for day in week.days {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.with_layout(
|
||||||
|
Layout::top_down_justified(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
//TODO: Colors from egui style
|
||||||
|
let fill_color = if popup_state.year
|
||||||
|
== day.year()
|
||||||
|
&& popup_state.month == day.month()
|
||||||
|
&& popup_state.day == day.day()
|
||||||
|
{
|
||||||
|
ui.visuals().selection.bg_fill
|
||||||
|
} else if day.weekday() == Weekday::Sat
|
||||||
|
|| day.weekday() == Weekday::Sun
|
||||||
|
{
|
||||||
|
Color32::DARK_RED
|
||||||
|
} else {
|
||||||
|
Color32::BLACK
|
||||||
|
};
|
||||||
|
let text_color = if day == today {
|
||||||
|
Color32::RED
|
||||||
|
} else if day.month() == popup_state.month {
|
||||||
|
Color32::WHITE
|
||||||
|
} else {
|
||||||
|
Color32::from_gray(80)
|
||||||
|
};
|
||||||
|
|
||||||
|
let button = Button::new(
|
||||||
|
RichText::new(day.day().to_string())
|
||||||
|
.color(text_color),
|
||||||
|
)
|
||||||
|
.fill(fill_color);
|
||||||
|
|
||||||
|
if ui.add(button).clicked() {
|
||||||
|
popup_state.year = day.year();
|
||||||
|
popup_state.month = day.month();
|
||||||
|
popup_state.day = day.day();
|
||||||
|
ui.memory().data.insert_persisted(
|
||||||
|
id,
|
||||||
|
popup_state.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
162
egui_extras/src/layout.rs
Normal file
162
egui_extras/src/layout.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
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> {
|
||||||
|
ui: &'l mut Ui,
|
||||||
|
direction: CellDirection,
|
||||||
|
rect: Rect,
|
||||||
|
pos: Pos2,
|
||||||
|
max: Pos2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'l> StripLayout<'l> {
|
||||||
|
pub(crate) fn new(ui: &'l mut Ui, direction: CellDirection) -> Self {
|
||||||
|
let rect = ui.available_rect_before_wrap();
|
||||||
|
let pos = rect.left_top();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
rect,
|
||||||
|
pos,
|
||||||
|
max: pos,
|
||||||
|
direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_y(&self) -> f32 {
|
||||||
|
self.rect.top()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
|
||||||
|
Rect {
|
||||||
|
min: self.pos,
|
||||||
|
max: Pos2 {
|
||||||
|
x: match width {
|
||||||
|
CellSize::Absolute(width) => self.pos.x + width,
|
||||||
|
CellSize::Remainder => self.rect.right() - self.ui.spacing().item_spacing.x,
|
||||||
|
},
|
||||||
|
y: match height {
|
||||||
|
CellSize::Absolute(height) => self.pos.y + height,
|
||||||
|
CellSize::Remainder => self.rect.bottom() - self.ui.spacing().item_spacing.y,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pos(&mut self, rect: Rect) {
|
||||||
|
match self.direction {
|
||||||
|
CellDirection::Horizontal => {
|
||||||
|
self.pos.x = rect.right() + self.ui.spacing().item_spacing.x;
|
||||||
|
}
|
||||||
|
CellDirection::Vertical => {
|
||||||
|
self.pos.y = rect.bottom() + self.ui.spacing().item_spacing.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.max.x = self
|
||||||
|
.max
|
||||||
|
.x
|
||||||
|
.max(rect.right() + self.ui.spacing().item_spacing.x);
|
||||||
|
self.max.y = self
|
||||||
|
.max
|
||||||
|
.y
|
||||||
|
.max(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,
|
||||||
|
clip: bool,
|
||||||
|
add_contents: impl FnOnce(&mut Ui),
|
||||||
|
) -> Response {
|
||||||
|
let rect = self.cell_rect(&width, &height);
|
||||||
|
self.cell(rect, clip, add_contents);
|
||||||
|
self.set_pos(rect);
|
||||||
|
|
||||||
|
self.ui.allocate_rect(rect, Sense::click())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_striped(
|
||||||
|
&mut self,
|
||||||
|
width: CellSize,
|
||||||
|
height: CellSize,
|
||||||
|
clip: bool,
|
||||||
|
add_contents: impl FnOnce(&mut Ui),
|
||||||
|
) -> Response {
|
||||||
|
let mut rect = self.cell_rect(&width, &height);
|
||||||
|
// Make sure we don't have a gap in the stripe background
|
||||||
|
*rect.top_mut() -= self.ui.spacing().item_spacing.y;
|
||||||
|
*rect.left_mut() -= self.ui.spacing().item_spacing.x;
|
||||||
|
|
||||||
|
self.ui
|
||||||
|
.painter()
|
||||||
|
.rect_filled(rect, 0.0, self.ui.visuals().faint_bg_color);
|
||||||
|
|
||||||
|
self.add(width, height, clip, add_contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// only needed for layouts with multiple lines, like Table
|
||||||
|
pub fn end_line(&mut self) {
|
||||||
|
match self.direction {
|
||||||
|
CellDirection::Horizontal => {
|
||||||
|
self.pos.y = self.max.y;
|
||||||
|
self.pos.x = self.rect.left();
|
||||||
|
}
|
||||||
|
CellDirection::Vertical => {
|
||||||
|
self.pos.x = self.max.x;
|
||||||
|
self.pos.y = self.rect.top();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
|
let mut child_ui = self.ui.child_ui(rect, *self.ui.layout());
|
||||||
|
|
||||||
|
if clip {
|
||||||
|
let mut clip_rect = child_ui.clip_rect();
|
||||||
|
clip_rect.min = clip_rect.min.max(rect.min);
|
||||||
|
clip_rect.max = clip_rect.max.min(rect.max);
|
||||||
|
child_ui.set_clip_rect(clip_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_contents(&mut child_ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,20 @@
|
||||||
#![allow(clippy::float_cmp)]
|
#![allow(clippy::float_cmp)]
|
||||||
#![allow(clippy::manual_range_contains)]
|
#![allow(clippy::manual_range_contains)]
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
mod datepicker;
|
||||||
|
|
||||||
pub mod image;
|
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 use crate::image::RetainedImage;
|
||||||
|
pub(crate) use crate::layout::StripLayout;
|
||||||
|
pub use crate::sizing::Size;
|
||||||
|
pub use crate::strip::*;
|
||||||
|
pub use crate::table::*;
|
||||||
|
|
120
egui_extras/src/sizing.rs
Normal file
120
egui_extras/src/sizing.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/// Size hint for table column/strip cell
|
||||||
|
#[derive(Clone, Debug, Copy)]
|
||||||
|
pub enum Size {
|
||||||
|
/// Absolute size in points
|
||||||
|
Absolute(f32),
|
||||||
|
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
|
||||||
|
Relative(f32),
|
||||||
|
/// [`Size::Relative`] with a minimum size in points
|
||||||
|
RelativeMinimum {
|
||||||
|
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
|
||||||
|
relative: f32,
|
||||||
|
/// Absolute minimum size in points
|
||||||
|
minimum: f32,
|
||||||
|
},
|
||||||
|
/// Multiple remainders each get the same space
|
||||||
|
Remainder,
|
||||||
|
/// [`Size::Remainder`] with a minimum size in points
|
||||||
|
RemainderMinimum(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Sizing {
|
||||||
|
sizes: Vec<Size>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sizing {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { sizes: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, size: Size) {
|
||||||
|
self.sizes.push(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_lengths(self, length: f32, spacing: f32) -> Vec<f32> {
|
||||||
|
let mut remainders = 0;
|
||||||
|
let sum_non_remainder = self
|
||||||
|
.sizes
|
||||||
|
.iter()
|
||||||
|
.map(|size| match size {
|
||||||
|
Size::Absolute(absolute) => *absolute,
|
||||||
|
Size::Relative(relative) => {
|
||||||
|
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
|
||||||
|
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
|
||||||
|
length * relative
|
||||||
|
}
|
||||||
|
Size::RelativeMinimum { relative, minimum } => {
|
||||||
|
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
|
||||||
|
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
|
||||||
|
minimum.max(length * relative)
|
||||||
|
}
|
||||||
|
Size::Remainder | Size::RemainderMinimum(..) => {
|
||||||
|
remainders += 1;
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum::<f32>()
|
||||||
|
+ 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::RemainderMinimum(minimum) = size {
|
||||||
|
if *minimum > avg_remainder_length {
|
||||||
|
remainder_length -= minimum;
|
||||||
|
remainders -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if remainders > 0 {
|
||||||
|
0.0f32.max(remainder_length / remainders as f32)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sizes
|
||||||
|
.into_iter()
|
||||||
|
.map(|size| match size {
|
||||||
|
Size::Absolute(absolute) => absolute,
|
||||||
|
Size::Relative(relative) => length * relative,
|
||||||
|
Size::RelativeMinimum { relative, minimum } => minimum.max(length * relative),
|
||||||
|
Size::Remainder => avg_remainder_length,
|
||||||
|
Size::RemainderMinimum(minimum) => minimum.max(avg_remainder_length),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Size>> for Sizing {
|
||||||
|
fn from(sizes: Vec<Size>) -> Self {
|
||||||
|
Self { sizes }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sizing() {
|
||||||
|
let sizing: Sizing = vec![Size::RemainderMinimum(20.0), Size::Remainder].into();
|
||||||
|
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
||||||
|
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![20.0, 10.0]);
|
||||||
|
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![20.0, 0.0]);
|
||||||
|
assert_eq!(sizing.clone().into_lengths(10.0, 0.0), vec![20.0, 0.0]);
|
||||||
|
assert_eq!(sizing.into_lengths(20.0, 10.0), vec![20.0, 0.0]);
|
||||||
|
|
||||||
|
let sizing: Sizing = vec![
|
||||||
|
Size::RelativeMinimum {
|
||||||
|
relative: 0.5,
|
||||||
|
minimum: 10.0,
|
||||||
|
},
|
||||||
|
Size::Absolute(10.0),
|
||||||
|
]
|
||||||
|
.into();
|
||||||
|
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 10.0]);
|
||||||
|
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![15.0, 10.0]);
|
||||||
|
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![10.0, 10.0]);
|
||||||
|
assert_eq!(sizing.into_lengths(10.0, 0.0), vec![10.0, 10.0]);
|
||||||
|
}
|
183
egui_extras/src/strip.rs
Normal file
183
egui_extras/src/strip.rs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
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!
|
||||||
|
///
|
||||||
|
/// After adding size hints with `[Self::column]`/`[Self::columns]` the strip can be build with `[Self::horizontal]`/`[Self::vertical]`.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// use egui_extras::{StripBuilder, Size};
|
||||||
|
/// StripBuilder::new(ui)
|
||||||
|
/// .size(Size::RemainderMinimum(100.0))
|
||||||
|
/// .size(Size::Absolute(40.0))
|
||||||
|
/// .vertical(|mut strip| {
|
||||||
|
/// 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");
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// strip.cell(|ui| {
|
||||||
|
/// ui.label("Fixed");
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
pub struct StripBuilder<'a> {
|
||||||
|
ui: &'a mut Ui,
|
||||||
|
sizing: Sizing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> StripBuilder<'a> {
|
||||||
|
/// Create new strip builder.
|
||||||
|
pub fn new(ui: &'a mut Ui) -> Self {
|
||||||
|
let sizing = Sizing::new();
|
||||||
|
|
||||||
|
Self { ui, sizing }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add size hint for one column/row.
|
||||||
|
pub fn size(mut self, size: Size) -> Self {
|
||||||
|
self.sizing.add(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add size hint 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.into_lengths(
|
||||||
|
self.ui.available_rect_before_wrap().width() - self.ui.spacing().item_spacing.x,
|
||||||
|
self.ui.spacing().item_spacing.x,
|
||||||
|
);
|
||||||
|
let mut layout = StripLayout::new(self.ui, CellDirection::Horizontal);
|
||||||
|
strip(Strip {
|
||||||
|
layout: &mut layout,
|
||||||
|
direction: CellDirection::Horizontal,
|
||||||
|
sizes: &widths,
|
||||||
|
});
|
||||||
|
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.into_lengths(
|
||||||
|
self.ui.available_rect_before_wrap().height() - self.ui.spacing().item_spacing.y,
|
||||||
|
self.ui.spacing().item_spacing.y,
|
||||||
|
);
|
||||||
|
let mut layout = StripLayout::new(self.ui, CellDirection::Vertical);
|
||||||
|
strip(Strip {
|
||||||
|
layout: &mut layout,
|
||||||
|
direction: CellDirection::Vertical,
|
||||||
|
sizes: &heights,
|
||||||
|
});
|
||||||
|
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: &'b [f32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Strip<'a, 'b> {
|
||||||
|
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
|
||||||
|
let size = self.sizes[0];
|
||||||
|
self.sizes = &self.sizes[1..];
|
||||||
|
|
||||||
|
match self.direction {
|
||||||
|
CellDirection::Horizontal => (CellSize::Absolute(size), CellSize::Remainder),
|
||||||
|
CellDirection::Vertical => (CellSize::Remainder, CellSize::Absolute(size)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add empty cell
|
||||||
|
pub fn empty(&mut self) {
|
||||||
|
assert!(
|
||||||
|
!self.sizes.is_empty(),
|
||||||
|
"Tried using more strip cells than available."
|
||||||
|
);
|
||||||
|
|
||||||
|
let (width, height) = self.next_cell_size();
|
||||||
|
self.layout.empty(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_impl(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
|
assert!(
|
||||||
|
!self.sizes.is_empty(),
|
||||||
|
"Tried using more strip cells than available."
|
||||||
|
);
|
||||||
|
|
||||||
|
let (width, height) = self.next_cell_size();
|
||||||
|
self.layout.add(width, height, clip, add_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add cell, content is wrapped
|
||||||
|
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
|
self.cell_impl(false, add_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add cell, content is clipped
|
||||||
|
pub fn cell_clip(&mut self, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
|
self.cell_impl(true, add_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_impl(&mut self, clip: bool, strip_builder: impl FnOnce(StripBuilder<'_>)) {
|
||||||
|
self.cell_impl(clip, |ui| {
|
||||||
|
strip_builder(StripBuilder::new(ui));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add strip as cell
|
||||||
|
pub fn strip(&mut self, strip_builder: impl FnOnce(StripBuilder<'_>)) {
|
||||||
|
self.strip_impl(false, strip_builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add strip as cell, content is clipped
|
||||||
|
pub fn strip_clip(&mut self, strip_builder: impl FnOnce(StripBuilder<'_>)) {
|
||||||
|
self.strip_impl(true, strip_builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Drop for Strip<'a, 'b> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
while !self.sizes.is_empty() {
|
||||||
|
self.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
335
egui_extras/src/table.rs
Normal file
335
egui_extras/src/table.rs
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
//! 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::{Response, Ui};
|
||||||
|
use std::cmp;
|
||||||
|
|
||||||
|
/// Builder for a [`Table`] 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 |
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// use egui_extras::{TableBuilder, Size};
|
||||||
|
/// TableBuilder::new(ui)
|
||||||
|
/// .column(Size::RemainderMinimum(100.0))
|
||||||
|
/// .column(Size::Absolute(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_clip(|ui| {
|
||||||
|
/// ui.button("action");
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
pub struct TableBuilder<'a> {
|
||||||
|
ui: &'a mut Ui,
|
||||||
|
sizing: Sizing,
|
||||||
|
scroll: bool,
|
||||||
|
striped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TableBuilder<'a> {
|
||||||
|
pub fn new(ui: &'a mut Ui) -> Self {
|
||||||
|
let sizing = Sizing::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
sizing,
|
||||||
|
scroll: true,
|
||||||
|
striped: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable scrollview in body (default: true)
|
||||||
|
pub fn scroll(mut self, scroll: bool) -> Self {
|
||||||
|
self.scroll = scroll;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable striped row background (default: false)
|
||||||
|
pub fn striped(mut self, striped: bool) -> Self {
|
||||||
|
self.striped = striped;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add size hint for column
|
||||||
|
pub fn column(mut self, width: Size) -> Self {
|
||||||
|
self.sizing.add(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add size hint 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()
|
||||||
|
- 2.0 * self.ui.spacing().item_spacing.x
|
||||||
|
- if self.scroll {
|
||||||
|
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 widths = self
|
||||||
|
.sizing
|
||||||
|
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
|
||||||
|
let ui = self.ui;
|
||||||
|
{
|
||||||
|
let mut layout = StripLayout::new(ui, CellDirection::Horizontal);
|
||||||
|
header(TableRow {
|
||||||
|
layout: &mut layout,
|
||||||
|
widths: &widths,
|
||||||
|
striped: false,
|
||||||
|
height,
|
||||||
|
clicked: false,
|
||||||
|
});
|
||||||
|
layout.allocate_rect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Table {
|
||||||
|
ui,
|
||||||
|
widths,
|
||||||
|
scroll: self.scroll,
|
||||||
|
striped: self.striped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create table body without a header row
|
||||||
|
pub fn body<F>(self, body: F)
|
||||||
|
where
|
||||||
|
F: for<'b> FnOnce(TableBody<'b>),
|
||||||
|
{
|
||||||
|
let available_width = self.available_width();
|
||||||
|
let widths = self
|
||||||
|
.sizing
|
||||||
|
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
|
||||||
|
|
||||||
|
Table {
|
||||||
|
ui: self.ui,
|
||||||
|
widths,
|
||||||
|
scroll: self.scroll,
|
||||||
|
striped: self.striped,
|
||||||
|
}
|
||||||
|
.body(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
widths: Vec<f32>,
|
||||||
|
scroll: bool,
|
||||||
|
striped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Table<'a> {
|
||||||
|
/// Create table body after adding a header row
|
||||||
|
pub fn body<F>(self, body: F)
|
||||||
|
where
|
||||||
|
F: for<'b> FnOnce(TableBody<'b>),
|
||||||
|
{
|
||||||
|
let Table {
|
||||||
|
ui,
|
||||||
|
widths,
|
||||||
|
scroll,
|
||||||
|
striped,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let start_y = ui.available_rect_before_wrap().top();
|
||||||
|
let end_y = ui.available_rect_before_wrap().bottom();
|
||||||
|
|
||||||
|
egui::ScrollArea::new([false, scroll]).show(ui, move |ui| {
|
||||||
|
let layout = StripLayout::new(ui, CellDirection::Horizontal);
|
||||||
|
|
||||||
|
body(TableBody {
|
||||||
|
layout,
|
||||||
|
widths,
|
||||||
|
striped,
|
||||||
|
row_nr: 0,
|
||||||
|
start_y,
|
||||||
|
end_y,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
/// Add rows with same height.
|
||||||
|
///
|
||||||
|
/// Is a lot more performant than adding each individual row as non visible rows must not be rendered
|
||||||
|
pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow<'_, '_>)) {
|
||||||
|
let delta = self.layout.current_y() - self.start_y;
|
||||||
|
let mut start = 0;
|
||||||
|
|
||||||
|
if delta < 0.0 {
|
||||||
|
start = (-delta / height).floor() as usize;
|
||||||
|
|
||||||
|
let skip_height = start as f32 * height;
|
||||||
|
TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: false,
|
||||||
|
height: skip_height,
|
||||||
|
clicked: false,
|
||||||
|
}
|
||||||
|
.col(|_| ()); // advances the cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_height = self.end_y - self.start_y;
|
||||||
|
let count = (max_height / height).ceil() as usize;
|
||||||
|
let end = cmp::min(start + count, rows);
|
||||||
|
|
||||||
|
for idx in start..end {
|
||||||
|
row(
|
||||||
|
idx,
|
||||||
|
TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: self.striped && idx % 2 == 0,
|
||||||
|
height,
|
||||||
|
clicked: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows - end > 0 {
|
||||||
|
let skip_height = (rows - end) as f32 * height;
|
||||||
|
|
||||||
|
TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: false,
|
||||||
|
height: skip_height,
|
||||||
|
clicked: false,
|
||||||
|
}
|
||||||
|
.col(|_| ()); // advances the cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add row with individual height
|
||||||
|
pub fn row(&mut self, height: f32, row: impl FnOnce(TableRow<'a, '_>)) {
|
||||||
|
row(TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: self.striped && self.row_nr % 2 == 0,
|
||||||
|
height,
|
||||||
|
clicked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.row_nr += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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],
|
||||||
|
striped: bool,
|
||||||
|
height: f32,
|
||||||
|
clicked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> TableRow<'a, 'b> {
|
||||||
|
/// Check if row was clicked
|
||||||
|
pub fn clicked(&self) -> bool {
|
||||||
|
self.clicked
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add column, content is wrapped
|
||||||
|
pub fn col(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
|
||||||
|
self.column(false, add_contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add column, content is clipped
|
||||||
|
pub fn col_clip(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
|
||||||
|
self.column(true, add_contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) -> Response {
|
||||||
|
assert!(
|
||||||
|
!self.widths.is_empty(),
|
||||||
|
"Tried using more table columns than available."
|
||||||
|
);
|
||||||
|
|
||||||
|
let width = self.widths[0];
|
||||||
|
self.widths = &self.widths[1..];
|
||||||
|
let width = CellSize::Absolute(width);
|
||||||
|
let height = CellSize::Absolute(self.height);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if self.striped {
|
||||||
|
response = self.layout.add_striped(width, height, clip, add_contents);
|
||||||
|
} else {
|
||||||
|
response = self.layout.add(width, height, clip, add_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
self.clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Drop for TableRow<'a, 'b> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.layout.end_line();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue