1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05:40:03 -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

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