1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 22:00:03 -04:00

Merge branch 'master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2025-03-26 18:52:43 +01:00
319 changed files with 10259 additions and 5112 deletions

View File

@@ -10,7 +10,7 @@ license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
categories = ["gui", "graphics"]
keywords = ["glium", "egui", "gui", "gamedev"]
keywords = ["glow", "egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
@@ -57,7 +57,6 @@ serde = { workspace = true, optional = true }
[dev-dependencies]
criterion.workspace = true
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
wgpu = { workspace = true, features = ["metal"] }
egui = { workspace = true, features = ["default_fonts"] }
rand = "0.8"

View File

@@ -79,7 +79,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let painter = ui.painter();
let rect = ui.max_rect();
b.iter(|| {
painter.rect(rect, 2.0, egui::Color32::RED, (1.0, egui::Color32::WHITE));
painter.rect(
rect,
2.0,
egui::Color32::RED,
(1.0, egui::Color32::WHITE),
egui::StrokeKind::Inside,
);
});
});
});

View File

@@ -98,14 +98,14 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
}
fn links(ui: &mut egui::Ui) {
use egui::special_emojis::{GITHUB, TWITTER};
use egui::special_emojis::GITHUB;
ui.hyperlink_to(
format!("{GITHUB} github.com/emilk/egui"),
"https://github.com/emilk/egui",
);
ui.hyperlink_to(
format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt",
"@ernerfeldt.bsky.social",
"https://bsky.app/profile/ernerfeldt.bsky.social",
);
ui.hyperlink_to("📓 egui documentation", "https://docs.rs/egui/");
}

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,12 +1,12 @@
use std::collections::BTreeSet;
use egui::{Context, Modifiers, ScrollArea, Ui};
use super::About;
use crate::is_mobile;
use crate::Demo;
use crate::View;
use egui::containers::menu;
use egui::style::StyleModifier;
use egui::{Context, Modifiers, ScrollArea, Ui};
// ----------------------------------------------------------------------------
struct DemoGroup {
@@ -65,7 +65,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(),
@@ -77,8 +76,9 @@ impl Default for DemoGroups {
Box::<super::modals::Modals>::default(),
Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::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(),
Box::<super::sliders::Sliders>::default(),
@@ -100,6 +100,7 @@ impl Default for DemoGroups {
Box::<super::tests::InputTest>::default(),
Box::<super::tests::LayoutTest>::default(),
Box::<super::tests::ManualLayoutTest>::default(),
Box::<super::tests::TessellationTest>::default(),
Box::<super::tests::WindowResizeTest>::default(),
]),
}
@@ -225,29 +226,27 @@ impl DemoWindows {
fn mobile_top_bar(&mut self, ctx: &Context) {
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
let font_size = 16.5;
menu::Bar::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, TWITTER};
ui.hyperlink_to(
egui::RichText::new(TWITTER).size(font_size),
"https://twitter.com/ernerfeldt",
);
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",
);
});
});
});
});
}
@@ -264,14 +263,14 @@ impl DemoWindows {
ui.separator();
use egui::special_emojis::{GITHUB, TWITTER};
use egui::special_emojis::GITHUB;
ui.hyperlink_to(
format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui",
);
ui.hyperlink_to(
format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt",
"@ernerfeldt.bsky.social",
"https://bsky.app/profile/ernerfeldt.bsky.social",
);
ui.separator();
@@ -280,7 +279,7 @@ impl DemoWindows {
});
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
menu::Bar::new().ui(ui, |ui| {
file_menu_button(ui);
});
});
@@ -343,7 +342,6 @@ fn file_menu_button(ui: &mut Ui) {
.clicked()
{
ui.ctx().memory_mut(|mem| mem.reset_areas());
ui.close_menu();
}
if ui
@@ -355,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
.clicked()
{
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
});
}
@@ -365,13 +362,13 @@ mod tests {
use crate::{demo::demo_app_windows::DemoGroups, Demo};
use egui::Vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::{Harness, SnapshotOptions};
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
#[test]
fn demos_should_match_snapshot() {
let demos = DemoGroups::default().demos;
let mut errors = Vec::new();
let mut results = SnapshotResults::new();
for mut demo in demos.demos {
// Widget Gallery needs to be customized (to set a specific date) and has its own test
@@ -397,7 +394,7 @@ mod tests {
harness.set_size(Vec2::new(size.width as f32, size.height as f32));
// Run the app for some more frames...
harness.run();
harness.run_ok();
let mut options = SnapshotOptions::default();
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux
@@ -405,12 +402,7 @@ mod tests {
options.threshold = 2.1;
}
let result = harness.try_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result {
errors.push(err.to_string());
}
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
}
assert!(errors.is_empty(), "Errors: {errors:#?}");
}
}

View File

@@ -200,7 +200,6 @@ fn special_char_name(chr: char) -> Option<&'static str> {
'\u{E600}' => Some("web-dribbble"),
'\u{E601}' => Some("web-stackoverflow"),
'\u{E602}' => Some("web-vimeo"),
'\u{E603}' => Some("web-twitter"),
'\u{E604}' => Some("web-facebook"),
'\u{E605}' => Some("web-googleplus"),
'\u{E606}' => Some("web-pinterest"),

View File

@@ -7,19 +7,18 @@ pub struct FrameDemo {
impl Default for FrameDemo {
fn default() -> Self {
Self {
frame: egui::Frame {
inner_margin: 12.0.into(),
outer_margin: 24.0.into(),
rounding: 14.0.into(),
shadow: egui::Shadow {
frame: egui::Frame::new()
.inner_margin(12)
.outer_margin(24)
.corner_radius(14)
.shadow(egui::Shadow {
offset: [8, 12],
blur: 16,
spread: 0,
color: egui::Color32::from_black_alpha(180),
},
fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128),
stroke: egui::Stroke::new(1.0, egui::Color32::GRAY),
},
})
.fill(egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128))
.stroke(egui::Stroke::new(1.0, egui::Color32::GRAY)),
}
}
}
@@ -57,7 +56,7 @@ impl crate::View for FrameDemo {
// We want to paint a background around the outer margin of the demonstration frame, so we use another frame around it:
egui::Frame::default()
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.rounding(ui.visuals().widgets.noninteractive.rounding)
.corner_radius(ui.visuals().widgets.noninteractive.corner_radius)
.show(ui, |ui| {
self.frame.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);

View File

@@ -1,8 +1,8 @@
use super::{Demo, View};
use egui::{
vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Frame, Resize, RichText,
Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window,
vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Resize, RichText, Sense,
Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window,
};
/// Showcase some ui code
@@ -124,9 +124,9 @@ impl View for MiscDemoWindow {
)
.changed()
{
self.checklist
.iter_mut()
.for_each(|checked| *checked = all_checked);
for check in &mut self.checklist {
*check = all_checked;
}
}
for (i, checked) in self.checklist.iter_mut().enumerate() {
ui.checkbox(checked, format!("Item {}", i + 1));
@@ -358,7 +358,7 @@ impl ColorWidgets {
#[cfg_attr(feature = "serde", serde(default))]
struct BoxPainting {
size: Vec2,
rounding: f32,
corner_radius: f32,
stroke_width: f32,
num_boxes: usize,
}
@@ -367,7 +367,7 @@ impl Default for BoxPainting {
fn default() -> Self {
Self {
size: vec2(64.0, 32.0),
rounding: 5.0,
corner_radius: 5.0,
stroke_width: 2.0,
num_boxes: 1,
}
@@ -378,7 +378,7 @@ impl BoxPainting {
pub fn ui(&mut self, ui: &mut Ui) {
ui.add(Slider::new(&mut self.size.x, 0.0..=500.0).text("width"));
ui.add(Slider::new(&mut self.size.y, 0.0..=500.0).text("height"));
ui.add(Slider::new(&mut self.rounding, 0.0..=50.0).text("rounding"));
ui.add(Slider::new(&mut self.corner_radius, 0.0..=50.0).text("corner_radius"));
ui.add(Slider::new(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
ui.add(Slider::new(&mut self.num_boxes, 0..=8).text("num_boxes"));
@@ -387,9 +387,10 @@ impl BoxPainting {
let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover());
ui.painter().rect(
rect,
self.rounding,
self.corner_radius,
ui.visuals().text_color().gamma_multiply(0.5),
Stroke::new(self.stroke_width, Color32::WHITE),
egui::StrokeKind::Inside,
);
}
});
@@ -512,54 +513,52 @@ fn ui_stack_demo(ui: &mut Ui) {
);
});
let stack = ui.stack().clone();
Frame {
inner_margin: ui.spacing().menu_margin,
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
..Default::default()
}
.show(ui, |ui| {
egui_extras::TableBuilder::new(ui)
.column(egui_extras::Column::auto())
.column(egui_extras::Column::auto())
.header(18.0, |mut header| {
header.col(|ui| {
ui.strong("id");
});
header.col(|ui| {
ui.strong("kind");
});
})
.body(|mut body| {
for node in stack.iter() {
body.row(18.0, |mut row| {
row.col(|ui| {
let response = ui.label(format!("{:?}", node.id));
egui::Frame::new()
.inner_margin(ui.spacing().menu_margin)
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.show(ui, |ui| {
egui_extras::TableBuilder::new(ui)
.column(egui_extras::Column::auto())
.column(egui_extras::Column::auto())
.header(18.0, |mut header| {
header.col(|ui| {
ui.strong("id");
});
header.col(|ui| {
ui.strong("kind");
});
})
.body(|mut body| {
for node in stack.iter() {
body.row(18.0, |mut row| {
row.col(|ui| {
let response = ui.label(format!("{:?}", node.id));
if response.hovered() {
ui.ctx().debug_painter().debug_rect(
node.max_rect,
Color32::GREEN,
"max_rect",
);
ui.ctx().debug_painter().circle_filled(
node.min_rect.min,
2.0,
Color32::RED,
);
}
});
if response.hovered() {
ui.ctx().debug_painter().debug_rect(
node.max_rect,
Color32::GREEN,
"max_rect",
);
ui.ctx().debug_painter().circle_filled(
node.min_rect.min,
2.0,
Color32::RED,
);
}
});
row.col(|ui| {
ui.label(if let Some(kind) = node.kind() {
format!("{kind:?}")
} else {
"-".to_owned()
row.col(|ui| {
ui.label(if let Some(kind) = node.kind() {
format!("{kind:?}")
} else {
"-".to_owned()
});
});
});
});
}
});
});
}
});
});
ui.small("Hover on UI's ids to display their origin and max rect.");
}

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;
@@ -21,9 +20,10 @@ pub mod modals;
pub mod multi_touch;
pub mod paint_bezier;
pub mod painting;
pub mod pan_zoom;
pub mod panels;
pub mod password;
mod popups;
pub mod scene;
pub mod screenshot;
pub mod scrolling;
pub mod sliders;

View File

@@ -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();
}
},
);
@@ -165,7 +167,7 @@ mod tests {
use egui::accesskit::Role;
use egui::Key;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use egui_kittest::{Harness, SnapshotResults};
#[test]
fn clicking_escape_when_popup_open_should_not_close_modal() {
@@ -183,12 +185,13 @@ mod tests {
harness.get_by_role(Role::ComboBox).click();
harness.run();
// 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!(harness.state().user_modal_open);
harness.press_key(Key::Escape);
harness.run();
harness.run_ok();
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
}
@@ -232,28 +235,18 @@ mod tests {
initial_state,
);
let mut results = Vec::new();
let mut results = SnapshotResults::new();
harness.run();
results.push(harness.try_snapshot("modals_1"));
results.add(harness.try_snapshot("modals_1"));
harness.get_by_label("Save").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_snapshot("modals_2"));
harness.run_ok();
results.add(harness.try_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_snapshot("modals_3"));
for result in results {
result.unwrap();
}
harness.run_ok();
results.add(harness.try_snapshot("modals_3"));
}
// This tests whether the backdrop actually prevents interaction with lower layers.
@@ -272,14 +265,11 @@ mod tests {
initial_state,
);
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.run_ok();
harness.get_by_label("Yes Please").simulate_click();
harness.run();
harness.run_ok();
// This snapshots should show the progress bar modal on top of the save modal.
harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area");

View File

@@ -1,6 +1,7 @@
use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape};
use egui::{
emath, epaint, pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, Ui, Vec2,
emath,
epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape},
pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
Widget, Window,
};
@@ -132,6 +133,7 @@ impl PaintBezier {
shape.visual_bounding_rect(),
0.0,
self.bounding_box_stroke,
StrokeKind::Outside,
));
painter.add(shape);
}
@@ -143,6 +145,7 @@ impl PaintBezier {
shape.visual_bounding_rect(),
0.0,
self.bounding_box_stroke,
StrokeKind::Outside,
));
painter.add(shape);
}

