1
0
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:
Emil Ernerfeldt
2025-11-16 11:31:23 +01:00
575 changed files with 22977 additions and 10608 deletions

View File

@@ -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"

View File

@@ -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"
```

View File

@@ -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(),

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -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| {

View File

@@ -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)

View File

@@ -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");
}
}

View File

@@ -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)]

View File

@@ -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
}
}

View File

@@ -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))]

View File

@@ -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| {

View File

@@ -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:

View File

@@ -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)]

View File

@@ -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);
});
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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))]

View File

@@ -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!())
}

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

View 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 {

View File

@@ -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!

View File

@@ -1,4 +1,4 @@
use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui};
use egui::{Slider, SliderClamping, SliderOrientation, Ui, style::HandleShape};
/// Showcase sliders
#[derive(PartialEq)]

View File

@@ -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)
}

View File

@@ -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());
}
});

View File

@@ -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();

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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))]

View File

@@ -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;

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

View File

@@ -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}"));
}
}
}

View File

@@ -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();

View File

@@ -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()
},

View File

@@ -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!())
}

View 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| {

View File

@@ -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();
}
});

View File

@@ -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);
}
}
}
}

View File

@@ -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!());
});
}

View 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.

View File

@@ -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));

View File

@@ -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));
}
}
}

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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}")));
}
}
}

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

View 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: 123456789");
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");
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90
size 31780
oid sha256:30929184fab7e7d5975243d86bcab79cd9f7a0c5d57dd9ae827464ff6570be7b
size 31795

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb944eca56724f6a2106ea8db2043dc94c0ea40bdd4cdeb0e520790f97cc9598
size 27049

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c129436a0b1dbfae999adfe0dcc6f5c4e0683c4e9b9a1e52f4b7bbb85ce3a462
size 27162
oid sha256:5e4a6476a2bb8980a9207868b77a253c65c0ba8433f843bb17e622856695b720
size 27686

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:322a50c522ba4ac67206332e1d251e121c8c3d5538ca7961880623b20f4933e5
size 81732
oid sha256:5c1951b99908326b3f05ebb72aa4d02d0f297bdd925f38ded09041fae45400c1
size 85217

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff
size 11741
oid sha256:eb2bc4a38f20ed0f5fced36e8e56936bee328b24a0a45127d5d3739d40331cb7
size 15514

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c303fa620a2c7bc491a0ac1f9afdf9601b352e0e5163526c5f8732edf6bd6b3
size 63404

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6ba28dacacf5b6f67746fb5187b601e222fd6baf190af2248fdc98909fc17fd
size 25921
oid sha256:0df751bac5947c9bf6f82d075cf5670a562742b80d6c512bcd642da5ed449d26
size 25975

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8196e08717f16c5ad17d0f84a4e57e63bb5a51c8f2b171071bf983af18ec161d
size 20834
oid sha256:0e26e87f2909414b614278a1cf0b485cda425aceb5419906426615dccdcbef2b
size 20877

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df029c69651ee452cc4b265828280e47ffbcafb2958d71d67a5fe38f5211afe7
size 10788
oid sha256:20e3050bd41c7b9d225feb71f3bea3fdd1b8f749f77c4d140b5e560f53eb32b7
size 10731

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a62d309912501be8a5de7af4f1039a2a5731b1ed76fad17527f5783a5375f42
size 133230
oid sha256:1227636b03a7d35db3482b19f6059ec7aaf03ca795edadd5338056be6f6a8f7f
size 126724

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70
size 24345
oid sha256:c01e96bf0aab24dbcfc05f2a6dcb0ffcddff69ec2474797de4fbce0a0670a8cd
size 24964

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec357eafd145194f99c36a53a149a8b331fd691c5088df43ee96282b84bc81a4
size 99439

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bae5f410ed30ef4dba6f3b529ae20e34a26f6c15c4cafd197899cf876271f5f1
size 17828
oid sha256:b5c95085e3c78b3fa1cc39ebd032834bd5ab5a80c3a2cad482d8a5bcbad004b9
size 18064

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:28a69a52c07344576f2b5335497151e4e923b838dfaec9791402949ffb099c12
size 116116

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1378d865af3df02e12a0c4bc087620a4e9ef0029221db3180cdd2fd34f69d7f
size 24832

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb3f7b5f790830b46d1410c2bbb5e19c6beb403f8fe979eb8d250fba4f89be3e
size 51670

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:68ded8dccceb3da2764243f2a554c2b4cf825fca09008d60dd520c7fbb2c5d3e
size 22445
oid sha256:70b00222e6c63f97bfd8c7a179c15cfaba93f8f2566702d4b03997f4714fe6cb
size 22609

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad26106a86a6236f0db1c51bed754b370530813e9bb6e36c1be2948820fbef25
size 47827

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afa66ba8daca5c00f9c49b1d9173ad8f5e826247d3a9369d7e7c360cbdfcb72e
size 22928

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d55baa6e3d4af44a35ec847639c35f968b05ad907352c45b3eb09cce6cd24280
size 64357
oid sha256:170cee9d72a4ab59aa2faf1b77aff4a9eee64f3380aa3f1b256340d88b1dabc2
size 66525

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98f7210fa72bdb00364e3576aefca126a6f31eff52870d116ba74c167354b13b
size 32533
oid sha256:90ab689d8a5034f5cab2ae2b44a8054d6dd815b3d295bee040c5bfcdf4564dee
size 33063

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7868d662bd61d490dce9c049fca6c6e6b978255664fa709e959891bb40a7d434
size 36577
oid sha256:bf7f0a76424a959ede7afbb0eaf777638038cc6fe208ef710d9d82638d68b4d0
size 37848

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb3d031b8f658a90cf98e7a7bc5e0d7a3b601d742e2a9469cd115e7466e06524
size 17628
oid sha256:2d2370972781f15a1d602deca28bca38f1c077152801870edf2112650b8b1349
size 17708

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ace9a6626446f8e29ec4c3f688e60cbeb86e79ad962044858aabe33a9c3d0e9
size 264538
oid sha256:f7a7d0e2618b852b5966073438c95cb62901d5410c1473639920b0b0bf2ec59b
size 256913

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8b937a8a63de6fedcd0f9748b1d04cd863331a297bec78906885a0107def32a
size 61242

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a11b0aeb8b8a7ff3acd54b99869af03cd04cc2edf13afc713ce924c52d39262d
size 24826

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94
size 35451
oid sha256:2855bd95ab33b5232edada1f65684bbba2748025b6b64eb9ac68a5f2d10ad4bd
size 34491

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89efd018caac097a5f9be37dcae15fc60b1475c72fc913ec9940540344e0b09f
size 23622
oid sha256:be599ae66323140bba4a7d63546acbf84340b57e2d82d4736bf3fe590040319d
size 23623

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:22c89f7b9b84563d6ee7db0d9a66f6b95c9034261fdffca53ae9737d70d2b376
size 183881
oid sha256:01c3cb5e8972e0cab5325328f93af8f51b35a0d61016e74969eab0f7ddea1e02
size 176973

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:316c172a936f215afdcc45e7f5b32400e6acd759551adb2cc741f7121b9d83eb
size 117790
oid sha256:f2f6cedc262259d52c1fbf4283d99b4b62ec732e8688b1e2799a2581425e0564
size 120342

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88913690a2b225ca634e38406a6a852250019a19d9bb33a4242e77c10fe88422
size 26142
oid sha256:142f65cee971f82a4917734c4f49ae233aa9a873028dca8c807d2825672bf2b2
size 26657

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43b8dae4a936bf56b92368fcef64ff2ce2518aabc534a77fa578730493034f0f
size 70536
oid sha256:72f4c6fe4f5ec243506152027e1150f3069caf98511ceef92b8fea4f6a1563d5
size 77614

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc24f146adf0282cfb51723b56c76eceb92f2988fc67bbeefd16b93950505922
size 70110

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a307ac48abc79548c16468b3606a5df283ab2a5ac28345bd801bcc3887063414
size 66384
oid sha256:77aeaa1dcd391a571cb38732686e0b85b2d727975c02507a114d4e932f2c351b
size 65562

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6311be2b850b5e41ac6dadf639b00584438b56f651a3c8d75ac8f5e06c9ad6fa
size 21224
oid sha256:5f964939ed1b3904706592915ca4fbbb951855ac88b466c51b835cd1c7467fb0
size 21501

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0
size 59662
oid sha256:415b1ce17dd6df7ca7a86fed92750c2ef811ff64720a447ae3ca6be10090666e
size 64624

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3584f16229bae50cc04b31df6bf5ccf43288fd05b447b34b29f118eb7435a090
size 13103
oid sha256:0eaf717bf0083737c4186ac39e7baf98f42fffb36b49434a6658eff1430a0ac6
size 13187

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3411a4a8939b7e731c9c1a6331921b0ac905f4e3e86a51af70bdb38d9446f5e1
size 35193
oid sha256:611a2d6c793a85eebe807b2ddd4446cc0bc21e4284343dd756e64f0232fb6815
size 35991

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b85a2af24c3361a0008fd0996e8d7244dc3e289646ec7233e8bad39a586c871c
size 44512

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:614046db82ef103f65bec3c09cc6afd9ee2b3835e9d32e2adff98f6d56714b22
size 807

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6298072b162623cec47d268fed5f8aa6189a2cf69074924a6eba26994fc6330
size 2027

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67b709c116f56fba7e4e9f182018e84f46f6c6dd33a51f9d0524125dc2056b8c
size 12950

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b0a70c0d66306edbbc6f77d03ea624aa68b846656811d4cc7d76d28572d177b
size 30723

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:137ba69ac73a5e9aacc6bf3bbd589e8640b41c50ccfb49edcda4e2d6efed6c09
size 13384

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f798c666ad21d3f9bb57826e30f2f6ef044543bc05af8c185e0e63c8297e824
size 33181

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44fc7d745b478fe937fa7c131871a00b26712a0317aaa027a088782533be6136
size 7125

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abfa00ef9385d380bbd188d6254f92d6839a94f368100e75a2780337438f969f
size 11068

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e5c9015e2005429ba83a407ed1f7d4dfbf30624f666152e82079c6ed3b3cda5
size 17238

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3319a8bca1213fc3a2dd91ead155be1e25045bc614701250bc961848cfc42176
size 7327

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5172aa12f07b4abf4bb217b8952b4cf5cf61b688455751964a1b54433d8c05b1
size 11709

View File

@@ -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