mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Merge branch 'main' into common-panels
This commit is contained in:
@@ -5,19 +5,13 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Example library for egui"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib"
|
||||
categories = ["gui", "graphics"]
|
||||
keywords = ["glow", "egui", "gui", "gamedev"]
|
||||
include = [
|
||||
"../LICENSE-APACHE",
|
||||
"../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
"data/icon.png",
|
||||
]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -43,12 +37,12 @@ syntect = ["egui_extras/syntect"]
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false, features = ["color-hex"] }
|
||||
egui_extras = { workspace = true, features = ["default"] }
|
||||
egui_extras = { workspace = true, features = ["image", "svg"] }
|
||||
|
||||
unicode_names2 = { version = "0.6.0", default-features = false } # this old version has fewer dependencies
|
||||
unicode_names2.workspace = true # this old version has fewer dependencies
|
||||
|
||||
#! ### Optional dependencies
|
||||
chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] }
|
||||
chrono = { workspace = true, optional = true, features = ["js-sys", "wasmbind"] }
|
||||
## Enable this when generating docs.
|
||||
document-features = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
@@ -56,8 +50,12 @@ serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||
egui = { workspace = true, features = ["default_fonts"] }
|
||||
egui_extras = { workspace = true, features = ["image", "svg"] }
|
||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||
image = { workspace = true, features = ["png"] }
|
||||
mimalloc.workspace = true # for benchmarks
|
||||
rand.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
|
||||
@@ -14,3 +14,19 @@ The demo library is a separate crate for three reasons:
|
||||
* To remove the amount of code in `egui` proper.
|
||||
* To make it easy for 3rd party egui integrations to use it for tests.
|
||||
- See for instance https://github.com/not-fl3/egui-miniquad/blob/master/examples/demo.rs
|
||||
|
||||
This crate also contains benchmarks for egui.
|
||||
Run them with
|
||||
```bash
|
||||
# Run all benchmarks
|
||||
cargo bench -p egui_demo_lib
|
||||
|
||||
# Run a single benchmark
|
||||
cargo bench -p egui_demo_lib "benchmark name"
|
||||
|
||||
# Profile benchmarks with cargo-flamegraph (--root flag is necessary for MacOS)
|
||||
CARGO_PROFILE_BENCH_DEBUG=true cargo flamegraph --bench benchmark --root -p egui_demo_lib -- --bench "benchmark name"
|
||||
|
||||
# Profile with cargo-instruments
|
||||
CARGO_PROFILE_BENCH_DEBUG=true cargo instruments --profile bench --bench benchmark -p egui_demo_lib -t time -- --bench "benchmark name"
|
||||
```
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||
|
||||
use egui::epaint::TextShape;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Button, Id, RichText, TextureId, Ui, UiBuilder, Vec2};
|
||||
use egui_demo_lib::LOREM_IPSUM_LONG;
|
||||
use rand::Rng as _;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator
|
||||
|
||||
/// Each iteration should be called in their own `Ui` with an intentional id clash,
|
||||
/// to prevent the Context from building a massive map of `WidgetRects` (which would slow the test,
|
||||
/// causing unreliable results).
|
||||
fn create_benchmark_ui(ctx: &egui::Context) -> Ui {
|
||||
Ui::new(ctx.clone(), Id::new("clashing_id"), UiBuilder::new())
|
||||
}
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
use egui::RawInput;
|
||||
@@ -52,17 +67,71 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let _ = ctx.run(RawInput::default(), |ctx| {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
c.bench_function("label &str", |b| {
|
||||
b.iter(|| {
|
||||
c.bench_function("label &str", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog");
|
||||
});
|
||||
});
|
||||
c.bench_function("label format!", |b| {
|
||||
b.iter(|| {
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("label format!", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog".to_owned());
|
||||
});
|
||||
});
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let _ = ctx.run(RawInput::default(), |ctx| {
|
||||
let mut group = c.benchmark_group("button");
|
||||
|
||||
// To ensure we have a valid image, let's use the font texture. The size
|
||||
// shouldn't be important for this benchmark.
|
||||
let image = SizedTexture::new(TextureId::default(), Vec2::splat(16.0));
|
||||
|
||||
group.bench_function("1_button_text", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.add(Button::new("Hello World"));
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
group.bench_function("2_button_text_image", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.add(Button::image_and_text(image, "Hello World"));
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
group.bench_function("3_button_text_image_right_text", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.add(Button::image_and_text(image, "Hello World").right_text("⏵"));
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
group.bench_function("4_button_italic", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| create_benchmark_ui(ctx),
|
||||
|ui| {
|
||||
ui.add(Button::new(RichText::new("Hello World").italics()));
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -96,16 +165,15 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let wrap_width = 512.0;
|
||||
let font_id = egui::FontId::default();
|
||||
let text_color = egui::Color32::WHITE;
|
||||
let fonts = egui::epaint::text::Fonts::new(
|
||||
pixels_per_point,
|
||||
let mut fonts = egui::epaint::text::Fonts::new(
|
||||
max_texture_side,
|
||||
egui::epaint::AlphaFromCoverage::default(),
|
||||
egui::FontDefinitions::default(),
|
||||
);
|
||||
{
|
||||
let mut locked_fonts = fonts.lock();
|
||||
c.bench_function("text_layout_uncached", |b| {
|
||||
b.iter(|| {
|
||||
use egui::epaint::text::{layout, LayoutJob};
|
||||
use egui::epaint::text::{LayoutJob, layout};
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
@@ -113,13 +181,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
text_color,
|
||||
wrap_width,
|
||||
);
|
||||
layout(&mut locked_fonts.fonts, job.into())
|
||||
layout(&mut fonts.fonts, pixels_per_point, job.into())
|
||||
});
|
||||
});
|
||||
}
|
||||
c.bench_function("text_layout_cached", |b| {
|
||||
b.iter(|| {
|
||||
fonts.layout(
|
||||
fonts.with_pixels_per_point(pixels_per_point).layout(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id.clone(),
|
||||
text_color,
|
||||
@@ -128,9 +196,43 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
});
|
||||
});
|
||||
|
||||
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width);
|
||||
c.bench_function("text_layout_cached_many_lines_modified", |b| {
|
||||
const NUM_LINES: usize = 2_000;
|
||||
|
||||
let mut string = String::new();
|
||||
for _ in 0..NUM_LINES {
|
||||
for i in 0..30_u8 {
|
||||
write!(string, "{i:02X} ").unwrap();
|
||||
}
|
||||
string.push('\n');
|
||||
}
|
||||
|
||||
let mut rng = rand::rng();
|
||||
b.iter(|| {
|
||||
fonts.begin_pass(max_texture_side, egui::epaint::AlphaFromCoverage::default());
|
||||
|
||||
// Delete a random character, simulating a user making an edit in a long file:
|
||||
let mut new_string = string.clone();
|
||||
let idx = rng.random_range(0..string.len());
|
||||
new_string.remove(idx);
|
||||
|
||||
fonts.with_pixels_per_point(pixels_per_point).layout(
|
||||
new_string,
|
||||
font_id.clone(),
|
||||
text_color,
|
||||
wrap_width,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let galley = fonts.with_pixels_per_point(pixels_per_point).layout(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id,
|
||||
text_color,
|
||||
wrap_width,
|
||||
);
|
||||
let font_image_size = fonts.font_image_size();
|
||||
let prepared_discs = fonts.texture_atlas().lock().prepared_discs();
|
||||
let prepared_discs = fonts.texture_atlas().prepared_discs();
|
||||
let mut tessellator = egui::epaint::Tessellator::new(
|
||||
1.0,
|
||||
Default::default(),
|
||||
|
||||
11
crates/egui_demo_lib/data/peace.svg
Normal file
11
crates/egui_demo_lib/data/peace.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 44 44" width="32" height="32">
|
||||
<g fill="none" stroke="white" stroke-width="3">
|
||||
<circle cx="22" cy="22" r="19"/>
|
||||
<path d="M 22,2 V 41"/>
|
||||
<path d="M 22,2 V 22" transform="rotate(135, 22, 22)"/>
|
||||
<path d="M 22,2 V 22" transform="rotate(225, 22, 22)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
BIN
crates/egui_demo_lib/data/ring.png
Normal file
BIN
crates/egui_demo_lib/data/ring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 507 B |
@@ -76,16 +76,16 @@ impl crate::View for CodeEditor {
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = egui_extras::syntax_highlighting::highlight(
|
||||
ui.ctx(),
|
||||
ui.style(),
|
||||
&theme,
|
||||
string,
|
||||
buf.as_str(),
|
||||
language,
|
||||
);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
|
||||
@@ -62,6 +62,7 @@ impl CodeExample {
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[expect(clippy::literal_string_with_formatting_args)]
|
||||
show_code(ui, r#"ui.label(format!("{name} is {age}"));"#);
|
||||
ui.label(format!("{name} is {age}"));
|
||||
ui.end_row();
|
||||
@@ -84,7 +85,7 @@ impl CodeExample {
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 2.0 * 4.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
|
||||
let indentation = 2.0 * 4.0 * ui.fonts_mut(|f| f.glyph_width(&font_id, ' '));
|
||||
ui.add_space(indentation);
|
||||
|
||||
egui::Grid::new("code_samples")
|
||||
@@ -105,7 +106,7 @@ impl crate::Demo for CodeExample {
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use crate::View;
|
||||
use crate::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.min_width(375.0)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {}
|
||||
|
||||
impl crate::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use crate::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
|
||||
if ui.ctx().is_context_menu_open() {
|
||||
ui.label("Context menu is open");
|
||||
} else {
|
||||
ui.label("Context menu is closed");
|
||||
}
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use egui::{
|
||||
Color32, Context, Pos2, Rect, Ui,
|
||||
containers::{Frame, Window},
|
||||
emath, epaint,
|
||||
epaint::PathStroke,
|
||||
hex_color, lerp, pos2, remap, vec2, Color32, Context, Pos2, Rect, Ui,
|
||||
hex_color, lerp, pos2, remap, vec2,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
|
||||
use super::About;
|
||||
use crate::is_mobile;
|
||||
use crate::Demo;
|
||||
use crate::View;
|
||||
|
||||
use crate::View as _;
|
||||
use crate::is_mobile;
|
||||
use egui::containers::menu;
|
||||
use egui::style::StyleModifier;
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct DemoGroup {
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
}
|
||||
|
||||
impl std::ops::Add for DemoGroup {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
let mut demos = self.demos;
|
||||
demos.extend(other.demos);
|
||||
Self { demos }
|
||||
}
|
||||
}
|
||||
|
||||
impl DemoGroup {
|
||||
pub fn new(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
Self { demos }
|
||||
@@ -65,7 +75,6 @@ impl Default for DemoGroups {
|
||||
Box::<super::paint_bezier::PaintBezier>::default(),
|
||||
Box::<super::code_editor::CodeEditor>::default(),
|
||||
Box::<super::code_example::CodeExample>::default(),
|
||||
Box::<super::context_menu::ContextMenus>::default(),
|
||||
Box::<super::dancing_strings::DancingStrings>::default(),
|
||||
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
||||
Box::<super::extra_viewport::ExtraViewport>::default(),
|
||||
@@ -78,6 +87,7 @@ impl Default for DemoGroups {
|
||||
Box::<super::multi_touch::MultiTouch>::default(),
|
||||
Box::<super::painting::Painting>::default(),
|
||||
Box::<super::panels::Panels>::default(),
|
||||
Box::<super::popups::PopupsDemo>::default(),
|
||||
Box::<super::scene::SceneDemo>::default(),
|
||||
Box::<super::screenshot::Screenshot>::default(),
|
||||
Box::<super::scrolling::Scrolling>::default(),
|
||||
@@ -100,6 +110,7 @@ impl Default for DemoGroups {
|
||||
Box::<super::tests::InputTest>::default(),
|
||||
Box::<super::tests::LayoutTest>::default(),
|
||||
Box::<super::tests::ManualLayoutTest>::default(),
|
||||
Box::<super::tests::SvgTest>::default(),
|
||||
Box::<super::tests::TessellationTest>::default(),
|
||||
Box::<super::tests::WindowResizeTest>::default(),
|
||||
]),
|
||||
@@ -225,38 +236,36 @@ impl DemoWindows {
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::Panel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
menu::MenuBar::new()
|
||||
.config(menu::MenuConfig::new().style(StyleModifier::default()))
|
||||
.ui(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::Panel::right("egui_demo_panel")
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_size(160.0)
|
||||
.min_size(160.0)
|
||||
.default_width(160.0)
|
||||
.min_width(160.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
@@ -280,8 +289,8 @@ impl DemoWindows {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::Panel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
menu::MenuBar::new().ui(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
@@ -344,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
@@ -356,21 +364,25 @@ fn file_menu_button(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{demo::demo_app_windows::DemoGroups, Demo};
|
||||
use egui::Vec2;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
|
||||
use crate::{Demo as _, demo::demo_app_windows::DemoGroups};
|
||||
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
fn demos_should_match_snapshot() {
|
||||
let demos = DemoGroups::default().demos;
|
||||
let DemoGroups {
|
||||
demos,
|
||||
tests,
|
||||
about: _,
|
||||
} = DemoGroups::default();
|
||||
let demos = demos + tests;
|
||||
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
@@ -380,33 +392,41 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove the emoji from the demo name
|
||||
let name = demo
|
||||
.name()
|
||||
.split_once(' ')
|
||||
.map_or(demo.name(), |(_, name)| name);
|
||||
let name = remove_leading_emoji(demo.name());
|
||||
|
||||
let mut harness = Harness::new(|ctx| {
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
demo.show(ctx, &mut true);
|
||||
});
|
||||
|
||||
let window = harness.node().children().next().unwrap();
|
||||
let window = harness.queryable_node().children().next().unwrap();
|
||||
// TODO(lucasmerlin): Windows should probably have a label?
|
||||
//let window = harness.get_by_label(name);
|
||||
|
||||
let size = window.raw_bounds().expect("window bounds").size();
|
||||
harness.set_size(Vec2::new(size.width as f32, size.height as f32));
|
||||
let size = window.rect().size();
|
||||
harness.set_size(size);
|
||||
|
||||
// Run the app for some more frames...
|
||||
harness.run_ok();
|
||||
|
||||
let mut options = SnapshotOptions::default();
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux
|
||||
|
||||
if name == "Bézier Curve" {
|
||||
options.threshold = 2.1;
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux:
|
||||
options = options.threshold(OsThreshold::new(0.0).linux(2.1));
|
||||
}
|
||||
|
||||
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
|
||||
results.add(harness.try_snapshot_options(format!("demos/{name}"), &options));
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_leading_emoji(full_name: &str) -> &str {
|
||||
if let Some((start, name)) = full_name.split_once(' ')
|
||||
&& start.len() <= 4
|
||||
&& start.bytes().next().is_some_and(|byte| byte >= 128)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
full_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{vec2, Color32, Context, Frame, Id, Ui, Window};
|
||||
use egui::{Color32, Context, Frame, Id, Ui, Window, vec2};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
|
||||
@@ -54,7 +54,11 @@ fn viewport_content(ui: &mut egui::Ui, ctx: &egui::Context, open: &mut bool) {
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let viewports = ui.input(|i| i.raw.viewports.clone());
|
||||
for (id, viewport) in viewports {
|
||||
let ordered_viewports = viewports
|
||||
.iter()
|
||||
.map(|(id, viewport)| (*id, viewport.clone()))
|
||||
.collect::<egui::OrderedViewportIdMap<_>>();
|
||||
for (id, viewport) in ordered_viewports {
|
||||
ui.group(|ui| {
|
||||
ui.label(format!("viewport {id:?}"));
|
||||
ui.push_id(id, |ui| {
|
||||
|
||||
@@ -77,7 +77,7 @@ impl crate::View for FontBook {
|
||||
let available_glyphs = self
|
||||
.available_glyphs
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
.or_insert_with(|| available_characters(ui, &self.font_id.family));
|
||||
|
||||
ui.separator();
|
||||
|
||||
@@ -140,11 +140,10 @@ fn char_info_ui(ui: &mut egui::Ui, chr: char, glyph_info: &GlyphInfo, font_id: e
|
||||
});
|
||||
}
|
||||
|
||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, GlyphInfo> {
|
||||
ui.fonts(|f| {
|
||||
f.lock()
|
||||
.fonts
|
||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||
fn available_characters(ui: &egui::Ui, family: &egui::FontFamily) -> BTreeMap<char, GlyphInfo> {
|
||||
ui.fonts_mut(|f| {
|
||||
f.fonts
|
||||
.font(family)
|
||||
.characters()
|
||||
.iter()
|
||||
.filter(|(chr, _fonts)| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||
@@ -169,7 +168,7 @@ fn char_name(chr: char) -> String {
|
||||
}
|
||||
|
||||
fn special_char_name(chr: char) -> Option<&'static str> {
|
||||
#[allow(clippy::match_same_arms)] // many "flag"
|
||||
#[expect(clippy::match_same_arms)] // many "flag"
|
||||
match chr {
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Private use area extensions:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget};
|
||||
use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget as _};
|
||||
|
||||
/// Showcase [`egui::Ui::response`].
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::{Demo, View};
|
||||
|
||||
use egui::{
|
||||
vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Resize, RichText, Sense,
|
||||
Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window,
|
||||
Align, Align2, Checkbox, CollapsingHeader, Color32, ComboBox, Context, FontId, Resize,
|
||||
RichText, Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, vec2,
|
||||
};
|
||||
|
||||
/// Showcase some ui code
|
||||
@@ -16,6 +16,7 @@ pub struct MiscDemoWindow {
|
||||
custom_collapsing_header: CustomCollapsingHeader,
|
||||
tree: Tree,
|
||||
box_painting: BoxPainting,
|
||||
text_rotation: TextRotation,
|
||||
|
||||
dummy_bool: bool,
|
||||
dummy_usize: usize,
|
||||
@@ -32,6 +33,7 @@ impl Default for MiscDemoWindow {
|
||||
custom_collapsing_header: Default::default(),
|
||||
tree: Tree::demo(),
|
||||
box_painting: Default::default(),
|
||||
text_rotation: Default::default(),
|
||||
|
||||
dummy_bool: false,
|
||||
dummy_usize: 0,
|
||||
@@ -79,6 +81,10 @@ impl View for MiscDemoWindow {
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Text rotation")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.text_rotation.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Colors")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
@@ -207,7 +213,7 @@ fn label_ui(ui: &mut egui::Ui) {
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Trick so we don't have to add spaces in the text below:
|
||||
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
|
||||
let width = ui.fonts_mut(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
|
||||
ui.spacing_mut().item_spacing.x = width;
|
||||
|
||||
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
|
||||
@@ -729,3 +735,95 @@ fn text_layout_demo(ui: &mut Ui) {
|
||||
|
||||
ui.label(job);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct TextRotation {
|
||||
size: Vec2,
|
||||
angle: f32,
|
||||
align: egui::Align2,
|
||||
}
|
||||
|
||||
impl Default for TextRotation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: vec2(200.0, 200.0),
|
||||
angle: 0.0,
|
||||
align: egui::Align2::LEFT_TOP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextRotation {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(Slider::new(&mut self.angle, 0.0..=2.0 * std::f32::consts::PI).text("angle"));
|
||||
|
||||
let default_color = if ui.visuals().dark_mode {
|
||||
Color32::LIGHT_GRAY
|
||||
} else {
|
||||
Color32::DARK_GRAY
|
||||
};
|
||||
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "LEFT_TOP"),
|
||||
(Align2::LEFT_CENTER, "LEFT_CENTER"),
|
||||
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
|
||||
(Align2::CENTER_TOP, "CENTER_TOP"),
|
||||
(Align2::CENTER_CENTER, "CENTER_CENTER"),
|
||||
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
|
||||
(Align2::RIGHT_TOP, "RIGHT_TOP"),
|
||||
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
|
||||
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
|
||||
];
|
||||
|
||||
ComboBox::new("anchor", "Anchor")
|
||||
.selected_text(aligns.iter().find(|(a, _)| *a == self.align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(&mut self.align, *align2, *name);
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let (response, painter) = ui.allocate_painter(self.size, Sense::empty());
|
||||
let rect = response.rect;
|
||||
|
||||
let start_pos = self.size / 2.0;
|
||||
|
||||
let s = ui.ctx().fonts_mut(|f| {
|
||||
let mut t = egui::Shape::text(
|
||||
f,
|
||||
rect.min + start_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
"sample_text",
|
||||
egui::FontId::new(12.0, egui::FontFamily::Proportional),
|
||||
default_color,
|
||||
);
|
||||
|
||||
if let egui::epaint::Shape::Text(ts) = &mut t {
|
||||
let new = ts.clone().with_angle_and_anchor(self.angle, self.align);
|
||||
*ts = new;
|
||||
}
|
||||
|
||||
t
|
||||
});
|
||||
|
||||
if let egui::epaint::Shape::Text(ts) = &s {
|
||||
let align_pt =
|
||||
rect.min + start_pos + self.align.pos_in_rect(&ts.galley.rect).to_vec2();
|
||||
painter.circle(align_pt, 2.0, Color32::RED, (0.0, Color32::RED));
|
||||
}
|
||||
|
||||
painter.rect(
|
||||
rect,
|
||||
0.0,
|
||||
default_color.gamma_multiply(0.3),
|
||||
(0.0, Color32::BLACK),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
painter.add(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
pub mod about;
|
||||
pub mod code_editor;
|
||||
pub mod code_example;
|
||||
pub mod context_menu;
|
||||
pub mod dancing_strings;
|
||||
pub mod demo_app_windows;
|
||||
pub mod drag_and_drop;
|
||||
@@ -23,6 +22,7 @@ pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod panels;
|
||||
pub mod password;
|
||||
mod popups;
|
||||
pub mod scene;
|
||||
pub mod screenshot;
|
||||
pub mod scrolling;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window};
|
||||
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget as _, Window};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
@@ -96,7 +96,9 @@ impl crate::View for Modals {
|
||||
*save_modal_open = true;
|
||||
}
|
||||
if ui.button("Cancel").clicked() {
|
||||
*user_modal_open = false;
|
||||
// You can call `ui.close()` to close the modal.
|
||||
// (This causes the current modals `should_close` to return true)
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -123,7 +125,7 @@ impl crate::View for Modals {
|
||||
}
|
||||
|
||||
if ui.button("No Thanks").clicked() {
|
||||
*save_modal_open = false;
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -160,11 +162,11 @@ impl crate::View for Modals {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Demo as _;
|
||||
use crate::demo::modals::Modals;
|
||||
use crate::Demo;
|
||||
use egui::accesskit::Role;
|
||||
use egui::Key;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use egui::{Key, Popup};
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
@@ -185,12 +187,12 @@ mod tests {
|
||||
|
||||
// Harness::run would fail because we keep requesting repaints to simulate progress.
|
||||
harness.run_ok();
|
||||
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
|
||||
assert!(Popup::is_any_open(&harness.ctx));
|
||||
assert!(harness.state().user_modal_open);
|
||||
|
||||
harness.press_key(Key::Escape);
|
||||
harness.key_press(Key::Escape);
|
||||
harness.run_ok();
|
||||
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
|
||||
assert!(!Popup::is_any_open(&harness.ctx));
|
||||
assert!(harness.state().user_modal_open);
|
||||
}
|
||||
|
||||
@@ -212,7 +214,7 @@ mod tests {
|
||||
assert!(harness.state().user_modal_open);
|
||||
assert!(harness.state().save_modal_open);
|
||||
|
||||
harness.press_key(Key::Escape);
|
||||
harness.key_press(Key::Escape);
|
||||
harness.run();
|
||||
|
||||
assert!(harness.state().user_modal_open);
|
||||
@@ -265,7 +267,7 @@ mod tests {
|
||||
|
||||
harness.run_ok();
|
||||
|
||||
harness.get_by_label("Yes Please").simulate_click();
|
||||
harness.get_by_label("Yes Please").click();
|
||||
|
||||
harness.run_ok();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use egui::{
|
||||
Color32, Event, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
vec2,
|
||||
};
|
||||
|
||||
pub struct MultiTouch {
|
||||
@@ -29,7 +30,7 @@ impl crate::Demo for MultiTouch {
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.default_size(vec2(544.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
@@ -44,13 +45,31 @@ impl crate::View for MultiTouch {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles, tablets, and trackpads).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let relative_pointer_gesture = ui.input(|i| {
|
||||
i.events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::MouseWheel { .. } | Event::Zoom { .. } | Event::Rotate { .. }
|
||||
)
|
||||
})
|
||||
});
|
||||
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
|
||||
ui.label(format!("Current touches: {num_touches}"));
|
||||
let num_touches_str = format!("{num_touches}-finger touch");
|
||||
ui.label(format!(
|
||||
"Input source: {}",
|
||||
if ui.input(|i| i.multi_touch().is_some()) {
|
||||
num_touches_str.as_str()
|
||||
} else if relative_pointer_gesture {
|
||||
"cursor"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
));
|
||||
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
@@ -82,18 +101,18 @@ impl crate::View for MultiTouch {
|
||||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
if ui.input(|i| i.multi_touch().is_some()) || relative_pointer_gesture {
|
||||
ui.input(|input| {
|
||||
// This adjusts the current zoom factor, rotation angle, and translation according
|
||||
// to the dynamic change (for the current frame) of the touch gesture:
|
||||
self.zoom *= input.zoom_delta();
|
||||
self.rotation += input.rotation_delta();
|
||||
self.translation += to_screen.inverse().scale() * input.translation_delta();
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * input.multi_touch().map_or(0.0, |touch| touch.force);
|
||||
|
||||
self.last_touch_time = ui.input(|i| i.time);
|
||||
self.last_touch_time = input.time;
|
||||
});
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use egui::{
|
||||
emath,
|
||||
Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
|
||||
Widget as _, Window, emath,
|
||||
epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape},
|
||||
pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
|
||||
Widget, Window,
|
||||
pos2,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -152,7 +152,7 @@ impl PaintBezier {
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
|
||||
painter.extend(control_point_shapes);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{emath, vec2, Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window};
|
||||
use egui::{Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window, emath, vec2};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/// ``` ignore
|
||||
/// password_ui(ui, &mut my_password);
|
||||
/// ```
|
||||
#[allow(clippy::ptr_arg)] // false positive
|
||||
pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
@@ -28,7 +27,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.selectable_label(show_plaintext, "👁")
|
||||
.on_hover_text("Show/hide password");
|
||||
|
||||
if response.clicked() {
|
||||
@@ -62,5 +61,5 @@ pub fn password(password: &mut String) -> impl egui::Widget + '_ {
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/main/{}", file!())
|
||||
}
|
||||
|
||||
319
crates/egui_demo_lib/src/demo/popups.rs
Normal file
319
crates/egui_demo_lib/src/demo/popups.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use crate::rust_view_ui;
|
||||
use egui::color_picker::{Alpha, color_picker_color32};
|
||||
use egui::containers::menu::{MenuConfig, SubMenuButton};
|
||||
use egui::{
|
||||
Align, Align2, Atom, Button, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, RectAlign,
|
||||
RichText, Tooltip, Ui, UiBuilder, include_image,
|
||||
};
|
||||
|
||||
/// Showcase [`Popup`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct PopupsDemo {
|
||||
align4: RectAlign,
|
||||
gap: f32,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
close_behavior: PopupCloseBehavior,
|
||||
popup_open: bool,
|
||||
checked: bool,
|
||||
color: egui::Color32,
|
||||
}
|
||||
|
||||
impl Default for PopupsDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
align4: RectAlign::default(),
|
||||
gap: 4.0,
|
||||
close_behavior: PopupCloseBehavior::CloseOnClick,
|
||||
popup_open: false,
|
||||
checked: true,
|
||||
color: egui::Color32::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupsDemo {
|
||||
fn apply_options<'a>(&self, popup: Popup<'a>) -> Popup<'a> {
|
||||
popup
|
||||
.align(self.align4)
|
||||
.gap(self.gap)
|
||||
.close_behavior(self.close_behavior)
|
||||
}
|
||||
|
||||
fn nested_menus(&mut self, ui: &mut Ui) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
ui.menu_button("Popups can have submenus", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
ui.menu_button("Recursive", |ui| self.nested_menus(ui));
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
ui.menu_button("SubMenus can be disabled", |_| {});
|
||||
});
|
||||
ui.menu_image_text_button(
|
||||
include_image!("../../data/icon.png"),
|
||||
"I have an icon!",
|
||||
|ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
SubMenuButton::new("Always CloseOnClickOutside")
|
||||
.config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside))
|
||||
.ui(ui, |ui| {
|
||||
ui.checkbox(&mut self.checked, "Checkbox");
|
||||
|
||||
// Customized color SubMenuButton
|
||||
let is_bright = self.color.intensity() > 0.5;
|
||||
let text_color = if is_bright {
|
||||
egui::Color32::BLACK
|
||||
} else {
|
||||
egui::Color32::WHITE
|
||||
};
|
||||
|
||||
let button = Button::new((
|
||||
RichText::new("Background").color(text_color),
|
||||
Atom::grow(),
|
||||
RichText::new(SubMenuButton::RIGHT_ARROW).color(text_color),
|
||||
))
|
||||
.fill(self.color);
|
||||
|
||||
SubMenuButton::from_button(button).ui(ui, |ui| {
|
||||
ui.spacing_mut().slider_width = 200.0;
|
||||
color_picker_color32(ui, &mut self.color, Alpha::Opaque);
|
||||
});
|
||||
|
||||
if self.checked {
|
||||
ui.menu_button("Only visible when checked", |ui| {
|
||||
if ui.button("Remove myself").clicked() {
|
||||
self.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for PopupsDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"\u{2755} Popups"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.default_width(250.0)
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for PopupsDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let response = Frame::group(ui.style())
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
||||
.inner
|
||||
})
|
||||
.inner;
|
||||
|
||||
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
||||
.show(|ui| self.nested_menus(ui));
|
||||
|
||||
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
||||
.show(|ui| self.nested_menus(ui));
|
||||
|
||||
if self.popup_open {
|
||||
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
|
||||
.show(|ui| {
|
||||
ui.label("Popup contents");
|
||||
});
|
||||
}
|
||||
|
||||
let mut tooltip = Tooltip::for_enabled(&response);
|
||||
tooltip.popup = self.apply_options(tooltip.popup);
|
||||
tooltip.show(|ui| {
|
||||
ui.label("Tooltips are popups, too!");
|
||||
});
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
let mut reset_btn_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.max_rect(ui.max_rect())
|
||||
.layout(Layout::right_to_left(Align::Min)),
|
||||
);
|
||||
if reset_btn_ui
|
||||
.button("⟲")
|
||||
.on_hover_text("Reset to defaults")
|
||||
.clicked()
|
||||
{
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
ui.set_width(ui.available_width());
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "LEFT_TOP"),
|
||||
(Align2::LEFT_CENTER, "LEFT_CENTER"),
|
||||
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
|
||||
(Align2::CENTER_TOP, "CENTER_TOP"),
|
||||
(Align2::CENTER_CENTER, "CENTER_CENTER"),
|
||||
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
|
||||
(Align2::RIGHT_TOP, "RIGHT_TOP"),
|
||||
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
|
||||
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
|
||||
];
|
||||
|
||||
ComboBox::new(label, "")
|
||||
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(align, *align2, *name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rust_view_ui(ui, "let align = RectAlign {");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " parent: Align2::");
|
||||
align_combobox(ui, "parent", &mut self.align4.parent);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " child: Align2::");
|
||||
align_combobox(ui, "child", &mut self.align4.child);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
rust_view_ui(ui, "};");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let align = RectAlign::");
|
||||
|
||||
let presets = [
|
||||
(RectAlign::TOP_START, "TOP_START"),
|
||||
(RectAlign::TOP, "TOP"),
|
||||
(RectAlign::TOP_END, "TOP_END"),
|
||||
(RectAlign::RIGHT_START, "RIGHT_START"),
|
||||
(RectAlign::RIGHT, "RIGHT"),
|
||||
(RectAlign::RIGHT_END, "RIGHT_END"),
|
||||
(RectAlign::BOTTOM_START, "BOTTOM_START"),
|
||||
(RectAlign::BOTTOM, "BOTTOM"),
|
||||
(RectAlign::BOTTOM_END, "BOTTOM_END"),
|
||||
(RectAlign::LEFT_START, "LEFT_START"),
|
||||
(RectAlign::LEFT, "LEFT"),
|
||||
(RectAlign::LEFT_END, "LEFT_END"),
|
||||
];
|
||||
|
||||
ComboBox::new("Preset", "")
|
||||
.selected_text(
|
||||
presets
|
||||
.iter()
|
||||
.find(|(a, _)| a == &self.align4)
|
||||
.map_or("<Select Preset>", |(_, name)| *name),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align4, name) in &presets {
|
||||
ui.selectable_value(&mut self.align4, *align4, *name);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let gap = ");
|
||||
ui.add(egui::DragValue::new(&mut self.gap));
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
rust_view_ui(ui, "let close_behavior");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " = PopupCloseBehavior::");
|
||||
let close_behaviors = [
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
"CloseOnClick",
|
||||
"Closes when the user clicks anywhere (inside or outside)",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClickOutside,
|
||||
"CloseOnClickOutside",
|
||||
"Closes when the user clicks outside the popup",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
"IgnoreClicks",
|
||||
"Close only when the button is clicked again",
|
||||
),
|
||||
];
|
||||
ComboBox::new("Close behavior", "")
|
||||
.selected_text(
|
||||
close_behaviors
|
||||
.iter()
|
||||
.find_map(|(behavior, text, _)| {
|
||||
(behavior == &self.close_behavior).then_some(*text)
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (close_behavior, name, tooltip) in &close_behaviors {
|
||||
ui.selectable_value(&mut self.close_behavior, *close_behavior, *name)
|
||||
.on_hover_text(*tooltip);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let popup_open = ");
|
||||
ui.checkbox(&mut self.popup_open, "");
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
ui.monospace("");
|
||||
rust_view_ui(ui, "let response = ui.button(\"Click me!\");");
|
||||
rust_view_ui(ui, "Popup::menu(&response)");
|
||||
rust_view_ui(ui, " .gap(gap).align(align)");
|
||||
rust_view_ui(ui, " .close_behavior(close_behavior)");
|
||||
rust_view_ui(ui, " .show(|ui| { /* menu contents */ });");
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Image, UserData, ViewportCommand, Widget};
|
||||
use egui::{Image, UserData, ViewportCommand, Widget as _};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Showcase [`ViewportCommand::Screenshot`].
|
||||
@@ -58,7 +58,7 @@ impl crate::View for Screenshot {
|
||||
None
|
||||
}
|
||||
})
|
||||
.last()
|
||||
.next_back()
|
||||
});
|
||||
|
||||
if let Some(image) = image {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use egui::{
|
||||
pos2, scroll_area::ScrollBarVisibility, Align, Align2, Color32, DragValue, NumExt, Rect,
|
||||
ScrollArea, Sense, Slider, TextStyle, TextWrapMode, Ui, Vec2, Widget,
|
||||
Align, Align2, Color32, DragValue, NumExt as _, Rect, ScrollArea, Sense, Slider, TextStyle,
|
||||
TextWrapMode, Ui, Vec2, Widget as _, pos2, scroll_area::ScrollBarVisibility,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum ScrollDemo {
|
||||
#[default]
|
||||
ScrollAppearance,
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
@@ -14,12 +15,6 @@ enum ScrollDemo {
|
||||
Bidirectional,
|
||||
}
|
||||
|
||||
impl Default for ScrollDemo {
|
||||
fn default() -> Self {
|
||||
Self::ScrollAppearance
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
@@ -191,7 +186,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
|
||||
let num_rows = 10_000;
|
||||
|
||||
ScrollArea::vertical()
|
||||
@@ -222,7 +217,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
used_rect = used_rect.union(text_rect);
|
||||
used_rect |= text_rect;
|
||||
}
|
||||
|
||||
ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui};
|
||||
use egui::{Slider, SliderClamping, SliderOrientation, Ui, style::HandleShape};
|
||||
|
||||
/// Showcase sliders
|
||||
#[derive(PartialEq)]
|
||||
|
||||
@@ -13,6 +13,7 @@ enum DemoType {
|
||||
pub struct TableDemo {
|
||||
demo: DemoType,
|
||||
striped: bool,
|
||||
overline: bool,
|
||||
resizable: bool,
|
||||
clickable: bool,
|
||||
num_rows: usize,
|
||||
@@ -28,6 +29,7 @@ impl Default for TableDemo {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
striped: true,
|
||||
overline: true,
|
||||
resizable: true,
|
||||
clickable: true,
|
||||
num_rows: 10_000,
|
||||
@@ -65,6 +67,7 @@ impl crate::View for TableDemo {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.striped, "Striped");
|
||||
ui.checkbox(&mut self.overline, "Overline some rows");
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
ui.checkbox(&mut self.clickable, "Clickable rows");
|
||||
});
|
||||
@@ -212,6 +215,7 @@ impl TableDemo {
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -247,6 +251,7 @@ impl TableDemo {
|
||||
};
|
||||
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -280,6 +285,7 @@ impl TableDemo {
|
||||
};
|
||||
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -324,9 +330,11 @@ fn expanding_content(ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
fn long_text(row_index: usize) -> String {
|
||||
format!("Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!")
|
||||
format!(
|
||||
"Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!"
|
||||
)
|
||||
}
|
||||
|
||||
fn thick_row(row_index: usize) -> bool {
|
||||
row_index % 6 == 0
|
||||
row_index.is_multiple_of(6)
|
||||
}
|
||||
|
||||
@@ -67,10 +67,9 @@ impl crate::View for ClipboardTest {
|
||||
|
||||
if let Ok(egui::load::ImagePoll::Ready { image }) =
|
||||
ui.ctx().try_load_image(&uri, Default::default())
|
||||
&& ui.button("📋").clicked()
|
||||
{
|
||||
if ui.button("📋").clicked() {
|
||||
ui.ctx().copy_image((*image).clone());
|
||||
}
|
||||
ui.ctx().copy_image((*image).clone());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ impl crate::View for GridTest {
|
||||
ui.end_row();
|
||||
|
||||
let mut dyn_text = String::from("O");
|
||||
dyn_text.extend(std::iter::repeat('h').take(self.text_length));
|
||||
dyn_text.extend(std::iter::repeat_n('h', self.text_length));
|
||||
ui.label(dyn_text);
|
||||
ui.label("Fifth row, second column");
|
||||
ui.end_row();
|
||||
|
||||
@@ -12,11 +12,11 @@ struct DeduplicatedHistory {
|
||||
|
||||
impl DeduplicatedHistory {
|
||||
fn add(&mut self, summary: String, full: String) {
|
||||
if let Some(entry) = self.history.back_mut() {
|
||||
if entry.summary == summary {
|
||||
entry.entries.push(full);
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.history.back_mut()
|
||||
&& entry.summary == summary
|
||||
{
|
||||
entry.entries.push(full);
|
||||
return;
|
||||
}
|
||||
self.history.push_back(HistoryEntry {
|
||||
summary,
|
||||
|
||||
@@ -10,11 +10,11 @@ struct DeduplicatedHistory {
|
||||
|
||||
impl DeduplicatedHistory {
|
||||
fn add(&mut self, text: String) {
|
||||
if let Some(entry) = self.history.back_mut() {
|
||||
if entry.text == text {
|
||||
entry.repeated += 1;
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.history.back_mut()
|
||||
&& entry.text == text
|
||||
{
|
||||
entry.repeated += 1;
|
||||
return;
|
||||
}
|
||||
self.history.push_back(HistoryEntry { text, repeated: 1 });
|
||||
if self.history.len() > 100 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{vec2, Align, Direction, Layout, Resize, Slider, Ui};
|
||||
use egui::{Align, Direction, Layout, Resize, Slider, Ui, vec2};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
||||
@@ -6,6 +6,7 @@ mod input_event_history;
|
||||
mod input_test;
|
||||
mod layout_test;
|
||||
mod manual_layout_test;
|
||||
mod svg_test;
|
||||
mod tessellation_test;
|
||||
mod window_resize_test;
|
||||
|
||||
@@ -17,5 +18,6 @@ pub use input_event_history::InputEventHistory;
|
||||
pub use input_test::InputTest;
|
||||
pub use layout_test::LayoutTest;
|
||||
pub use manual_layout_test::ManualLayoutTest;
|
||||
pub use svg_test::SvgTest;
|
||||
pub use tessellation_test::TessellationTest;
|
||||
pub use window_resize_test::WindowResizeTest;
|
||||
|
||||
42
crates/egui_demo_lib/src/demo/tests/svg_test.rs
Normal file
42
crates/egui_demo_lib/src/demo/tests/svg_test.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
pub struct SvgTest {
|
||||
color: egui::Color32,
|
||||
}
|
||||
|
||||
impl Default for SvgTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: egui::Color32::LIGHT_RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for SvgTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVG Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for SvgTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { color } = self;
|
||||
ui.color_edit_button_srgba(color);
|
||||
let img_src = egui::include_image!("../../../data/peace.svg");
|
||||
|
||||
// First paint a small version, sized the same as the source…
|
||||
ui.add(
|
||||
egui::Image::new(img_src.clone())
|
||||
.fit_to_original_size(1.0)
|
||||
.tint(*color),
|
||||
);
|
||||
|
||||
// …then a big one, to make sure they are both crisp
|
||||
ui.add(egui::Image::new(img_src).tint(*color));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use egui::{
|
||||
emath::{GuiRounding, TSTransform},
|
||||
Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
|
||||
emath::{GuiRounding as _, TSTransform},
|
||||
epaint::{self, RectShape},
|
||||
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
|
||||
vec2,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -373,7 +374,7 @@ mod tests {
|
||||
harness.fit_contents();
|
||||
harness.run();
|
||||
|
||||
harness.snapshot(&format!("tessellation_test/{name}"));
|
||||
harness.snapshot(format!("tessellation_test/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,20 +65,20 @@ impl crate::View for TextEditDemo {
|
||||
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
|
||||
);
|
||||
|
||||
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) {
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y))
|
||||
&& let Some(text_cursor_range) = output.cursor_range
|
||||
{
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@@ -113,9 +113,9 @@ impl crate::View for TextEditDemo {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{accesskit, CentralPanel};
|
||||
use egui_kittest::kittest::{Key, Queryable};
|
||||
use egui::{CentralPanel, Key, Modifiers, accesskit};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
pub fn should_type() {
|
||||
@@ -133,8 +133,9 @@ mod tests {
|
||||
|
||||
let text_edit = harness.get_by_role(accesskit::Role::TextInput);
|
||||
assert_eq!(text_edit.value().as_deref(), Some("Hello, world!"));
|
||||
text_edit.focus();
|
||||
|
||||
text_edit.key_combination(&[Key::Command, Key::A]);
|
||||
harness.key_press_modifiers(Modifiers::COMMAND, Key::A);
|
||||
text_edit.type_text("Hi ");
|
||||
|
||||
harness.run();
|
||||
|
||||
@@ -5,7 +5,7 @@ pub struct TextLayoutDemo {
|
||||
break_anywhere: bool,
|
||||
max_rows: usize,
|
||||
overflow_character: Option<char>,
|
||||
extra_letter_spacing_pixels: i32,
|
||||
extra_letter_spacing: f32,
|
||||
line_height_pixels: u32,
|
||||
lorem_ipsum: bool,
|
||||
}
|
||||
@@ -16,7 +16,7 @@ impl Default for TextLayoutDemo {
|
||||
max_rows: 6,
|
||||
break_anywhere: true,
|
||||
overflow_character: Some('…'),
|
||||
extra_letter_spacing_pixels: 0,
|
||||
extra_letter_spacing: 0.0,
|
||||
line_height_pixels: 0,
|
||||
lorem_ipsum: true,
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl crate::View for TextLayoutDemo {
|
||||
break_anywhere,
|
||||
max_rows,
|
||||
overflow_character,
|
||||
extra_letter_spacing_pixels,
|
||||
extra_letter_spacing,
|
||||
line_height_pixels,
|
||||
lorem_ipsum,
|
||||
} = self;
|
||||
@@ -85,7 +85,7 @@ impl crate::View for TextLayoutDemo {
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Extra letter spacing:");
|
||||
ui.add(egui::DragValue::new(extra_letter_spacing_pixels).suffix(" pixels"));
|
||||
ui.add(egui::DragValue::new(extra_letter_spacing).speed(0.1));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Line height:");
|
||||
@@ -126,14 +126,13 @@ impl crate::View for TextLayoutDemo {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32;
|
||||
let line_height = (*line_height_pixels != 0)
|
||||
.then_some(points_per_pixel * *line_height_pixels as f32);
|
||||
|
||||
let mut job = LayoutJob::single_section(
|
||||
text.to_owned(),
|
||||
egui::TextFormat {
|
||||
extra_letter_spacing,
|
||||
extra_letter_spacing: *extra_letter_spacing,
|
||||
line_height,
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
@@ -121,5 +121,5 @@ pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/main/{}", file!())
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ impl Tooltips {
|
||||
ui.label("You can select this text.");
|
||||
});
|
||||
|
||||
ui.label("This tooltip shows at the mouse cursor.")
|
||||
.on_hover_text_at_pointer("Move me around!!");
|
||||
|
||||
ui.separator(); // ---------------------------------------------------------
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{util::undoer::Undoer, Button};
|
||||
use egui::{Button, util::undoer::Undoer};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -60,15 +60,11 @@ impl crate::View for UndoRedoDemo {
|
||||
let undo = ui.add_enabled(can_undo, Button::new("⟲ Undo")).clicked();
|
||||
let redo = ui.add_enabled(can_redo, Button::new("⟳ Redo")).clicked();
|
||||
|
||||
if undo {
|
||||
if let Some(undo_text) = self.undoer.undo(&self.state) {
|
||||
self.state = undo_text.clone();
|
||||
}
|
||||
if undo && let Some(undo_text) = self.undoer.undo(&self.state) {
|
||||
self.state = undo_text.clone();
|
||||
}
|
||||
if redo {
|
||||
if let Some(redo_text) = self.undoer.redo(&self.state) {
|
||||
self.state = redo_text.clone();
|
||||
}
|
||||
if redo && let Some(redo_text) = self.undoer.redo(&self.state) {
|
||||
self.state = redo_text.clone();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Default for WidgetGallery {
|
||||
}
|
||||
|
||||
impl WidgetGallery {
|
||||
#[allow(unused_mut)] // if not chrono
|
||||
#[allow(unused_mut, clippy::allow_attributes)] // if not chrono
|
||||
#[inline]
|
||||
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
|
||||
#[cfg(feature = "chrono")]
|
||||
@@ -308,7 +308,7 @@ fn doc_link_label_with_crate<'a>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::View;
|
||||
use crate::View as _;
|
||||
use egui::Vec2;
|
||||
use egui_kittest::Harness;
|
||||
|
||||
@@ -319,13 +319,27 @@ mod tests {
|
||||
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| demo.ui(ui));
|
||||
|
||||
harness.fit_contents();
|
||||
for pixels_per_point in [1, 2] {
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point as f32)
|
||||
.with_theme(theme)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
demo.ui(ui);
|
||||
});
|
||||
|
||||
harness.snapshot("widget_gallery");
|
||||
harness.fit_contents();
|
||||
|
||||
let theme_name = match theme {
|
||||
egui::Theme::Light => "light",
|
||||
egui::Theme::Dark => "dark",
|
||||
};
|
||||
let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}");
|
||||
harness.snapshot(&image_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::Vec2b;
|
||||
use egui::{UiKind, Vec2b};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -149,6 +149,16 @@ impl crate::View for WindowOptions {
|
||||
self.disabled_time = ui.input(|i| i.time);
|
||||
}
|
||||
egui::reset_button(ui, self, "Reset");
|
||||
if ui
|
||||
.button("Close")
|
||||
.on_hover_text("You can collapse / close Windows via Ui::close")
|
||||
.clicked()
|
||||
{
|
||||
// Calling close would close the collapsible within the window
|
||||
// ui.close();
|
||||
// Instead, we close the window itself
|
||||
ui.close_kind(UiKind::Window);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use egui::{
|
||||
text::CCursorRange, Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui,
|
||||
Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui, text::CCursorRange,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -80,10 +80,10 @@ impl EasyMarkEditor {
|
||||
} = self;
|
||||
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
ui.add(
|
||||
@@ -96,13 +96,13 @@ impl EasyMarkEditor {
|
||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||
};
|
||||
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||
if let Some(mut ccursor_range) = state.cursor.char_range() {
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.cursor.set_char_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id)
|
||||
&& let Some(mut ccursor_range) = state.cursor.char_range()
|
||||
{
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.cursor.set_char_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
|
||||
if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) {
|
||||
// This is a placeholder till we can indent the active line
|
||||
any_change = true;
|
||||
let [primary, _secondary] = ccursor_range.sorted();
|
||||
let [primary, _secondary] = ccursor_range.sorted_cursors();
|
||||
|
||||
let advance = code.insert_text(" ", primary.index);
|
||||
ccursor_range.primary.index += advance;
|
||||
@@ -165,7 +165,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
|
||||
if ui.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
any_change = true;
|
||||
toggle_surrounding(code, ccursor_range, surrounding);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
any_change
|
||||
@@ -177,7 +177,7 @@ fn toggle_surrounding(
|
||||
ccursor_range: &mut CCursorRange,
|
||||
surrounding: &str,
|
||||
) {
|
||||
let [primary, secondary] = ccursor_range.sorted();
|
||||
let [primary, secondary] = ccursor_range.sorted_cursors();
|
||||
|
||||
let surrounding_ccount = surrounding.chars().count();
|
||||
|
||||
@@ -237,7 +237,7 @@ Goals:
|
||||
2. easy to learn
|
||||
3. similar to markdown
|
||||
|
||||
[The reference parser](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||
[The reference parser](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||
|
||||
There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list.
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::tex
|
||||
// Swallow everything up to the next special character:
|
||||
let line_end = text[skip..]
|
||||
.find('\n')
|
||||
.map_or_else(|| text.len(), |i| (skip + i + 1));
|
||||
.map_or_else(|| text.len(), |i| skip + i + 1);
|
||||
let end = text[skip..]
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
|
||||
.map_or_else(|| text.len(), |i| (skip + i).max(1));
|
||||
|
||||
@@ -113,19 +113,19 @@ impl<'a> Parser<'a> {
|
||||
|
||||
// ```{language}\n{code}```
|
||||
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||
if let Some(newline) = language_start.find('\n') {
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
if let Some(language_start) = self.s.strip_prefix("```")
|
||||
&& let Some(newline) = language_start.find('\n')
|
||||
{
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -171,14 +171,14 @@ impl<'a> Parser<'a> {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(bracket_end) = this_line.find(']') {
|
||||
let text = &this_line[1..bracket_end];
|
||||
if this_line[bracket_end + 1..].starts_with('(') {
|
||||
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
if this_line[bracket_end + 1..].starts_with('(')
|
||||
&& let Some(parens_end) = this_line[bracket_end + 2..].find(')')
|
||||
{
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::easy_mark_parser as easy_mark;
|
||||
use egui::{
|
||||
vec2, Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle,
|
||||
Ui,
|
||||
Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle, Ui,
|
||||
vec2,
|
||||
};
|
||||
|
||||
/// Parse and display a VERY simple and small subset of Markdown.
|
||||
@@ -101,7 +101,7 @@ pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
|
||||
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
|
||||
@@ -162,7 +162,7 @@ fn bullet_point(ui: &mut Ui, width: f32) -> Response {
|
||||
|
||||
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id));
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
let text = format!("{number}.");
|
||||
let text_color = ui.visuals().strong_text_color();
|
||||
|
||||
@@ -35,7 +35,7 @@ macro_rules! egui_github_link_file {
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
"https://github.com/emilk/egui/blob/main/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
@@ -49,7 +49,7 @@ macro_rules! egui_github_link_file_line {
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file_line!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
"https://github.com/emilk/egui/blob/main/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
@@ -109,6 +109,6 @@ fn test_egui_zero_window_size() {
|
||||
/// Detect narrow screens. This is used to show a simpler UI on mobile devices,
|
||||
/// especially for the web demo at <https://egui.rs>.
|
||||
pub fn is_mobile(ctx: &egui::Context) -> bool {
|
||||
let screen_size = ctx.screen_rect().size();
|
||||
let screen_size = ctx.content_rect().size();
|
||||
screen_size.x < 550.0
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use egui::{
|
||||
emath::GuiRounding as _, epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2,
|
||||
Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke,
|
||||
TextureHandle, TextureOptions, Ui, Vec2,
|
||||
Align2, Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape,
|
||||
Stroke, TextureHandle, TextureOptions, Ui, Vec2, emath::GuiRounding as _, epaint, lerp, pos2,
|
||||
vec2, widgets::color_picker::show_color,
|
||||
};
|
||||
|
||||
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
|
||||
@@ -92,8 +92,6 @@ impl ColorTest {
|
||||
|
||||
ui.label("Test that vertex color times texture color is done in gamma space:");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
let tex_color = Color32::from_rgb(64, 128, 255);
|
||||
let vertex_color = Color32::from_rgb(128, 196, 196);
|
||||
let ground_truth = mul_color_gamma(tex_color, vertex_color);
|
||||
@@ -106,6 +104,9 @@ impl ColorTest {
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
{
|
||||
let g = Gradient::one_color(ground_truth);
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
@@ -129,8 +130,36 @@ impl ColorTest {
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Test that blending is done in gamma space:");
|
||||
ui.scope(|ui| {
|
||||
let background = Color32::from_rgb(200, 60, 10);
|
||||
let foreground = Color32::from_rgba_unmultiplied(108, 65, 200, 82);
|
||||
let ground_truth = background.blend(foreground);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
ui.label("Background:");
|
||||
show_color(ui, background, color_size);
|
||||
ui.label(", foreground: ");
|
||||
show_color(ui, foreground, color_size);
|
||||
});
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
{
|
||||
let g = Gradient::one_color(ground_truth);
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
{
|
||||
let g = Gradient::one_color(foreground);
|
||||
self.vertex_gradient(ui, "Vertex blending", background, &g);
|
||||
self.tex_gradient(ui, "Texture blending", background, &g);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
// to make sure vertex and texture color multiplication is done in gamma space.
|
||||
|
||||
ui.label("Gamma interpolation:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
@@ -162,8 +191,8 @@ impl ColorTest {
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Linear interpolation (texture sampling):");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
|
||||
ui.label("Texture interpolation (texture sampling) should be in gamma space:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
}
|
||||
|
||||
fn show_gradients(
|
||||
@@ -216,11 +245,10 @@ impl ColorTest {
|
||||
let g = Gradient::endpoints(left, right);
|
||||
|
||||
match interpolation {
|
||||
Interpolation::Linear => {
|
||||
// texture sampler is sRGBA aware, and should therefore be linear
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
}
|
||||
Interpolation::Linear => {}
|
||||
Interpolation::Gamma => {
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
|
||||
// vertex shader uses gamma
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
@@ -278,7 +306,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
assert!(
|
||||
n >= 2,
|
||||
"A gradient must have at least two colors, but this had {n}"
|
||||
);
|
||||
let mut mesh = Mesh::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
@@ -298,7 +329,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Interpolation {
|
||||
/// egui used to want Linear interpolation for some things, but now we're always in gamma space.
|
||||
#[expect(unused)]
|
||||
Linear,
|
||||
|
||||
Gamma,
|
||||
}
|
||||
|
||||
@@ -387,10 +421,7 @@ impl TextureManager {
|
||||
let height = 1;
|
||||
ctx.load_texture(
|
||||
"color_test_gradient",
|
||||
epaint::ColorImage {
|
||||
size: [width, height],
|
||||
pixels,
|
||||
},
|
||||
epaint::ColorImage::new([width, height], pixels),
|
||||
TextureOptions::LINEAR,
|
||||
)
|
||||
})
|
||||
@@ -437,7 +468,7 @@ fn pixel_test_strokes(ui: &mut Ui) {
|
||||
let thickness_points = thickness_pixels / pixels_per_point;
|
||||
let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32;
|
||||
let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let size_points = size_pixels / pixels_per_point;
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
let mut cursor_pixel = Pos2::new(
|
||||
@@ -565,8 +596,14 @@ fn blending_and_feathering_test(ui: &mut Ui) {
|
||||
}
|
||||
|
||||
fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) {
|
||||
assert!(fg.is_opaque());
|
||||
assert!(bg.is_opaque());
|
||||
assert!(
|
||||
fg.is_opaque(),
|
||||
"Foreground color must be opaque, but was: {fg:?}",
|
||||
);
|
||||
assert!(
|
||||
bg.is_opaque(),
|
||||
"Background color must be opaque, but was: {bg:?}",
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
@@ -687,8 +724,8 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ColorTest;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use egui_kittest::SnapshotResults;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
pub fn rendering_test() {
|
||||
@@ -702,14 +739,15 @@ mod tests {
|
||||
});
|
||||
|
||||
{
|
||||
// Expand color-test collapsing header
|
||||
harness.get_by_label("Color test").click();
|
||||
// Expand color-test collapsing header. We accesskit-click since collapsing header
|
||||
// might not be on screen at this point.
|
||||
harness.get_by_label("Color test").click_accesskit();
|
||||
harness.run();
|
||||
}
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")));
|
||||
results.add(harness.try_snapshot(format!("rendering_test/dpi_{dpi:.2}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
crates/egui_demo_lib/tests/image_blending.rs
Normal file
25
crates/egui_demo_lib/tests/image_blending.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use egui::{hex_color, include_image};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
fn test_image_blending() {
|
||||
for pixels_per_point in [1.0, 2.0] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point)
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
egui::Frame::new()
|
||||
.fill(hex_color!("#5981FF"))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::Image::new(include_image!("../data/ring.png"))
|
||||
.max_height(18.0)
|
||||
.tint(egui::Color32::GRAY),
|
||||
);
|
||||
});
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!("image_blending/image_x{pixels_per_point}"));
|
||||
}
|
||||
}
|
||||
75
crates/egui_demo_lib/tests/misc.rs
Normal file
75
crates/egui_demo_lib/tests/misc.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use egui::{Color32, accesskit::Role};
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
|
||||
#[test]
|
||||
fn test_kerning() {
|
||||
for pixels_per_point in [1.0, 2.0] {
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point)
|
||||
.with_theme(theme)
|
||||
.build_ui(|ui| {
|
||||
ui.label("Hello world!");
|
||||
ui.label("Repeated characters: iiiiiiiiiiiii lllllllll mmmmmmmmmmmmmmmm");
|
||||
ui.label("Thin spaces: −123 456 789");
|
||||
ui.label("Ligature: fi :)");
|
||||
ui.label("\ttabbed");
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!(
|
||||
"image_kerning/image_{theme}_x{pixels_per_point}",
|
||||
theme = match theme {
|
||||
egui::Theme::Dark => "dark",
|
||||
egui::Theme::Light => "light",
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_italics() {
|
||||
for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point)
|
||||
.with_theme(theme)
|
||||
.build_ui(|ui| {
|
||||
ui.label(egui::RichText::new("Small italics").italics().small());
|
||||
ui.label(egui::RichText::new("Normal italics").italics());
|
||||
ui.label(egui::RichText::new("Large italics").italics().size(22.0));
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!(
|
||||
"italics/image_{theme}_x{pixels_per_point:.2}",
|
||||
theme = match theme {
|
||||
egui::Theme::Dark => "dark",
|
||||
egui::Theme::Light => "light",
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_selection() {
|
||||
let mut harness = Harness::builder().build_ui(|ui| {
|
||||
let visuals = ui.visuals_mut();
|
||||
visuals.selection.bg_fill = Color32::LIGHT_GREEN;
|
||||
visuals.selection.stroke.color = Color32::DARK_BLUE;
|
||||
|
||||
ui.label("Some varied ☺ text :)\nAnd it has a second line!");
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
|
||||
// Drag to select text:
|
||||
let label = harness.get_by_role(Role::Label);
|
||||
harness.drag_at(label.rect().lerp_inside([0.2, 0.25]));
|
||||
harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
|
||||
harness.run();
|
||||
|
||||
harness.snapshot("text_selection");
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90
|
||||
size 31780
|
||||
oid sha256:30929184fab7e7d5975243d86bcab79cd9f7a0c5d57dd9ae827464ff6570be7b
|
||||
size 31795
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb944eca56724f6a2106ea8db2043dc94c0ea40bdd4cdeb0e520790f97cc9598
|
||||
size 27049
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c129436a0b1dbfae999adfe0dcc6f5c4e0683c4e9b9a1e52f4b7bbb85ce3a462
|
||||
size 27162
|
||||
oid sha256:5e4a6476a2bb8980a9207868b77a253c65c0ba8433f843bb17e622856695b720
|
||||
size 27686
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:322a50c522ba4ac67206332e1d251e121c8c3d5538ca7961880623b20f4933e5
|
||||
size 81732
|
||||
oid sha256:5c1951b99908326b3f05ebb72aa4d02d0f297bdd925f38ded09041fae45400c1
|
||||
size 85217
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff
|
||||
size 11741
|
||||
oid sha256:eb2bc4a38f20ed0f5fced36e8e56936bee328b24a0a45127d5d3739d40331cb7
|
||||
size 15514
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c303fa620a2c7bc491a0ac1f9afdf9601b352e0e5163526c5f8732edf6bd6b3
|
||||
size 63404
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d6ba28dacacf5b6f67746fb5187b601e222fd6baf190af2248fdc98909fc17fd
|
||||
size 25921
|
||||
oid sha256:0df751bac5947c9bf6f82d075cf5670a562742b80d6c512bcd642da5ed449d26
|
||||
size 25975
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8196e08717f16c5ad17d0f84a4e57e63bb5a51c8f2b171071bf983af18ec161d
|
||||
size 20834
|
||||
oid sha256:0e26e87f2909414b614278a1cf0b485cda425aceb5419906426615dccdcbef2b
|
||||
size 20877
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df029c69651ee452cc4b265828280e47ffbcafb2958d71d67a5fe38f5211afe7
|
||||
size 10788
|
||||
oid sha256:20e3050bd41c7b9d225feb71f3bea3fdd1b8f749f77c4d140b5e560f53eb32b7
|
||||
size 10731
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0a62d309912501be8a5de7af4f1039a2a5731b1ed76fad17527f5783a5375f42
|
||||
size 133230
|
||||
oid sha256:1227636b03a7d35db3482b19f6059ec7aaf03ca795edadd5338056be6f6a8f7f
|
||||
size 126724
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70
|
||||
size 24345
|
||||
oid sha256:c01e96bf0aab24dbcfc05f2a6dcb0ffcddff69ec2474797de4fbce0a0670a8cd
|
||||
size 24964
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec357eafd145194f99c36a53a149a8b331fd691c5088df43ee96282b84bc81a4
|
||||
size 99439
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bae5f410ed30ef4dba6f3b529ae20e34a26f6c15c4cafd197899cf876271f5f1
|
||||
size 17828
|
||||
oid sha256:b5c95085e3c78b3fa1cc39ebd032834bd5ab5a80c3a2cad482d8a5bcbad004b9
|
||||
size 18064
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/demos/ID Test.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/demos/ID Test.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:28a69a52c07344576f2b5335497151e4e923b838dfaec9791402949ffb099c12
|
||||
size 116116
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e1378d865af3df02e12a0c4bc087620a4e9ef0029221db3180cdd2fd34f69d7f
|
||||
size 24832
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb3f7b5f790830b46d1410c2bbb5e19c6beb403f8fe979eb8d250fba4f89be3e
|
||||
size 51670
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68ded8dccceb3da2764243f2a554c2b4cf825fca09008d60dd520c7fbb2c5d3e
|
||||
size 22445
|
||||
oid sha256:70b00222e6c63f97bfd8c7a179c15cfaba93f8f2566702d4b03997f4714fe6cb
|
||||
size 22609
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad26106a86a6236f0db1c51bed754b370530813e9bb6e36c1be2948820fbef25
|
||||
size 47827
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:afa66ba8daca5c00f9c49b1d9173ad8f5e826247d3a9369d7e7c360cbdfcb72e
|
||||
size 22928
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d55baa6e3d4af44a35ec847639c35f968b05ad907352c45b3eb09cce6cd24280
|
||||
size 64357
|
||||
oid sha256:170cee9d72a4ab59aa2faf1b77aff4a9eee64f3380aa3f1b256340d88b1dabc2
|
||||
size 66525
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:98f7210fa72bdb00364e3576aefca126a6f31eff52870d116ba74c167354b13b
|
||||
size 32533
|
||||
oid sha256:90ab689d8a5034f5cab2ae2b44a8054d6dd815b3d295bee040c5bfcdf4564dee
|
||||
size 33063
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7868d662bd61d490dce9c049fca6c6e6b978255664fa709e959891bb40a7d434
|
||||
size 36577
|
||||
oid sha256:bf7f0a76424a959ede7afbb0eaf777638038cc6fe208ef710d9d82638d68b4d0
|
||||
size 37848
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fb3d031b8f658a90cf98e7a7bc5e0d7a3b601d742e2a9469cd115e7466e06524
|
||||
size 17628
|
||||
oid sha256:2d2370972781f15a1d602deca28bca38f1c077152801870edf2112650b8b1349
|
||||
size 17708
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ace9a6626446f8e29ec4c3f688e60cbeb86e79ad962044858aabe33a9c3d0e9
|
||||
size 264538
|
||||
oid sha256:f7a7d0e2618b852b5966073438c95cb62901d5410c1473639920b0b0bf2ec59b
|
||||
size 256913
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/demos/Popups.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/demos/Popups.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8b937a8a63de6fedcd0f9748b1d04cd863331a297bec78906885a0107def32a
|
||||
size 61242
|
||||
3
crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a11b0aeb8b8a7ff3acd54b99869af03cd04cc2edf13afc713ce924c52d39262d
|
||||
size 24826
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94
|
||||
size 35451
|
||||
oid sha256:2855bd95ab33b5232edada1f65684bbba2748025b6b64eb9ac68a5f2d10ad4bd
|
||||
size 34491
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89efd018caac097a5f9be37dcae15fc60b1475c72fc913ec9940540344e0b09f
|
||||
size 23622
|
||||
oid sha256:be599ae66323140bba4a7d63546acbf84340b57e2d82d4736bf3fe590040319d
|
||||
size 23623
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22c89f7b9b84563d6ee7db0d9a66f6b95c9034261fdffca53ae9737d70d2b376
|
||||
size 183881
|
||||
oid sha256:01c3cb5e8972e0cab5325328f93af8f51b35a0d61016e74969eab0f7ddea1e02
|
||||
size 176973
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:316c172a936f215afdcc45e7f5b32400e6acd759551adb2cc741f7121b9d83eb
|
||||
size 117790
|
||||
oid sha256:f2f6cedc262259d52c1fbf4283d99b4b62ec732e8688b1e2799a2581425e0564
|
||||
size 120342
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88913690a2b225ca634e38406a6a852250019a19d9bb33a4242e77c10fe88422
|
||||
size 26142
|
||||
oid sha256:142f65cee971f82a4917734c4f49ae233aa9a873028dca8c807d2825672bf2b2
|
||||
size 26657
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43b8dae4a936bf56b92368fcef64ff2ce2518aabc534a77fa578730493034f0f
|
||||
size 70536
|
||||
oid sha256:72f4c6fe4f5ec243506152027e1150f3069caf98511ceef92b8fea4f6a1563d5
|
||||
size 77614
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc24f146adf0282cfb51723b56c76eceb92f2988fc67bbeefd16b93950505922
|
||||
size 70110
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a307ac48abc79548c16468b3606a5df283ab2a5ac28345bd801bcc3887063414
|
||||
size 66384
|
||||
oid sha256:77aeaa1dcd391a571cb38732686e0b85b2d727975c02507a114d4e932f2c351b
|
||||
size 65562
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6311be2b850b5e41ac6dadf639b00584438b56f651a3c8d75ac8f5e06c9ad6fa
|
||||
size 21224
|
||||
oid sha256:5f964939ed1b3904706592915ca4fbbb951855ac88b466c51b835cd1c7467fb0
|
||||
size 21501
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0
|
||||
size 59662
|
||||
oid sha256:415b1ce17dd6df7ca7a86fed92750c2ef811ff64720a447ae3ca6be10090666e
|
||||
size 64624
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3584f16229bae50cc04b31df6bf5ccf43288fd05b447b34b29f118eb7435a090
|
||||
size 13103
|
||||
oid sha256:0eaf717bf0083737c4186ac39e7baf98f42fffb36b49434a6658eff1430a0ac6
|
||||
size 13187
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3411a4a8939b7e731c9c1a6331921b0ac905f4e3e86a51af70bdb38d9446f5e1
|
||||
size 35193
|
||||
oid sha256:611a2d6c793a85eebe807b2ddd4446cc0bc21e4284343dd756e64f0232fb6815
|
||||
size 35991
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b85a2af24c3361a0008fd0996e8d7244dc3e289646ec7233e8bad39a586c871c
|
||||
size 44512
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:614046db82ef103f65bec3c09cc6afd9ee2b3835e9d32e2adff98f6d56714b22
|
||||
size 807
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6298072b162623cec47d268fed5f8aa6189a2cf69074924a6eba26994fc6330
|
||||
size 2027
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67b709c116f56fba7e4e9f182018e84f46f6c6dd33a51f9d0524125dc2056b8c
|
||||
size 12950
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b0a70c0d66306edbbc6f77d03ea624aa68b846656811d4cc7d76d28572d177b
|
||||
size 30723
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:137ba69ac73a5e9aacc6bf3bbd589e8640b41c50ccfb49edcda4e2d6efed6c09
|
||||
size 13384
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f798c666ad21d3f9bb57826e30f2f6ef044543bc05af8c185e0e63c8297e824
|
||||
size 33181
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44fc7d745b478fe937fa7c131871a00b26712a0317aaa027a088782533be6136
|
||||
size 7125
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abfa00ef9385d380bbd188d6254f92d6839a94f368100e75a2780337438f969f
|
||||
size 11068
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e5c9015e2005429ba83a407ed1f7d4dfbf30624f666152e82079c6ed3b3cda5
|
||||
size 17238
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3319a8bca1213fc3a2dd91ead155be1e25045bc614701250bc961848cfc42176
|
||||
size 7327
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5172aa12f07b4abf4bb217b8952b4cf5cf61b688455751964a1b54433d8c05b1
|
||||
size 11709
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e3eedd952d4416af73179451c0c90bcb76635c9c3c94d37f42bdd228ddbdd03
|
||||
size 18802
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user