View File

@@ -1,145 +0,0 @@
use egui::emath::TSTransform;
use egui::TextWrapMode;
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PanZoom {
transform: TSTransform,
drag_value: f32,
}
impl Eq for PanZoom {}
impl crate::Demo for PanZoom {
fn name(&self) -> &'static str {
"🔍 Pan Zoom"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use crate::View as _;
let window = egui::Window::new("Pan Zoom")
.default_width(300.0)
.default_height(300.0)
.vscroll(false)
.open(open);
window.show(ctx, |ui| self.ui(ui));
}
}
impl crate::View for PanZoom {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(
"Pan, zoom in, and zoom out with scrolling (see the plot demo for more instructions). \
Double click on the background to reset.",
);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
ui.separator();
let (id, rect) = ui.allocate_space(ui.available_size());
let response = ui.interact(rect, id, egui::Sense::click_and_drag());
// Allow dragging the background as well.
if response.dragged() {
self.transform.translation += response.drag_delta();
}
// Plot-like reset
if response.double_clicked() {
self.transform = TSTransform::default();
}
let transform =
TSTransform::from_translation(ui.min_rect().left_top().to_vec2()) * self.transform;
if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
// Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered.
if response.hovered() {
let pointer_in_layer = transform.inverse() * pointer;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
// Zoom in on pointer:
self.transform = self.transform
* TSTransform::from_translation(pointer_in_layer.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_layer.to_vec2());
// Pan:
self.transform = TSTransform::from_translation(pan_delta) * self.transform;
}
}
for (i, (pos, callback)) in [
(
egui::Pos2::new(0.0, 0.0),
Box::new(|ui: &mut egui::Ui, _: &mut Self| {
ui.button("top left").on_hover_text("Normal tooltip")
}) as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
),
(
egui::Pos2::new(0.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("bottom left").on_hover_text("Normal tooltip")
}),
),
(
egui::Pos2::new(120.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("right bottom")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
),
(
egui::Pos2::new(120.0, 0.0),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("right top")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
),
(
egui::Pos2::new(60.0, 60.0),
Box::new(|ui, state| {
use egui::epaint::{pos2, CircleShape, Color32, QuadraticBezierShape, Stroke};
// Smiley face.
let painter = ui.painter();
painter.add(CircleShape::filled(pos2(0.0, -10.0), 1.0, Color32::YELLOW));
painter.add(CircleShape::filled(pos2(10.0, -10.0), 1.0, Color32::YELLOW));
painter.add(QuadraticBezierShape::from_points_stroke(
[pos2(0.0, 0.0), pos2(5.0, 3.0), pos2(10.0, 0.0)],
false,
Color32::TRANSPARENT,
Stroke::new(1.0, Color32::YELLOW),
));
ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value"))
}),
),
]
.into_iter()
.enumerate()
{
let window_layer = ui.layer_id();
let id = egui::Area::new(id.with(("subarea", i)))
.default_pos(pos)
.order(egui::Order::Middle)
.constrain(false)
.show(ui.ctx(), |ui| {
ui.set_clip_rect(transform.inverse() * rect);
egui::Frame::default()
.rounding(egui::Rounding::same(4))
.inner_margin(egui::Margin::same(8))
.stroke(ui.ctx().style().visuals.window_stroke)
.fill(ui.style().visuals.panel_fill)
.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
callback(ui, self)
});
})
.response
.layer_id;
ui.ctx().set_transform_layer(id, transform);
ui.ctx().set_sublayer(window_layer, id);
}
}
}

