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:
@@ -76,16 +76,16 @@ impl crate::View for CodeEditor {
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = egui_extras::syntax_highlighting::highlight(
|
||||
ui.ctx(),
|
||||
ui.style(),
|
||||
&theme,
|
||||
string,
|
||||
buf.as_str(),
|
||||
language,
|
||||
);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
|
||||
@@ -62,6 +62,7 @@ impl CodeExample {
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[expect(clippy::literal_string_with_formatting_args)]
|
||||
show_code(ui, r#"ui.label(format!("{name} is {age}"));"#);
|
||||
ui.label(format!("{name} is {age}"));
|
||||
ui.end_row();
|
||||
@@ -84,7 +85,7 @@ impl CodeExample {
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 2.0 * 4.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
|
||||
let indentation = 2.0 * 4.0 * ui.fonts_mut(|f| f.glyph_width(&font_id, ' '));
|
||||
ui.add_space(indentation);
|
||||
|
||||
egui::Grid::new("code_samples")
|
||||
@@ -105,7 +106,7 @@ impl crate::Demo for CodeExample {
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use crate::View;
|
||||
use crate::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.min_width(375.0)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {}
|
||||
|
||||
impl crate::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use crate::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
|
||||
if ui.ctx().is_context_menu_open() {
|
||||
ui.label("Context menu is open");
|
||||
} else {
|
||||
ui.label("Context menu is closed");
|
||||
}
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use egui::{
|
||||
Color32, Context, Pos2, Rect, Ui,
|
||||
containers::{Frame, Window},
|
||||
emath, epaint,
|
||||
epaint::PathStroke,
|
||||
hex_color, lerp, pos2, remap, vec2, Color32, Context, Pos2, Rect, Ui,
|
||||
hex_color, lerp, pos2, remap, vec2,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
|
||||
use super::About;
|
||||
use crate::is_mobile;
|
||||
use crate::Demo;
|
||||
use crate::View;
|
||||
|
||||
use crate::View as _;
|
||||
use crate::is_mobile;
|
||||
use egui::containers::menu;
|
||||
use egui::style::StyleModifier;
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct DemoGroup {
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
}
|
||||
|
||||
impl std::ops::Add for DemoGroup {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
let mut demos = self.demos;
|
||||
demos.extend(other.demos);
|
||||
Self { demos }
|
||||
}
|
||||
}
|
||||
|
||||
impl DemoGroup {
|
||||
pub fn new(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
Self { demos }
|
||||
@@ -65,7 +75,6 @@ impl Default for DemoGroups {
|
||||
Box::<super::paint_bezier::PaintBezier>::default(),
|
||||
Box::<super::code_editor::CodeEditor>::default(),
|
||||
Box::<super::code_example::CodeExample>::default(),
|
||||
Box::<super::context_menu::ContextMenus>::default(),
|
||||
Box::<super::dancing_strings::DancingStrings>::default(),
|
||||
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
||||
Box::<super::extra_viewport::ExtraViewport>::default(),
|
||||
@@ -78,6 +87,7 @@ impl Default for DemoGroups {
|
||||
Box::<super::multi_touch::MultiTouch>::default(),
|
||||
Box::<super::painting::Painting>::default(),
|
||||
Box::<super::panels::Panels>::default(),
|
||||
Box::<super::popups::PopupsDemo>::default(),
|
||||
Box::<super::scene::SceneDemo>::default(),
|
||||
Box::<super::screenshot::Screenshot>::default(),
|
||||
Box::<super::scrolling::Scrolling>::default(),
|
||||
@@ -100,6 +110,7 @@ impl Default for DemoGroups {
|
||||
Box::<super::tests::InputTest>::default(),
|
||||
Box::<super::tests::LayoutTest>::default(),
|
||||
Box::<super::tests::ManualLayoutTest>::default(),
|
||||
Box::<super::tests::SvgTest>::default(),
|
||||
Box::<super::tests::TessellationTest>::default(),
|
||||
Box::<super::tests::WindowResizeTest>::default(),
|
||||
]),
|
||||
@@ -225,38 +236,36 @@ impl DemoWindows {
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::Panel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
menu::MenuBar::new()
|
||||
.config(menu::MenuConfig::new().style(StyleModifier::default()))
|
||||
.ui(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::Panel::right("egui_demo_panel")
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_size(160.0)
|
||||
.min_size(160.0)
|
||||
.default_width(160.0)
|
||||
.min_width(160.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
@@ -280,8 +289,8 @@ impl DemoWindows {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::Panel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
menu::MenuBar::new().ui(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
@@ -344,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
@@ -356,21 +364,25 @@ fn file_menu_button(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{demo::demo_app_windows::DemoGroups, Demo};
|
||||
use egui::Vec2;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
|
||||
use crate::{Demo as _, demo::demo_app_windows::DemoGroups};
|
||||
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
fn demos_should_match_snapshot() {
|
||||
let demos = DemoGroups::default().demos;
|
||||
let DemoGroups {
|
||||
demos,
|
||||
tests,
|
||||
about: _,
|
||||
} = DemoGroups::default();
|
||||
let demos = demos + tests;
|
||||
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
@@ -380,33 +392,41 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove the emoji from the demo name
|
||||
let name = demo
|
||||
.name()
|
||||
.split_once(' ')
|
||||
.map_or(demo.name(), |(_, name)| name);
|
||||
let name = remove_leading_emoji(demo.name());
|
||||
|
||||
let mut harness = Harness::new(|ctx| {
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
demo.show(ctx, &mut true);
|
||||
});
|
||||
|
||||
let window = harness.node().children().next().unwrap();
|
||||
let window = harness.queryable_node().children().next().unwrap();
|
||||
// TODO(lucasmerlin): Windows should probably have a label?
|
||||
//let window = harness.get_by_label(name);
|
||||
|
||||
let size = window.raw_bounds().expect("window bounds").size();
|
||||
harness.set_size(Vec2::new(size.width as f32, size.height as f32));
|
||||
let size = window.rect().size();
|
||||
harness.set_size(size);
|
||||
|
||||
// Run the app for some more frames...
|
||||
harness.run_ok();
|
||||
|
||||
let mut options = SnapshotOptions::default();
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux
|
||||
|
||||
if name == "Bézier Curve" {
|
||||
options.threshold = 2.1;
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux:
|
||||
options = options.threshold(OsThreshold::new(0.0).linux(2.1));
|
||||
}
|
||||
|
||||
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
|
||||
results.add(harness.try_snapshot_options(format!("demos/{name}"), &options));
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_leading_emoji(full_name: &str) -> &str {
|
||||
if let Some((start, name)) = full_name.split_once(' ')
|
||||
&& start.len() <= 4
|
||||
&& start.bytes().next().is_some_and(|byte| byte >= 128)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
full_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{vec2, Color32, Context, Frame, Id, Ui, Window};
|
||||
use egui::{Color32, Context, Frame, Id, Ui, Window, vec2};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
|
||||
@@ -54,7 +54,11 @@ fn viewport_content(ui: &mut egui::Ui, ctx: &egui::Context, open: &mut bool) {
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let viewports = ui.input(|i| i.raw.viewports.clone());
|
||||
for (id, viewport) in viewports {
|
||||
let ordered_viewports = viewports
|
||||
.iter()
|
||||
.map(|(id, viewport)| (*id, viewport.clone()))
|
||||
.collect::<egui::OrderedViewportIdMap<_>>();
|
||||
for (id, viewport) in ordered_viewports {
|
||||
ui.group(|ui| {
|
||||
ui.label(format!("viewport {id:?}"));
|
||||
ui.push_id(id, |ui| {
|
||||
|
||||
@@ -77,7 +77,7 @@ impl crate::View for FontBook {
|
||||
let available_glyphs = self
|
||||
.available_glyphs
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
.or_insert_with(|| available_characters(ui, &self.font_id.family));
|
||||
|
||||
ui.separator();
|
||||
|
||||
@@ -140,11 +140,10 @@ fn char_info_ui(ui: &mut egui::Ui, chr: char, glyph_info: &GlyphInfo, font_id: e
|
||||
});
|
||||
}
|
||||
|
||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, GlyphInfo> {
|
||||
ui.fonts(|f| {
|
||||
f.lock()
|
||||
.fonts
|
||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||
fn available_characters(ui: &egui::Ui, family: &egui::FontFamily) -> BTreeMap<char, GlyphInfo> {
|
||||
ui.fonts_mut(|f| {
|
||||
f.fonts
|
||||
.font(family)
|
||||
.characters()
|
||||
.iter()
|
||||
.filter(|(chr, _fonts)| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||
@@ -169,7 +168,7 @@ fn char_name(chr: char) -> String {
|
||||
}
|
||||
|
||||
fn special_char_name(chr: char) -> Option<&'static str> {
|
||||
#[allow(clippy::match_same_arms)] // many "flag"
|
||||
#[expect(clippy::match_same_arms)] // many "flag"
|
||||
match chr {
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Private use area extensions:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget};
|
||||
use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget as _};
|
||||
|
||||
/// Showcase [`egui::Ui::response`].
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::{Demo, View};
|
||||
|
||||
use egui::{
|
||||
vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Resize, RichText, Sense,
|
||||
Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window,
|
||||
Align, Align2, Checkbox, CollapsingHeader, Color32, ComboBox, Context, FontId, Resize,
|
||||
RichText, Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, vec2,
|
||||
};
|
||||
|
||||
/// Showcase some ui code
|
||||
@@ -16,6 +16,7 @@ pub struct MiscDemoWindow {
|
||||
custom_collapsing_header: CustomCollapsingHeader,
|
||||
tree: Tree,
|
||||
box_painting: BoxPainting,
|
||||
text_rotation: TextRotation,
|
||||
|
||||
dummy_bool: bool,
|
||||
dummy_usize: usize,
|
||||
@@ -32,6 +33,7 @@ impl Default for MiscDemoWindow {
|
||||
custom_collapsing_header: Default::default(),
|
||||
tree: Tree::demo(),
|
||||
box_painting: Default::default(),
|
||||
text_rotation: Default::default(),
|
||||
|
||||
dummy_bool: false,
|
||||
dummy_usize: 0,
|
||||
@@ -79,6 +81,10 @@ impl View for MiscDemoWindow {
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Text rotation")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.text_rotation.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Colors")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
@@ -207,7 +213,7 @@ fn label_ui(ui: &mut egui::Ui) {
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Trick so we don't have to add spaces in the text below:
|
||||
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
|
||||
let width = ui.fonts_mut(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
|
||||
ui.spacing_mut().item_spacing.x = width;
|
||||
|
||||
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
|
||||
@@ -729,3 +735,95 @@ fn text_layout_demo(ui: &mut Ui) {
|
||||
|
||||
ui.label(job);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct TextRotation {
|
||||
size: Vec2,
|
||||
angle: f32,
|
||||
align: egui::Align2,
|
||||
}
|
||||
|
||||
impl Default for TextRotation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: vec2(200.0, 200.0),
|
||||
angle: 0.0,
|
||||
align: egui::Align2::LEFT_TOP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextRotation {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(Slider::new(&mut self.angle, 0.0..=2.0 * std::f32::consts::PI).text("angle"));
|
||||
|
||||
let default_color = if ui.visuals().dark_mode {
|
||||
Color32::LIGHT_GRAY
|
||||
} else {
|
||||
Color32::DARK_GRAY
|
||||
};
|
||||
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "LEFT_TOP"),
|
||||
(Align2::LEFT_CENTER, "LEFT_CENTER"),
|
||||
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
|
||||
(Align2::CENTER_TOP, "CENTER_TOP"),
|
||||
(Align2::CENTER_CENTER, "CENTER_CENTER"),
|
||||
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
|
||||
(Align2::RIGHT_TOP, "RIGHT_TOP"),
|
||||
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
|
||||
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
|
||||
];
|
||||
|
||||
ComboBox::new("anchor", "Anchor")
|
||||
.selected_text(aligns.iter().find(|(a, _)| *a == self.align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(&mut self.align, *align2, *name);
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let (response, painter) = ui.allocate_painter(self.size, Sense::empty());
|
||||
let rect = response.rect;
|
||||
|
||||
let start_pos = self.size / 2.0;
|
||||
|
||||
let s = ui.ctx().fonts_mut(|f| {
|
||||
let mut t = egui::Shape::text(
|
||||
f,
|
||||
rect.min + start_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
"sample_text",
|
||||
egui::FontId::new(12.0, egui::FontFamily::Proportional),
|
||||
default_color,
|
||||
);
|
||||
|
||||
if let egui::epaint::Shape::Text(ts) = &mut t {
|
||||
let new = ts.clone().with_angle_and_anchor(self.angle, self.align);
|
||||
*ts = new;
|
||||
}
|
||||
|
||||
t
|
||||
});
|
||||
|
||||
if let egui::epaint::Shape::Text(ts) = &s {
|
||||
let align_pt =
|
||||
rect.min + start_pos + self.align.pos_in_rect(&ts.galley.rect).to_vec2();
|
||||
painter.circle(align_pt, 2.0, Color32::RED, (0.0, Color32::RED));
|
||||
}
|
||||
|
||||
painter.rect(
|
||||
rect,
|
||||
0.0,
|
||||
default_color.gamma_multiply(0.3),
|
||||
(0.0, Color32::BLACK),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
painter.add(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
pub mod about;
|
||||
pub mod code_editor;
|
||||
pub mod code_example;
|
||||
pub mod context_menu;
|
||||
pub mod dancing_strings;
|
||||
pub mod demo_app_windows;
|
||||
pub mod drag_and_drop;
|
||||
@@ -23,6 +22,7 @@ pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod panels;
|
||||
pub mod password;
|
||||
mod popups;
|
||||
pub mod scene;
|
||||
pub mod screenshot;
|
||||
pub mod scrolling;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window};
|
||||
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget as _, Window};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
@@ -96,7 +96,9 @@ impl crate::View for Modals {
|
||||
*save_modal_open = true;
|
||||
}
|
||||
if ui.button("Cancel").clicked() {
|
||||
*user_modal_open = false;
|
||||
// You can call `ui.close()` to close the modal.
|
||||
// (This causes the current modals `should_close` to return true)
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -123,7 +125,7 @@ impl crate::View for Modals {
|
||||
}
|
||||
|
||||
if ui.button("No Thanks").clicked() {
|
||||
*save_modal_open = false;
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -160,11 +162,11 @@ impl crate::View for Modals {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Demo as _;
|
||||
use crate::demo::modals::Modals;
|
||||
use crate::Demo;
|
||||
use egui::accesskit::Role;
|
||||
use egui::Key;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use egui::{Key, Popup};
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
@@ -185,12 +187,12 @@ mod tests {
|
||||
|
||||
// Harness::run would fail because we keep requesting repaints to simulate progress.
|
||||
harness.run_ok();
|
||||
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
|
||||
assert!(Popup::is_any_open(&harness.ctx));
|
||||
assert!(harness.state().user_modal_open);
|
||||
|
||||
harness.press_key(Key::Escape);
|
||||
harness.key_press(Key::Escape);
|
||||
harness.run_ok();
|
||||
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
|
||||
assert!(!Popup::is_any_open(&harness.ctx));
|
||||
assert!(harness.state().user_modal_open);
|
||||
}
|
||||
|
||||
@@ -212,7 +214,7 @@ mod tests {
|
||||
assert!(harness.state().user_modal_open);
|
||||
assert!(harness.state().save_modal_open);
|
||||
|
||||
harness.press_key(Key::Escape);
|
||||
harness.key_press(Key::Escape);
|
||||
harness.run();
|
||||
|
||||
assert!(harness.state().user_modal_open);
|
||||
@@ -265,7 +267,7 @@ mod tests {
|
||||
|
||||
harness.run_ok();
|
||||
|
||||
harness.get_by_label("Yes Please").simulate_click();
|
||||
harness.get_by_label("Yes Please").click();
|
||||
|
||||
harness.run_ok();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use egui::{
|
||||
Color32, Event, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
vec2,
|
||||
};
|
||||
|
||||
pub struct MultiTouch {
|
||||
@@ -29,7 +30,7 @@ impl crate::Demo for MultiTouch {
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.default_size(vec2(544.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
@@ -44,13 +45,31 @@ impl crate::View for MultiTouch {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles, tablets, and trackpads).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let relative_pointer_gesture = ui.input(|i| {
|
||||
i.events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::MouseWheel { .. } | Event::Zoom { .. } | Event::Rotate { .. }
|
||||
)
|
||||
})
|
||||
});
|
||||
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
|
||||
ui.label(format!("Current touches: {num_touches}"));
|
||||
let num_touches_str = format!("{num_touches}-finger touch");
|
||||
ui.label(format!(
|
||||
"Input source: {}",
|
||||
if ui.input(|i| i.multi_touch().is_some()) {
|
||||
num_touches_str.as_str()
|
||||
} else if relative_pointer_gesture {
|
||||
"cursor"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
));
|
||||
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
@@ -82,18 +101,18 @@ impl crate::View for MultiTouch {
|
||||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
if ui.input(|i| i.multi_touch().is_some()) || relative_pointer_gesture {
|
||||
ui.input(|input| {
|
||||
// This adjusts the current zoom factor, rotation angle, and translation according
|
||||
// to the dynamic change (for the current frame) of the touch gesture:
|
||||
self.zoom *= input.zoom_delta();
|
||||
self.rotation += input.rotation_delta();
|
||||
self.translation += to_screen.inverse().scale() * input.translation_delta();
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * input.multi_touch().map_or(0.0, |touch| touch.force);
|
||||
|
||||
self.last_touch_time = ui.input(|i| i.time);
|
||||
self.last_touch_time = input.time;
|
||||
});
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use egui::{
|
||||
emath,
|
||||
Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
|
||||
Widget as _, Window, emath,
|
||||
epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape},
|
||||
pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
|
||||
Widget, Window,
|
||||
pos2,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -152,7 +152,7 @@ impl PaintBezier {
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
|
||||
painter.extend(control_point_shapes);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{emath, vec2, Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window};
|
||||
use egui::{Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window, emath, vec2};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/// ``` ignore
|
||||
/// password_ui(ui, &mut my_password);
|
||||
/// ```
|
||||
#[allow(clippy::ptr_arg)] // false positive
|
||||
pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
@@ -28,7 +27,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.selectable_label(show_plaintext, "👁")
|
||||
.on_hover_text("Show/hide password");
|
||||
|
||||
if response.clicked() {
|
||||
@@ -62,5 +61,5 @@ pub fn password(password: &mut String) -> impl egui::Widget + '_ {
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/main/{}", file!())
|
||||
}
|
||||
|
||||
319
crates/egui_demo_lib/src/demo/popups.rs
Normal file
319
crates/egui_demo_lib/src/demo/popups.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use crate::rust_view_ui;
|
||||
use egui::color_picker::{Alpha, color_picker_color32};
|
||||
use egui::containers::menu::{MenuConfig, SubMenuButton};
|
||||
use egui::{
|
||||
Align, Align2, Atom, Button, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, RectAlign,
|
||||
RichText, Tooltip, Ui, UiBuilder, include_image,
|
||||
};
|
||||
|
||||
/// Showcase [`Popup`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct PopupsDemo {
|
||||
align4: RectAlign,
|
||||
gap: f32,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
close_behavior: PopupCloseBehavior,
|
||||
popup_open: bool,
|
||||
checked: bool,
|
||||
color: egui::Color32,
|
||||
}
|
||||
|
||||
impl Default for PopupsDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
align4: RectAlign::default(),
|
||||
gap: 4.0,
|
||||
close_behavior: PopupCloseBehavior::CloseOnClick,
|
||||
popup_open: false,
|
||||
checked: true,
|
||||
color: egui::Color32::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupsDemo {
|
||||
fn apply_options<'a>(&self, popup: Popup<'a>) -> Popup<'a> {
|
||||
popup
|
||||
.align(self.align4)
|
||||
.gap(self.gap)
|
||||
.close_behavior(self.close_behavior)
|
||||
}
|
||||
|
||||
fn nested_menus(&mut self, ui: &mut Ui) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
ui.menu_button("Popups can have submenus", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
ui.menu_button("Recursive", |ui| self.nested_menus(ui));
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
ui.menu_button("SubMenus can be disabled", |_| {});
|
||||
});
|
||||
ui.menu_image_text_button(
|
||||
include_image!("../../data/icon.png"),
|
||||
"I have an icon!",
|
||||
|ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
SubMenuButton::new("Always CloseOnClickOutside")
|
||||
.config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside))
|
||||
.ui(ui, |ui| {
|
||||
ui.checkbox(&mut self.checked, "Checkbox");
|
||||
|
||||
// Customized color SubMenuButton
|
||||
let is_bright = self.color.intensity() > 0.5;
|
||||
let text_color = if is_bright {
|
||||
egui::Color32::BLACK
|
||||
} else {
|
||||
egui::Color32::WHITE
|
||||
};
|
||||
|
||||
let button = Button::new((
|
||||
RichText::new("Background").color(text_color),
|
||||
Atom::grow(),
|
||||
RichText::new(SubMenuButton::RIGHT_ARROW).color(text_color),
|
||||
))
|
||||
.fill(self.color);
|
||||
|
||||
SubMenuButton::from_button(button).ui(ui, |ui| {
|
||||
ui.spacing_mut().slider_width = 200.0;
|
||||
color_picker_color32(ui, &mut self.color, Alpha::Opaque);
|
||||
});
|
||||
|
||||
if self.checked {
|
||||
ui.menu_button("Only visible when checked", |ui| {
|
||||
if ui.button("Remove myself").clicked() {
|
||||
self.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for PopupsDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"\u{2755} Popups"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.default_width(250.0)
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for PopupsDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let response = Frame::group(ui.style())
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
||||
.inner
|
||||
})
|
||||
.inner;
|
||||
|
||||
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
||||
.show(|ui| self.nested_menus(ui));
|
||||
|
||||
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
||||
.show(|ui| self.nested_menus(ui));
|
||||
|
||||
if self.popup_open {
|
||||
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
|
||||
.show(|ui| {
|
||||
ui.label("Popup contents");
|
||||
});
|
||||
}
|
||||
|
||||
let mut tooltip = Tooltip::for_enabled(&response);
|
||||
tooltip.popup = self.apply_options(tooltip.popup);
|
||||
tooltip.show(|ui| {
|
||||
ui.label("Tooltips are popups, too!");
|
||||
});
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
let mut reset_btn_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.max_rect(ui.max_rect())
|
||||
.layout(Layout::right_to_left(Align::Min)),
|
||||
);
|
||||
if reset_btn_ui
|
||||
.button("⟲")
|
||||
.on_hover_text("Reset to defaults")
|
||||
.clicked()
|
||||
{
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
ui.set_width(ui.available_width());
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "LEFT_TOP"),
|
||||
(Align2::LEFT_CENTER, "LEFT_CENTER"),
|
||||
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
|
||||
(Align2::CENTER_TOP, "CENTER_TOP"),
|
||||
(Align2::CENTER_CENTER, "CENTER_CENTER"),
|
||||
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
|
||||
(Align2::RIGHT_TOP, "RIGHT_TOP"),
|
||||
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
|
||||
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
|
||||
];
|
||||
|
||||
ComboBox::new(label, "")
|
||||
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(align, *align2, *name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rust_view_ui(ui, "let align = RectAlign {");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " parent: Align2::");
|
||||
align_combobox(ui, "parent", &mut self.align4.parent);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " child: Align2::");
|
||||
align_combobox(ui, "child", &mut self.align4.child);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
rust_view_ui(ui, "};");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let align = RectAlign::");
|
||||
|
||||
let presets = [
|
||||
(RectAlign::TOP_START, "TOP_START"),
|
||||
(RectAlign::TOP, "TOP"),
|
||||
(RectAlign::TOP_END, "TOP_END"),
|
||||
(RectAlign::RIGHT_START, "RIGHT_START"),
|
||||
(RectAlign::RIGHT, "RIGHT"),
|
||||
(RectAlign::RIGHT_END, "RIGHT_END"),
|
||||
(RectAlign::BOTTOM_START, "BOTTOM_START"),
|
||||
(RectAlign::BOTTOM, "BOTTOM"),
|
||||
(RectAlign::BOTTOM_END, "BOTTOM_END"),
|
||||
(RectAlign::LEFT_START, "LEFT_START"),
|
||||
(RectAlign::LEFT, "LEFT"),
|
||||
(RectAlign::LEFT_END, "LEFT_END"),
|
||||
];
|
||||
|
||||
ComboBox::new("Preset", "")
|
||||
.selected_text(
|
||||
presets
|
||||
.iter()
|
||||
.find(|(a, _)| a == &self.align4)
|
||||
.map_or("<Select Preset>", |(_, name)| *name),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align4, name) in &presets {
|
||||
ui.selectable_value(&mut self.align4, *align4, *name);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let gap = ");
|
||||
ui.add(egui::DragValue::new(&mut self.gap));
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
rust_view_ui(ui, "let close_behavior");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " = PopupCloseBehavior::");
|
||||
let close_behaviors = [
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
"CloseOnClick",
|
||||
"Closes when the user clicks anywhere (inside or outside)",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClickOutside,
|
||||
"CloseOnClickOutside",
|
||||
"Closes when the user clicks outside the popup",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
"IgnoreClicks",
|
||||
"Close only when the button is clicked again",
|
||||
),
|
||||
];
|
||||
ComboBox::new("Close behavior", "")
|
||||
.selected_text(
|
||||
close_behaviors
|
||||
.iter()
|
||||
.find_map(|(behavior, text, _)| {
|
||||
(behavior == &self.close_behavior).then_some(*text)
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (close_behavior, name, tooltip) in &close_behaviors {
|
||||
ui.selectable_value(&mut self.close_behavior, *close_behavior, *name)
|
||||
.on_hover_text(*tooltip);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let popup_open = ");
|
||||
ui.checkbox(&mut self.popup_open, "");
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
ui.monospace("");
|
||||
rust_view_ui(ui, "let response = ui.button(\"Click me!\");");
|
||||
rust_view_ui(ui, "Popup::menu(&response)");
|
||||
rust_view_ui(ui, " .gap(gap).align(align)");
|
||||
rust_view_ui(ui, " .close_behavior(close_behavior)");
|
||||
rust_view_ui(ui, " .show(|ui| { /* menu contents */ });");
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Image, UserData, ViewportCommand, Widget};
|
||||
use egui::{Image, UserData, ViewportCommand, Widget as _};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Showcase [`ViewportCommand::Screenshot`].
|
||||
@@ -58,7 +58,7 @@ impl crate::View for Screenshot {
|
||||
None
|
||||
}
|
||||
})
|
||||
.last()
|
||||
.next_back()
|
||||
});
|
||||
|
||||
if let Some(image) = image {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use egui::{
|
||||
pos2, scroll_area::ScrollBarVisibility, Align, Align2, Color32, DragValue, NumExt, Rect,
|
||||
ScrollArea, Sense, Slider, TextStyle, TextWrapMode, Ui, Vec2, Widget,
|
||||
Align, Align2, Color32, DragValue, NumExt as _, Rect, ScrollArea, Sense, Slider, TextStyle,
|
||||
TextWrapMode, Ui, Vec2, Widget as _, pos2, scroll_area::ScrollBarVisibility,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum ScrollDemo {
|
||||
#[default]
|
||||
ScrollAppearance,
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
@@ -14,12 +15,6 @@ enum ScrollDemo {
|
||||
Bidirectional,
|
||||
}
|
||||
|
||||
impl Default for ScrollDemo {
|
||||
fn default() -> Self {
|
||||
Self::ScrollAppearance
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
@@ -191,7 +186,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
|
||||
let num_rows = 10_000;
|
||||
|
||||
ScrollArea::vertical()
|
||||
@@ -222,7 +217,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
used_rect = used_rect.union(text_rect);
|
||||
used_rect |= text_rect;
|
||||
}
|
||||
|
||||
ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui};
|
||||
use egui::{Slider, SliderClamping, SliderOrientation, Ui, style::HandleShape};
|
||||
|
||||
/// Showcase sliders
|
||||
#[derive(PartialEq)]
|
||||
|
||||
@@ -13,6 +13,7 @@ enum DemoType {
|
||||
pub struct TableDemo {
|
||||
demo: DemoType,
|
||||
striped: bool,
|
||||
overline: bool,
|
||||
resizable: bool,
|
||||
clickable: bool,
|
||||
num_rows: usize,
|
||||
@@ -28,6 +29,7 @@ impl Default for TableDemo {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
striped: true,
|
||||
overline: true,
|
||||
resizable: true,
|
||||
clickable: true,
|
||||
num_rows: 10_000,
|
||||
@@ -65,6 +67,7 @@ impl crate::View for TableDemo {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.striped, "Striped");
|
||||
ui.checkbox(&mut self.overline, "Overline some rows");
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
ui.checkbox(&mut self.clickable, "Clickable rows");
|
||||
});
|
||||
@@ -212,6 +215,7 @@ impl TableDemo {
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -247,6 +251,7 @@ impl TableDemo {
|
||||
};
|
||||
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -280,6 +285,7 @@ impl TableDemo {
|
||||
};
|
||||
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
row.set_overline(self.overline && row_index % 7 == 3);
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
@@ -324,9 +330,11 @@ fn expanding_content(ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
fn long_text(row_index: usize) -> String {
|
||||
format!("Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!")
|
||||
format!(
|
||||
"Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!"
|
||||
)
|
||||
}
|
||||
|
||||
fn thick_row(row_index: usize) -> bool {
|
||||
row_index % 6 == 0
|
||||
row_index.is_multiple_of(6)
|
||||
}
|
||||
|
||||
@@ -67,10 +67,9 @@ impl crate::View for ClipboardTest {
|
||||
|
||||
if let Ok(egui::load::ImagePoll::Ready { image }) =
|
||||
ui.ctx().try_load_image(&uri, Default::default())
|
||||
&& ui.button("📋").clicked()
|
||||
{
|
||||
if ui.button("📋").clicked() {
|
||||
ui.ctx().copy_image((*image).clone());
|
||||
}
|
||||
ui.ctx().copy_image((*image).clone());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ impl crate::View for GridTest {
|
||||
ui.end_row();
|
||||
|
||||
let mut dyn_text = String::from("O");
|
||||
dyn_text.extend(std::iter::repeat('h').take(self.text_length));
|
||||
dyn_text.extend(std::iter::repeat_n('h', self.text_length));
|
||||
ui.label(dyn_text);
|
||||
ui.label("Fifth row, second column");
|
||||
ui.end_row();
|
||||
|
||||
@@ -12,11 +12,11 @@ struct DeduplicatedHistory {
|
||||
|
||||
impl DeduplicatedHistory {
|
||||
fn add(&mut self, summary: String, full: String) {
|
||||
if let Some(entry) = self.history.back_mut() {
|
||||
if entry.summary == summary {
|
||||
entry.entries.push(full);
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.history.back_mut()
|
||||
&& entry.summary == summary
|
||||
{
|
||||
entry.entries.push(full);
|
||||
return;
|
||||
}
|
||||
self.history.push_back(HistoryEntry {
|
||||
summary,
|
||||
|
||||
@@ -10,11 +10,11 @@ struct DeduplicatedHistory {
|
||||
|
||||
impl DeduplicatedHistory {
|
||||
fn add(&mut self, text: String) {
|
||||
if let Some(entry) = self.history.back_mut() {
|
||||
if entry.text == text {
|
||||
entry.repeated += 1;
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.history.back_mut()
|
||||
&& entry.text == text
|
||||
{
|
||||
entry.repeated += 1;
|
||||
return;
|
||||
}
|
||||
self.history.push_back(HistoryEntry { text, repeated: 1 });
|
||||
if self.history.len() > 100 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{vec2, Align, Direction, Layout, Resize, Slider, Ui};
|
||||
use egui::{Align, Direction, Layout, Resize, Slider, Ui, vec2};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
||||
@@ -6,6 +6,7 @@ mod input_event_history;
|
||||
mod input_test;
|
||||
mod layout_test;
|
||||
mod manual_layout_test;
|
||||
mod svg_test;
|
||||
mod tessellation_test;
|
||||
mod window_resize_test;
|
||||
|
||||
@@ -17,5 +18,6 @@ pub use input_event_history::InputEventHistory;
|
||||
pub use input_test::InputTest;
|
||||
pub use layout_test::LayoutTest;
|
||||
pub use manual_layout_test::ManualLayoutTest;
|
||||
pub use svg_test::SvgTest;
|
||||
pub use tessellation_test::TessellationTest;
|
||||
pub use window_resize_test::WindowResizeTest;
|
||||
|
||||
42
crates/egui_demo_lib/src/demo/tests/svg_test.rs
Normal file
42
crates/egui_demo_lib/src/demo/tests/svg_test.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
pub struct SvgTest {
|
||||
color: egui::Color32,
|
||||
}
|
||||
|
||||
impl Default for SvgTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: egui::Color32::LIGHT_RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for SvgTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVG Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for SvgTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { color } = self;
|
||||
ui.color_edit_button_srgba(color);
|
||||
let img_src = egui::include_image!("../../../data/peace.svg");
|
||||
|
||||
// First paint a small version, sized the same as the source…
|
||||
ui.add(
|
||||
egui::Image::new(img_src.clone())
|
||||
.fit_to_original_size(1.0)
|
||||
.tint(*color),
|
||||
);
|
||||
|
||||
// …then a big one, to make sure they are both crisp
|
||||
ui.add(egui::Image::new(img_src).tint(*color));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use egui::{
|
||||
emath::{GuiRounding, TSTransform},
|
||||
Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
|
||||
emath::{GuiRounding as _, TSTransform},
|
||||
epaint::{self, RectShape},
|
||||
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
|
||||
vec2,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -373,7 +374,7 @@ mod tests {
|
||||
harness.fit_contents();
|
||||
harness.run();
|
||||
|
||||
harness.snapshot(&format!("tessellation_test/{name}"));
|
||||
harness.snapshot(format!("tessellation_test/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,20 +65,20 @@ impl crate::View for TextEditDemo {
|
||||
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
|
||||
);
|
||||
|
||||
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) {
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y))
|
||||
&& let Some(text_cursor_range) = output.cursor_range
|
||||
{
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@@ -113,9 +113,9 @@ impl crate::View for TextEditDemo {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{accesskit, CentralPanel};
|
||||
use egui_kittest::kittest::{Key, Queryable};
|
||||
use egui::{CentralPanel, Key, Modifiers, accesskit};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
pub fn should_type() {
|
||||
@@ -133,8 +133,9 @@ mod tests {
|
||||
|
||||
let text_edit = harness.get_by_role(accesskit::Role::TextInput);
|
||||
assert_eq!(text_edit.value().as_deref(), Some("Hello, world!"));
|
||||
text_edit.focus();
|
||||
|
||||
text_edit.key_combination(&[Key::Command, Key::A]);
|
||||
harness.key_press_modifiers(Modifiers::COMMAND, Key::A);
|
||||
text_edit.type_text("Hi ");
|
||||
|
||||
harness.run();
|
||||
|
||||
@@ -5,7 +5,7 @@ pub struct TextLayoutDemo {
|
||||
break_anywhere: bool,
|
||||
max_rows: usize,
|
||||
overflow_character: Option<char>,
|
||||
extra_letter_spacing_pixels: i32,
|
||||
extra_letter_spacing: f32,
|
||||
line_height_pixels: u32,
|
||||
lorem_ipsum: bool,
|
||||
}
|
||||
@@ -16,7 +16,7 @@ impl Default for TextLayoutDemo {
|
||||
max_rows: 6,
|
||||
break_anywhere: true,
|
||||
overflow_character: Some('…'),
|
||||
extra_letter_spacing_pixels: 0,
|
||||
extra_letter_spacing: 0.0,
|
||||
line_height_pixels: 0,
|
||||
lorem_ipsum: true,
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl crate::View for TextLayoutDemo {
|
||||
break_anywhere,
|
||||
max_rows,
|
||||
overflow_character,
|
||||
extra_letter_spacing_pixels,
|
||||
extra_letter_spacing,
|
||||
line_height_pixels,
|
||||
lorem_ipsum,
|
||||
} = self;
|
||||
@@ -85,7 +85,7 @@ impl crate::View for TextLayoutDemo {
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Extra letter spacing:");
|
||||
ui.add(egui::DragValue::new(extra_letter_spacing_pixels).suffix(" pixels"));
|
||||
ui.add(egui::DragValue::new(extra_letter_spacing).speed(0.1));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Line height:");
|
||||
@@ -126,14 +126,13 @@ impl crate::View for TextLayoutDemo {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32;
|
||||
let line_height = (*line_height_pixels != 0)
|
||||
.then_some(points_per_pixel * *line_height_pixels as f32);
|
||||
|
||||
let mut job = LayoutJob::single_section(
|
||||
text.to_owned(),
|
||||
egui::TextFormat {
|
||||
extra_letter_spacing,
|
||||
extra_letter_spacing: *extra_letter_spacing,
|
||||
line_height,
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
@@ -121,5 +121,5 @@ pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/main/{}", file!())
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ impl Tooltips {
|
||||
ui.label("You can select this text.");
|
||||
});
|
||||
|
||||
ui.label("This tooltip shows at the mouse cursor.")
|
||||
.on_hover_text_at_pointer("Move me around!!");
|
||||
|
||||
ui.separator(); // ---------------------------------------------------------
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{util::undoer::Undoer, Button};
|
||||
use egui::{Button, util::undoer::Undoer};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -60,15 +60,11 @@ impl crate::View for UndoRedoDemo {
|
||||
let undo = ui.add_enabled(can_undo, Button::new("⟲ Undo")).clicked();
|
||||
let redo = ui.add_enabled(can_redo, Button::new("⟳ Redo")).clicked();
|
||||
|
||||
if undo {
|
||||
if let Some(undo_text) = self.undoer.undo(&self.state) {
|
||||
self.state = undo_text.clone();
|
||||
}
|
||||
if undo && let Some(undo_text) = self.undoer.undo(&self.state) {
|
||||
self.state = undo_text.clone();
|
||||
}
|
||||
if redo {
|
||||
if let Some(redo_text) = self.undoer.redo(&self.state) {
|
||||
self.state = redo_text.clone();
|
||||
}
|
||||
if redo && let Some(redo_text) = self.undoer.redo(&self.state) {
|
||||
self.state = redo_text.clone();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Default for WidgetGallery {
|
||||
}
|
||||
|
||||
impl WidgetGallery {
|
||||
#[allow(unused_mut)] // if not chrono
|
||||
#[allow(unused_mut, clippy::allow_attributes)] // if not chrono
|
||||
#[inline]
|
||||
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
|
||||
#[cfg(feature = "chrono")]
|
||||
@@ -308,7 +308,7 @@ fn doc_link_label_with_crate<'a>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::View;
|
||||
use crate::View as _;
|
||||
use egui::Vec2;
|
||||
use egui_kittest::Harness;
|
||||
|
||||
@@ -319,13 +319,27 @@ mod tests {
|
||||
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| demo.ui(ui));
|
||||
|
||||
harness.fit_contents();
|
||||
for pixels_per_point in [1, 2] {
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point as f32)
|
||||
.with_theme(theme)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
demo.ui(ui);
|
||||
});
|
||||
|
||||
harness.snapshot("widget_gallery");
|
||||
harness.fit_contents();
|
||||
|
||||
let theme_name = match theme {
|
||||
egui::Theme::Light => "light",
|
||||
egui::Theme::Dark => "dark",
|
||||
};
|
||||
let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}");
|
||||
harness.snapshot(&image_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::Vec2b;
|
||||
use egui::{UiKind, Vec2b};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -149,6 +149,16 @@ impl crate::View for WindowOptions {
|
||||
self.disabled_time = ui.input(|i| i.time);
|
||||
}
|
||||
egui::reset_button(ui, self, "Reset");
|
||||
if ui
|
||||
.button("Close")
|
||||
.on_hover_text("You can collapse / close Windows via Ui::close")
|
||||
.clicked()
|
||||
{
|
||||
// Calling close would close the collapsible within the window
|
||||
// ui.close();
|
||||
// Instead, we close the window itself
|
||||
ui.close_kind(UiKind::Window);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use egui::{
|
||||
text::CCursorRange, Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui,
|
||||
Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui, text::CCursorRange,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -80,10 +80,10 @@ impl EasyMarkEditor {
|
||||
} = self;
|
||||
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
ui.add(
|
||||
@@ -96,13 +96,13 @@ impl EasyMarkEditor {
|
||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||
};
|
||||
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||
if let Some(mut ccursor_range) = state.cursor.char_range() {
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.cursor.set_char_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id)
|
||||
&& let Some(mut ccursor_range) = state.cursor.char_range()
|
||||
{
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.cursor.set_char_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
|
||||
if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) {
|
||||
// This is a placeholder till we can indent the active line
|
||||
any_change = true;
|
||||
let [primary, _secondary] = ccursor_range.sorted();
|
||||
let [primary, _secondary] = ccursor_range.sorted_cursors();
|
||||
|
||||
let advance = code.insert_text(" ", primary.index);
|
||||
ccursor_range.primary.index += advance;
|
||||
@@ -165,7 +165,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
|
||||
if ui.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
any_change = true;
|
||||
toggle_surrounding(code, ccursor_range, surrounding);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
any_change
|
||||
@@ -177,7 +177,7 @@ fn toggle_surrounding(
|
||||
ccursor_range: &mut CCursorRange,
|
||||
surrounding: &str,
|
||||
) {
|
||||
let [primary, secondary] = ccursor_range.sorted();
|
||||
let [primary, secondary] = ccursor_range.sorted_cursors();
|
||||
|
||||
let surrounding_ccount = surrounding.chars().count();
|
||||
|
||||
@@ -237,7 +237,7 @@ Goals:
|
||||
2. easy to learn
|
||||
3. similar to markdown
|
||||
|
||||
[The reference parser](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||
[The reference parser](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||
|
||||
There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list.
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::tex
|
||||
// Swallow everything up to the next special character:
|
||||
let line_end = text[skip..]
|
||||
.find('\n')
|
||||
.map_or_else(|| text.len(), |i| (skip + i + 1));
|
||||
.map_or_else(|| text.len(), |i| skip + i + 1);
|
||||
let end = text[skip..]
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
|
||||
.map_or_else(|| text.len(), |i| (skip + i).max(1));
|
||||
|
||||
@@ -113,19 +113,19 @@ impl<'a> Parser<'a> {
|
||||
|
||||
// ```{language}\n{code}```
|
||||
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||
if let Some(newline) = language_start.find('\n') {
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
if let Some(language_start) = self.s.strip_prefix("```")
|
||||
&& let Some(newline) = language_start.find('\n')
|
||||
{
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -171,14 +171,14 @@ impl<'a> Parser<'a> {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(bracket_end) = this_line.find(']') {
|
||||
let text = &this_line[1..bracket_end];
|
||||
if this_line[bracket_end + 1..].starts_with('(') {
|
||||
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
if this_line[bracket_end + 1..].starts_with('(')
|
||||
&& let Some(parens_end) = this_line[bracket_end + 2..].find(')')
|
||||
{
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::easy_mark_parser as easy_mark;
|
||||
use egui::{
|
||||
vec2, Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle,
|
||||
Ui,
|
||||
Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle, Ui,
|
||||
vec2,
|
||||
};
|
||||
|
||||
/// Parse and display a VERY simple and small subset of Markdown.
|
||||
@@ -101,7 +101,7 @@ pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
|
||||
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
|
||||
@@ -162,7 +162,7 @@ fn bullet_point(ui: &mut Ui, width: f32) -> Response {
|
||||
|
||||
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id));
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
let text = format!("{number}.");
|
||||
let text_color = ui.visuals().strong_text_color();
|
||||
|
||||
@@ -35,7 +35,7 @@ macro_rules! egui_github_link_file {
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
"https://github.com/emilk/egui/blob/main/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
@@ -49,7 +49,7 @@ macro_rules! egui_github_link_file_line {
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file_line!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
"https://github.com/emilk/egui/blob/main/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
@@ -109,6 +109,6 @@ fn test_egui_zero_window_size() {
|
||||
/// Detect narrow screens. This is used to show a simpler UI on mobile devices,
|
||||
/// especially for the web demo at <https://egui.rs>.
|
||||
pub fn is_mobile(ctx: &egui::Context) -> bool {
|
||||
let screen_size = ctx.screen_rect().size();
|
||||
let screen_size = ctx.content_rect().size();
|
||||
screen_size.x < 550.0
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use egui::{
|
||||
emath::GuiRounding as _, epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2,
|
||||
Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke,
|
||||
TextureHandle, TextureOptions, Ui, Vec2,
|
||||
Align2, Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape,
|
||||
Stroke, TextureHandle, TextureOptions, Ui, Vec2, emath::GuiRounding as _, epaint, lerp, pos2,
|
||||
vec2, widgets::color_picker::show_color,
|
||||
};
|
||||
|
||||
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
|
||||
@@ -92,8 +92,6 @@ impl ColorTest {
|
||||
|
||||
ui.label("Test that vertex color times texture color is done in gamma space:");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
let tex_color = Color32::from_rgb(64, 128, 255);
|
||||
let vertex_color = Color32::from_rgb(128, 196, 196);
|
||||
let ground_truth = mul_color_gamma(tex_color, vertex_color);
|
||||
@@ -106,6 +104,9 @@ impl ColorTest {
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
{
|
||||
let g = Gradient::one_color(ground_truth);
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
@@ -129,8 +130,36 @@ impl ColorTest {
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Test that blending is done in gamma space:");
|
||||
ui.scope(|ui| {
|
||||
let background = Color32::from_rgb(200, 60, 10);
|
||||
let foreground = Color32::from_rgba_unmultiplied(108, 65, 200, 82);
|
||||
let ground_truth = background.blend(foreground);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
ui.label("Background:");
|
||||
show_color(ui, background, color_size);
|
||||
ui.label(", foreground: ");
|
||||
show_color(ui, foreground, color_size);
|
||||
});
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
{
|
||||
let g = Gradient::one_color(ground_truth);
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
{
|
||||
let g = Gradient::one_color(foreground);
|
||||
self.vertex_gradient(ui, "Vertex blending", background, &g);
|
||||
self.tex_gradient(ui, "Texture blending", background, &g);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
// to make sure vertex and texture color multiplication is done in gamma space.
|
||||
|
||||
ui.label("Gamma interpolation:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
@@ -162,8 +191,8 @@ impl ColorTest {
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Linear interpolation (texture sampling):");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
|
||||
ui.label("Texture interpolation (texture sampling) should be in gamma space:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
}
|
||||
|
||||
fn show_gradients(
|
||||
@@ -216,11 +245,10 @@ impl ColorTest {
|
||||
let g = Gradient::endpoints(left, right);
|
||||
|
||||
match interpolation {
|
||||
Interpolation::Linear => {
|
||||
// texture sampler is sRGBA aware, and should therefore be linear
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
}
|
||||
Interpolation::Linear => {}
|
||||
Interpolation::Gamma => {
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
|
||||
// vertex shader uses gamma
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
@@ -278,7 +306,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
assert!(
|
||||
n >= 2,
|
||||
"A gradient must have at least two colors, but this had {n}"
|
||||
);
|
||||
let mut mesh = Mesh::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
@@ -298,7 +329,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Interpolation {
|
||||
/// egui used to want Linear interpolation for some things, but now we're always in gamma space.
|
||||
#[expect(unused)]
|
||||
Linear,
|
||||
|
||||
Gamma,
|
||||
}
|
||||
|
||||
@@ -387,10 +421,7 @@ impl TextureManager {
|
||||
let height = 1;
|
||||
ctx.load_texture(
|
||||
"color_test_gradient",
|
||||
epaint::ColorImage {
|
||||
size: [width, height],
|
||||
pixels,
|
||||
},
|
||||
epaint::ColorImage::new([width, height], pixels),
|
||||
TextureOptions::LINEAR,
|
||||
)
|
||||
})
|
||||
@@ -437,7 +468,7 @@ fn pixel_test_strokes(ui: &mut Ui) {
|
||||
let thickness_points = thickness_pixels / pixels_per_point;
|
||||
let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32;
|
||||
let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let size_points = size_pixels / pixels_per_point;
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
let mut cursor_pixel = Pos2::new(
|
||||
@@ -565,8 +596,14 @@ fn blending_and_feathering_test(ui: &mut Ui) {
|
||||
}
|
||||
|
||||
fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) {
|
||||
assert!(fg.is_opaque());
|
||||
assert!(bg.is_opaque());
|
||||
assert!(
|
||||
fg.is_opaque(),
|
||||
"Foreground color must be opaque, but was: {fg:?}",
|
||||
);
|
||||
assert!(
|
||||
bg.is_opaque(),
|
||||
"Background color must be opaque, but was: {bg:?}",
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
@@ -687,8 +724,8 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ColorTest;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use egui_kittest::SnapshotResults;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
pub fn rendering_test() {
|
||||
@@ -702,14 +739,15 @@ mod tests {
|
||||
});
|
||||
|
||||
{
|
||||
// Expand color-test collapsing header
|
||||
harness.get_by_label("Color test").click();
|
||||
// Expand color-test collapsing header. We accesskit-click since collapsing header
|
||||
// might not be on screen at this point.
|
||||
harness.get_by_label("Color test").click_accesskit();
|
||||
harness.run();
|
||||
}
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")));
|
||||
results.add(harness.try_snapshot(format!("rendering_test/dpi_{dpi:.2}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user