View File

@@ -0,0 +1,306 @@
use crate::rust_view_ui;
use egui::color_picker::{color_picker_color32, Alpha};
use egui::containers::menu::{MenuConfig, SubMenuButton};
use egui::{
include_image, Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior,
RectAlign, RichText, Tooltip, Ui, UiBuilder,
};
/// 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 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.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 mut color_button =
SubMenuButton::new(RichText::new("Background").color(text_color));
color_button.button = color_button.button.fill(self.color);
color_button.button = color_button
.button
.right_text(RichText::new(SubMenuButton::RIGHT_ARROW).color(text_color));
color_button.ui(ui, |ui| {
ui.spacing_mut().slider_width = 200.0;
color_picker_color32(ui, &mut self.color, Alpha::Opaque);
});
if ui.button("Open…").clicked() {
ui.close();
}
});
}
}
impl Default for PopupsDemo {
fn default() -> Self {
Self {
align4: RectAlign::default(),
gap: 4.0,
close_behavior: PopupCloseBehavior::CloseOnClick,
popup_open: false,
checked: false,
color: egui::Color32::RED,
}
}
}
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

@@ -0,0 +1,82 @@
use egui::{Pos2, Rect, Scene, Vec2};
use super::widget_gallery;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SceneDemo {
widget_gallery: widget_gallery::WidgetGallery,
scene_rect: Rect,
}
impl Default for SceneDemo {
fn default() -> Self {
Self {
widget_gallery: widget_gallery::WidgetGallery::default().with_date_button(false), // disable date button so that we don't fail the snapshot test
scene_rect: Rect::ZERO, // `egui::Scene` will initialize this to something valid
}
}
}
impl crate::Demo for SceneDemo {
fn name(&self) -> &'static str {
"🔍 Scene"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use crate::View as _;
let window = egui::Window::new("Scene")
.default_width(300.0)
.default_height(300.0)
.scroll(false)
.open(open);
window.show(ctx, |ui| self.ui(ui));
}
}
impl crate::View for SceneDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(
"You can pan by scrolling, and zoom using cmd-scroll. \
Double click on the background to reset view.",
);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
ui.separator();
ui.label(format!("Scene rect: {:#?}", &mut self.scene_rect));
ui.separator();
egui::Frame::group(ui.style())
.inner_margin(0.0)
.show(ui, |ui| {
let scene = Scene::new()
.max_inner_size([350.0, 1000.0])
.zoom_range(0.1..=2.0);
let mut reset_view = false;
let mut inner_rect = Rect::NAN;
let response = scene
.show(ui, &mut self.scene_rect, |ui| {
reset_view = ui.button("Reset view").clicked();
ui.add_space(16.0);
self.widget_gallery.ui(ui);
ui.put(
Rect::from_min_size(Pos2::new(0.0, -64.0), Vec2::new(200.0, 16.0)),
egui::Label::new("You can put a widget anywhere").selectable(false),
);
inner_rect = ui.min_rect();
})
.response;
if reset_view || response.double_clicked() {
self.scene_rect = inner_rect;
}
});
}
}

View File

@@ -16,6 +16,14 @@ impl crate::Demo for CursorTest {
impl crate::View for CursorTest {
fn ui(&mut self, ui: &mut egui::Ui) {
if ui
.button("Center pointer in window")
.on_hover_text("The platform may not support this.")
.clicked()
{
let position = ui.ctx().available_rect().center();
ui.ctx().set_pointer_position(position);
}
ui.vertical_centered_justified(|ui| {
ui.heading("Hover to switch cursor icon:");
for &cursor_icon in &egui::CursorIcon::ALL {

View File

@@ -6,6 +6,7 @@ mod input_event_history;
mod input_test;
mod layout_test;
mod manual_layout_test;
mod tessellation_test;
mod window_resize_test;
pub use clipboard_test::ClipboardTest;
@@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory;
pub use input_test::InputTest;
pub use layout_test::LayoutTest;
pub use manual_layout_test::ManualLayoutTest;
pub use tessellation_test::TessellationTest;
pub use window_resize_test::WindowResizeTest;

View File

@@ -0,0 +1,379 @@
use egui::{
emath::{GuiRounding, TSTransform},
epaint::{self, RectShape},
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
};
#[derive(Clone, Debug, PartialEq)]
pub struct TessellationTest {
shape: RectShape,
magnification_pixel_size: f32,
tessellation_options: epaint::TessellationOptions,
paint_edges: bool,
}
impl Default for TessellationTest {
fn default() -> Self {
let shape = Self::interesting_shapes()[0].1.clone();
Self {
shape,
magnification_pixel_size: 12.0,
tessellation_options: Default::default(),
paint_edges: false,
}
}
}
impl TessellationTest {
fn interesting_shapes() -> Vec<(&'static str, RectShape)> {
fn sized(size: impl Into<Vec2>) -> Rect {
Rect::from_center_size(Pos2::ZERO, size.into())
}
let baby_blue = Color32::from_rgb(0, 181, 255);
let mut shapes = vec![
(
"Normal",
RectShape::new(
sized([20.0, 16.0]),
2.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Thin filled",
RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue),
),
(
"Thin stroked",
RectShape::new(
sized([20.0, 0.5]),
2.0,
baby_blue,
(0.5, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred",
RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0),
),
(
"Thick stroke, minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(3.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred stroke",
RectShape::new(
sized([20.0, 16.0]),
0.0,
baby_blue,
(5.0, Color32::WHITE),
StrokeKind::Inside,
)
.with_blur_width(5.0),
),
(
"Additive rectangle",
RectShape::new(
sized([24.0, 12.0]),
0.0,
egui::Color32::LIGHT_RED.additive().linear_multiply(0.025),
(
1.0,
egui::Color32::LIGHT_BLUE.additive().linear_multiply(0.1),
),
StrokeKind::Outside,
),
),
];
for (_name, shape) in &mut shapes {
shape.round_to_pixels = Some(true);
}
shapes
}
}
impl crate::Demo for TessellationTest {
fn name(&self) -> &'static str {
"Tessellation Test"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.resizable(false)
.open(open)
.show(ctx, |ui| {
use crate::View as _;
self.ui(ui);
});
}
}
impl crate::View for TessellationTest {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.add(crate::egui_github_link_file!());
egui::reset_button(ui, self, "Reset");
ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
rect_shape_ui(ui, &mut self.shape);
});
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.heading("Real size");
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) =
ui.allocate_painter(Vec2::splat(128.0), Sense::hover());
let canvas = resp.rect;
let pixels_per_point = ui.pixels_per_point();
let pixel_size = 1.0 / pixels_per_point;
let mut shape = self.shape.clone();
shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size())
.round_to_pixel_center(pixels_per_point)
.translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding
painter.add(shape);
});
});
});
});
ui.group(|ui| {
ui.heading("Zoomed in");
let magnification_pixel_size = &mut self.magnification_pixel_size;
let tessellation_options = &mut self.tessellation_options;
egui::Grid::new("TessellationOptions")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Magnification");
ui.add(
egui::DragValue::new(magnification_pixel_size)
.speed(0.5)
.range(1.0..=32.0),
);
ui.end_row();
ui.label("Feathering width");
ui.horizontal(|ui| {
ui.checkbox(&mut tessellation_options.feathering, "");
ui.add_enabled(
tessellation_options.feathering,
egui::DragValue::new(
&mut tessellation_options.feathering_size_in_pixels,
)
.speed(0.1)
.range(0.0..=4.0)
.suffix(" px"),
);
});
ui.end_row();
ui.label("Paint edges");
ui.checkbox(&mut self.paint_edges, "");
ui.end_row();
});
let magnification_pixel_size = *magnification_pixel_size;
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) = ui.allocate_painter(
magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)),
Sense::hover(),
);
let canvas = resp.rect;
let mut shape = self.shape.clone();
shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding
let mut mesh = epaint::Mesh::default();
let mut tessellator = epaint::Tessellator::new(
1.0,
*tessellation_options,
ui.fonts(|f| f.font_image_size()),
vec![],
);
tessellator.tessellate_rect(&shape, &mut mesh);
// Scale and position the mesh:
mesh.transform(
TSTransform::from_translation(canvas.center().to_vec2())
* TSTransform::from_scaling(magnification_pixel_size),
);
let mesh = std::sync::Arc::new(mesh);
painter.add(epaint::Shape::mesh(mesh.clone()));
if self.paint_edges {
let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA);
for triangle in mesh.triangles() {
let a = mesh.vertices[triangle[0] as usize];
let b = mesh.vertices[triangle[1] as usize];
let c = mesh.vertices[triangle[2] as usize];
painter.line_segment([a.pos, b.pos], stroke);
painter.line_segment([b.pos, c.pos], stroke);
painter.line_segment([c.pos, a.pos], stroke);
}
}
if 3.0 < magnification_pixel_size {
// Draw pixel centers:
let pixel_radius = 0.75;
let pixel_color = Color32::GRAY;
for yi in 0.. {
let y = (yi as f32 + 0.5) * magnification_pixel_size;
if y > canvas.height() / 2.0 {
break;
}
for xi in 0.. {
let x = (xi as f32 + 0.5) * magnification_pixel_size;
if x > canvas.width() / 2.0 {
break;
}
for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] {
painter.circle_filled(
canvas.center() + offset,
pixel_radius,
pixel_color,
);
}
}
}
}
});
});
}
}
fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
egui::ComboBox::from_id_salt("prefabs")
.selected_text("Prefabs")
.show_ui(ui, |ui| {
for (name, prefab) in TessellationTest::interesting_shapes() {
ui.selectable_value(shape, prefab, name);
}
});
ui.add_space(4.0);
let RectShape {
rect,
corner_radius,
fill,
stroke,
stroke_kind,
blur_width,
round_to_pixels,
brush: _,
} = shape;
let round_to_pixels = round_to_pixels.get_or_insert(true);
egui::Grid::new("RectShape")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Size");
ui.horizontal(|ui| {
let mut size = rect.size();
ui.add(
egui::DragValue::new(&mut size.x)
.speed(0.2)
.range(0.0..=64.0),
);
ui.add(
egui::DragValue::new(&mut size.y)
.speed(0.2)
.range(0.0..=64.0),
);
*rect = Rect::from_center_size(Pos2::ZERO, size);
});
ui.end_row();
ui.label("Corner radius");
ui.add(corner_radius);
ui.end_row();
ui.label("Fill");
ui.color_edit_button_srgba(fill);
ui.end_row();
ui.label("Stroke");
ui.add(stroke);
ui.end_row();
ui.label("Stroke kind");
ui.horizontal(|ui| {
ui.selectable_value(stroke_kind, StrokeKind::Inside, "Inside");
ui.selectable_value(stroke_kind, StrokeKind::Middle, "Middle");
ui.selectable_value(stroke_kind, StrokeKind::Outside, "Outside");
});
ui.end_row();
ui.label("Blur width");
ui.add(
egui::DragValue::new(blur_width)
.speed(0.5)
.range(0.0..=20.0),
);
ui.end_row();
ui.label("Round to pixels");
ui.checkbox(round_to_pixels, "");
ui.end_row();
});
}
#[cfg(test)]
mod tests {
use crate::View as _;
use super::*;
#[test]
fn snapshot_tessellation_test() {
for (name, shape) in TessellationTest::interesting_shapes() {
let mut test = TessellationTest {
shape,
..Default::default()
};
let mut harness = egui_kittest::Harness::new_ui(|ui| {
test.ui(ui);
});
harness.fit_contents();
harness.run();
harness.snapshot(&format!("tessellation_test/{name}"));
}
}
}

View File

@@ -58,9 +58,7 @@ impl crate::View for TextEditDemo {
}
});
let anything_selected = output
.cursor_range
.map_or(false, |cursor| !cursor.is_empty());
let anything_selected = output.cursor_range.is_some_and(|cursor| !cursor.is_empty());
ui.add_enabled(
anything_selected,

View File

@@ -56,8 +56,13 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
let rect = rect.expand(visuals.expansion);
let radius = 0.5 * rect.height();
ui.painter()
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
ui.painter().rect(
rect,
radius,
visuals.bg_fill,
visuals.bg_stroke,
egui::StrokeKind::Inside,
);
// Paint the circle, animating it from left to right with `how_on`:
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
let center = egui::pos2(circle_x, rect.center().y);
@@ -88,8 +93,13 @@ fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
let visuals = ui.style().interact_selectable(&response, *on);
let rect = rect.expand(visuals.expansion);
let radius = 0.5 * rect.height();
ui.painter()
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
ui.painter().rect(
rect,
radius,
visuals.bg_fill,
visuals.bg_stroke,
egui::StrokeKind::Inside,
);
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
let center = egui::pos2(circle_x, rect.center().y);
ui.painter()

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

@@ -22,6 +22,9 @@ pub struct WidgetGallery {
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "serde", serde(skip))]
date: Option<chrono::NaiveDate>,
#[cfg(feature = "chrono")]
with_date_button: bool,
}
impl Default for WidgetGallery {
@@ -38,10 +41,24 @@ impl Default for WidgetGallery {
animate_progress_bar: false,
#[cfg(feature = "chrono")]
date: None,
#[cfg(feature = "chrono")]
with_date_button: true,
}
}
}
impl WidgetGallery {
#[allow(unused_mut)] // if not chrono
#[inline]
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
#[cfg(feature = "chrono")]
{
self.with_date_button = _with_date_button;
}
self
}
}
impl crate::Demo for WidgetGallery {
fn name(&self) -> &'static str {
"🗄 Widget Gallery"
@@ -124,6 +141,8 @@ impl WidgetGallery {
animate_progress_bar,
#[cfg(feature = "chrono")]
date,
#[cfg(feature = "chrono")]
with_date_button,
} = self;
ui.add(doc_link_label("Label", "label"));
@@ -226,7 +245,7 @@ impl WidgetGallery {
ui.end_row();
#[cfg(feature = "chrono")]
{
if *with_date_button {
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive());
ui.add(doc_link_label_with_crate(
"egui_extras",

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

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

View File

@@ -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,6 +130,34 @@ 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.
@@ -278,7 +307,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);
@@ -452,7 +484,12 @@ fn pixel_test_strokes(ui: &mut Ui) {
Pos2::new(cursor_pixel.x, cursor_pixel.y),
Vec2::splat(size as f32),
);
painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke);
painter.rect_stroke(
rect_points / pixels_per_point,
0.0,
stroke,
egui::StrokeKind::Outside,
);
cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0;
}
}
@@ -560,8 +597,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(
@@ -683,10 +726,11 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
mod tests {
use crate::ColorTest;
use egui_kittest::kittest::Queryable as _;
use egui_kittest::SnapshotResults;
#[test]
pub fn rendering_test() {
let mut errors = vec![];
let mut results = SnapshotResults::new();
for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] {
let mut color_test = ColorTest::default();
let mut harness = egui_kittest::Harness::builder()
@@ -703,12 +747,7 @@ mod tests {
harness.fit_contents();
let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
if let Err(err) = result {
errors.push(err);
}
results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")));
}
assert!(errors.is_empty(), "Errors: {errors:#?}");
}
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf83bead834ec8f88d74b32ae6331715e8c6df183e007e2a16004c019534a30f
size 31810
oid sha256:cbe9f58cce2466360b4b93b03afaaee36711b3017ddff1b2b56bfe49ea91a076
size 31306

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a1099b85a1aaf20f3f1e091bc68259f811737feaefdfcc12acd067eca8f9117
size 27083
oid sha256:7224afc6e728f60c28c027bf4be03d1f598dc70977274bcd32b7398d11dd36c7
size 26416

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6969c6da67ea6cc7ebbbd7a2cc1cb13d4720befe28126367cbf2b2679d037674
size 82363
oid sha256:5cfc3ee54a0e64fb8b72d55e9fc2079aa2517b200665684076d63b87c381cdb9
size 78704

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0d0b1b4d2c4b624904250bc8d6870559f0179e3f7f2d6dc4a4ff256df356237
size 20626
oid sha256:49b08c1fb7878d8670d96de9f9791e2db5cf7206812da1d9102c4dd1758cb803
size 25833

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5fe6166bb8cd5fae0899957e968312b9229a79e423a5d154eda483a149b264d
size 20831
oid sha256:1e3e0330de3f68593329d2f36649127d5ac70109232c68f5c7ce310fa919fda5
size 20348

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b71e1d109f90e017644dd20b9d84d81e3a6d5195afbd01ba86c85fa248c8b5c5
size 10703
oid sha256:2882a9842f51a7c3e9642a9a3d260407e1194648f47574608822a293bd3b1d56
size 10465

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3c9ba9064f44a4a14405f53316c1c602184caf16cb584d7c1f1912fe59f85ab
size 135712
oid sha256:af99bd49ceee6bbd96cc813cd96ac01f5c135ed7d94b8ff4010fa45feac5359a
size 127703

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:097bd512dd71c17370f6e374312c19e7ab6845f151b3c3565f2a0790b61ee7ba
size 24413
oid sha256:a0b999914adab3d44c614bdf3b28abd268a4ff6162c5680b43035b3f71cb69bb
size 23999

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bdf54573a6b0d2fedd90314f91dd7de22dd13709e8dd699b37ef8935b6adda5
size 17785
oid sha256:57e09bcf48541af11e44ff07122f09640e0329db0c2bc7a6ecb406a3ece572ac
size 17608

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6328c86b70f3e24aaf87db200d13dfd0baa787dd8431e47621457937f8f5021
size 22552
oid sha256:641e5c7d4deccc8eb0374db4707dc356285a5c72186f9021d0d601c22bc5115f
size 21894

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afb57dc0bb8ff839138e36b8780136e0c8da55ff380832538fae0394143807c0
size 65321
oid sha256:880344367ed65f83898ceca4843b1b6259d1690242ced0d29ac8dc48100a8faa
size 62956

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:403fc1d08d79168bc9f7a08ac7f363f2df916547f08311838edfda8a499a9b2d
size 32879
oid sha256:2f467edf4a84c8a98d96f168d843edb201ad2ee067dcd9d8d9ea214a02a41b1f
size 32182

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbd490a15377bdd4fd3272f3cd126cbc4fb9ed2f6f84edcbba03dd30dc3f3d99
size 36780
oid sha256:01705a1a49350278f524bbc5dbd47ae9da4b57ee7f6f34fb20186e1aa9b9f1d4
size 35714

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c31b3998817e345b12b037d7f8bec7641f77d0c7eab7da9a746b7b51c9efc8fb
size 17531
oid sha256:03cb424100e99a141daeacc78036c4334d74cace3fae19bb878565ccda68457d
size 17448

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e8963c3ecd0e74fe9641d87101742a0d45c82a582d70e30eb36bc835f5aac06
size 25330
oid sha256:dfa05d7f8c36b51c054253fbe8483c38637a12dde4a9d6051b226680820db319
size 25097

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88b3a50b481942630b5804c60227f23b719dc7e3eb6dbe432c2448cb69332def
size 262141
oid sha256:df1e4a1e355100056713e751a8979d4201d0e4aab5513ba2f7a3e4852e1347dd
size 264340

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d5d628b54b28eccac0d9ef21bbdabace0cdf507898707956943e2259df846ca
size 23741
oid sha256:63f5c3be15164e6f008fb09b4ff37eff2af0ab361de28d1994d595789c379df5
size 23205

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8763a8f8b4242cbe589cd3282dc5f4b32a84b4e0416fb8072dfefb7879a5d4f6
size 187982
oid sha256:b236fe02f6cd52041359cf4b1a00e9812b95560353ce5df4fa6cb20fdbb45307
size 179653

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d61088bf1f992467e8396ac89407fa158d6f44e2d2d196778247f3ff18cb893
size 119759
oid sha256:1579351658875af48ad9aafeb08d928d83f1bda42bf092fdcceecd0aa6730e26
size 115313

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79ebaf9cccd19da2923507b5a553a23edc67382ef59e4b71f01d3bd6cc913539
size 25829
oid sha256:eb7c844f6b745f66304ad036790a5121e4827fa91569b28ffa301794aecd0c66
size 25592

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06cbf13841e6ac5fbc57bdae6f5ad9d19718193c344420dedcc0e9d7ed2b8ba9
size 71590
oid sha256:13115759157beb57febcff4be6f1710340736108b520e9ad3efb04be3cedcf7b
size 68767

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35e66f211c0b30a684371b520c46dbe4f9d5b6835e053a4eb65f492dd66a9e6c
size 67288
oid sha256:e177888e10f357f1be8ad80f7a0a33c93798c1e7c43cfe382119eeb12f21279f
size 64732

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa9ee8631bfe433ee6fad1fb1651fd6b63e2fb3fbc5f321a5410f7266dc16d09
size 21296
oid sha256:e177e2631414784161a5556bdd1420ce8432f9859faede1a2e6f791a02814412
size 20918

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6475702b1bf2c65fb5196928a8934ade647a6053d6902a836e3d69cb7920091e
size 59874
oid sha256:c49c489fe1bb00512c9d08e8d8454fce786744f4ebff0bfd27dac68b7e67b815
size 62317

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d8daaec0c58709298a4594b7b2aa935aa2de327e6b71bd7417c2ba3a6eb060c
size 13020
oid sha256:c4d6a15094eee5d96a8af5c44ea9d0c962d650ee9b867344c86d1229e526dcb5
size 12822

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29d8d859a8cb11e4b390d45c658ed8ff191c2e542909e12f2385c0cba62baa2d
size 35109
oid sha256:02abc0cbab97e572218f422f4b167957869d4e2b4b388355444c20148d998015
size 35200

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17217e600d8a85ec00ecb11f0e5fe69496b30bbf715cc86785cec6e18b8c2fa1
size 48158
oid sha256:e954bf915d562abc69269cd10a4df8fbd0e5603929e6446fefa694099e2494a4
size 47542

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fac50d2327a9e5e99989dd81d5644db86031b91b9c5c68fc17b5ef53ae655048
size 47970
oid sha256:1c7bd1a65b6c33eff2fe17f7af2dd731a03658abc2419f8722c0e9395b26fdef
size 47515

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5e829257b742194742429be0efd326be17ff7f5b9b16f9df58df21e899320bd
size 43963
oid sha256:e89cd220a925150384b9f9987b178036ffacfe29cdb36ed688205524dbb731fd
size 43803

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5da064332c669a860a92a34b101f23e28026d4f07948f7c3e9a40e611f5e284f
size 43986
oid sha256:b71da58f5c0178517f9e0cc97753a0a5d1653cc5d094b5a35ffe050499bcd569
size 43679

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed2a356452d792e32bea57f044da9d86da27fd8504826dd6b87618a53519ea6a
size 522556
oid sha256:5a1f0d0759458017127d93278b89278af20fdc57c7747652ac6554f24cc708f2
size 552939

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d04a5854528c6141f7def6f9229c51c6d2d4c87e2f656be4d149e7b2b852976
size 729056
oid sha256:7948ced20e3f62b8356fc978ca7b12f8522b7c15716409cc661c0ebd2f12047f
size 770062

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6e5c1a745e357faa7b98f7a2cd1ca139c4a14be154b9d21feb8030933acfdb7
size 867552
oid sha256:e6f394c2beb51d95edaf8c7ddc9ff62d3f95913ea88a3840245b6bacf8b850cc
size 907997

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d9f4a37541fd1a0754c1cb1f3a2d4a76f03d67ca4e5596c8e6982d691d29dea
size 980286
oid sha256:d31f018cdabf92966b5636d9aef7f11ef1a0383884867e819e7ec99c0474e872
size 1025013

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d1c99867202e16500146b7146a32fd83d70d60f5ac94aae4ca405a6377e4625
size 1066559
oid sha256:e18f7ddadd53e16d04f191268747f244c98d0ea0f4cb9c0ea299f5da7affbc58
size 1139723

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08fc1c89fd2d04aa12c75a1829dacfff090e322c65ad969648799833e1b072eb
size 1235574
oid sha256:09a49cb9da7269bec6eef30c46a0bc85df6538d6e31dc0d0ff2758dbee45f3d8
size 1291804

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3dc1bf9a59007a6ad0fb66a345d6cf272bd8bdcd26b10dbf411c1280e62b6fc
size 158285
oid sha256:af75f773e9e4ad2615893babce5b99e7fd127c76dd0976ac8dc95307f38a59dc
size 152854