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

@@ -0,0 +1,253 @@
use accesskit::{Action, ActionRequest, NodeId};
use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler};
use eframe::epaint::text::TextWrapMode;
use egui::collapsing_header::CollapsingState;
use egui::{
Button, Color32, Context, Event, Frame, FullOutput, Id, Key, KeyboardShortcut, Label,
Modifiers, RawInput, RichText, ScrollArea, SidePanel, TopBottomPanel, Ui,
};
use std::mem;
/// This [`egui::Plugin`] adds an inspector Panel.
///
/// It can be opened with the `(Cmd/Ctrl)+Alt+I`. It shows the current AccessKit tree and details
/// for the selected node.
/// Useful when debugging accessibility issues or trying to understand the structure of the Ui.
///
/// Add via
/// ```
/// # use egui_demo_app::accessibility_inspector::AccessibilityInspectorPlugin;
/// # let ctx = egui::Context::default();
/// ctx.add_plugin(AccessibilityInspectorPlugin::default());
/// ```
#[derive(Default, Debug)]
pub struct AccessibilityInspectorPlugin {
pub open: bool,
tree: Option<accesskit_consumer::Tree>,
selected_node: Option<Id>,
queued_action: Option<ActionRequest>,
}
struct ChangeHandler;
impl TreeChangeHandler for ChangeHandler {
fn node_added(&mut self, _node: &Node<'_>) {}
fn node_updated(&mut self, _old_node: &Node<'_>, _new_node: &Node<'_>) {}
fn focus_moved(&mut self, _old_node: Option<&Node<'_>>, _new_node: Option<&Node<'_>>) {}
fn node_removed(&mut self, _node: &Node<'_>) {}
}
impl egui::Plugin for AccessibilityInspectorPlugin {
fn debug_name(&self) -> &'static str {
"Accessibility Inspector"
}
fn input_hook(&mut self, input: &mut RawInput) {
if let Some(queued_action) = self.queued_action.take() {
input
.events
.push(Event::AccessKitActionRequest(queued_action));
}
}
fn output_hook(&mut self, output: &mut FullOutput) {
if let Some(update) = output.platform_output.accesskit_update.clone() {
self.tree = match mem::take(&mut self.tree) {
None => {
// Create a new tree if it doesn't exist
Some(Tree::new(update, true))
}
Some(mut tree) => {
// Update the tree with the latest accesskit data
tree.update_and_process_changes(update, &mut ChangeHandler);
Some(tree)
}
}
}
}
fn on_begin_pass(&mut self, ctx: &Context) {
if ctx.input_mut(|i| {
i.consume_shortcut(&KeyboardShortcut::new(
Modifiers::COMMAND | Modifiers::ALT,
Key::I,
))
}) {
self.open = !self.open;
}
if !self.open {
return;
}
ctx.enable_accesskit();
SidePanel::right(Self::id()).show(ctx, |ui| {
ui.heading("🔎 AccessKit Inspector");
if let Some(selected_node) = self.selected_node {
TopBottomPanel::bottom(Self::id().with("details_panel"))
.frame(Frame::new())
.show_separator_line(false)
.show_inside(ui, |ui| {
self.selection_ui(ui, selected_node);
});
}
ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate);
ScrollArea::vertical().show(ui, |ui| {
if let Some(tree) = &self.tree {
Self::node_ui(ui, &tree.state().root(), &mut self.selected_node);
}
});
});
}
}
impl AccessibilityInspectorPlugin {
fn id() -> Id {
Id::new("Accessibility Inspector")
}
fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) {
ui.separator();
if let Some(tree) = &self.tree
&& let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value()))
{
let node_response = ui.ctx().read_response(selected_node);
if let Some(widget_response) = node_response {
ui.ctx().debug_painter().debug_rect(
widget_response.rect,
ui.style_mut().visuals.selection.bg_fill,
"",
);
}
egui::Grid::new("node_details_grid")
.num_columns(2)
.show(ui, |ui| {
ui.label("Node ID");
ui.strong(format!("{selected_node:?}"));
ui.end_row();
ui.label("Role");
ui.strong(format!("{:?}", node.role()));
ui.end_row();
ui.label("Label");
ui.add(
Label::new(RichText::new(node.label().unwrap_or_default()).strong())
.truncate(),
);
ui.end_row();
ui.label("Value");
ui.add(
Label::new(RichText::new(node.value().unwrap_or_default()).strong())
.truncate(),
);
ui.end_row();
ui.label("Children");
ui.label(RichText::new(node.children().len().to_string()).strong());
ui.end_row();
});
ui.label("Actions");
ui.horizontal_wrapped(|ui| {
// Iterate through all possible actions via the `Action::n` helper.
let mut current_action = 0;
let all_actions = std::iter::from_fn(|| {
let action = Action::n(current_action);
current_action += 1;
action
});
for action in all_actions {
if node.supports_action(action, &|_node| FilterResult::Include)
&& ui.button(format!("{action:?}")).clicked()
{
let action_request = ActionRequest {
target: node.id(),
action,
data: None,
};
self.queued_action = Some(action_request);
}
}
});
} else {
ui.label("Node not found");
}
}
fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option<Id>) {
if node.id() == Self::id().value().into()
|| node
.value()
.as_deref()
.is_some_and(|l| l.contains("AccessKit Inspector"))
{
return;
}
let label = node
.label()
.or(node.value())
.unwrap_or(node.id().0.to_string());
let label = format!("({:?}) {}", node.role(), label);
// Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
#[expect(unsafe_code)]
let egui_node_id = unsafe { Id::from_high_entropy_bits(node.id().0) };
ui.push_id(node.id(), |ui| {
let child_count = node.children().len();
let has_children = child_count > 0;
let default_open = child_count == 1 && node.role() != accesskit::Role::Label;
let mut collapsing = CollapsingState::load_with_default_open(
ui.ctx(),
egui_node_id.with("ak_collapse"),
default_open,
);
let header_response = ui.horizontal(|ui| {
let text = if collapsing.is_open() { "" } else { "" };
if ui
.add_visible(has_children, Button::new(text).frame_when_inactive(false))
.clicked()
{
collapsing.set_open(!collapsing.is_open());
}
let label_response =
ui.selectable_value(selected_node, Some(egui_node_id), label.clone());
if label_response.hovered() {
let widget_response = ui.ctx().read_response(egui_node_id);
if let Some(widget_response) = widget_response {
ui.ctx()
.debug_painter()
.debug_rect(widget_response.rect, Color32::RED, "");
}
}
});
if has_children {
collapsing.show_body_indented(&header_response.response, ui, |ui| {
node.children().for_each(|c| {
Self::node_ui(ui, &c, selected_node);
});
});
}
collapsing.store(ui.ctx());
});
}
}

View File

@@ -80,7 +80,7 @@ struct RotatingTriangle {
vertex_array: glow::VertexArray,
}
#[allow(unsafe_code)] // we need unsafe code to use glow
#[expect(unsafe_code)] // we need unsafe code to use glow
impl RotatingTriangle {
fn new(gl: &glow::Context) -> Option<Self> {
use glow::HasContext as _;
@@ -91,10 +91,7 @@ impl RotatingTriangle {
let program = gl.create_program().expect("Cannot create program");
if !shader_version.is_new_shader_interface() {
log::warn!(
"Custom 3D painting hasn't been ported to {:?}",
shader_version
);
log::warn!("Custom 3D painting hasn't been ported to {shader_version:?}");
return None;
}

View File

@@ -1,7 +1,7 @@
use std::num::NonZeroU64;
use eframe::{
egui_wgpu::wgpu::util::DeviceExt,
egui_wgpu::wgpu::util::DeviceExt as _,
egui_wgpu::{self, wgpu},
};

View File

@@ -1,8 +1,8 @@
use egui::{
Color32, Painter, Pos2, Rect, Shape, Stroke, Ui, Vec2,
containers::{CollapsingHeader, Frame},
emath, pos2,
widgets::Slider,
Color32, Painter, Pos2, Rect, Shape, Stroke, Ui, Vec2,
};
use std::f32::consts::TAU;
@@ -73,7 +73,7 @@ impl FractalClock {
));
} else {
ui.label("The fractal_clock clock is not showing the correct time");
};
}
ui.label(format!("Painted line count: {}", self.line_count));
ui.checkbox(&mut self.paused, "Paused");

View File

@@ -53,7 +53,7 @@ pub struct HttpApp {
impl Default for HttpApp {
fn default() -> Self {
Self {
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
url: "https://raw.githubusercontent.com/emilk/egui/main/README.md".to_owned(),
promise: Default::default(),
}
}
@@ -133,7 +133,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &eframe::Frame, url: &mut String) -> bool {
ui.horizontal(|ui| {
if ui.button("Source code for this example").clicked() {
*url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}",
"https://raw.githubusercontent.com/emilk/egui/main/{}",
file!()
);
trigger_fetch = true;
@@ -238,7 +238,7 @@ impl ColoredText {
pub fn ui(&self, ui: &mut egui::Ui) {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts(|f| f.layout_job(job));
let galley = ui.fonts_mut(|f| f.layout_job(job));
ui.add(egui::Label::new(galley).selectable(true));
}
}

View File

@@ -1,10 +1,9 @@
use egui::emath::Rot2;
use egui::panel::HorizontalSide;
use egui::panel::PanelSide;
use egui::panel::VerticalSide;
use egui::ImageFit;
use egui::Slider;
use egui::Vec2;
use egui::emath::Rot2;
use egui::panel::Side;
use egui::panel::TopBottomSide;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ImageViewer {
@@ -53,31 +52,28 @@ impl Default for ImageViewer {
impl eframe::App for ImageViewer {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::Panel::new(PanelSide::Horizontal(HorizontalSide::Top), "url bar").show(
ctx,
|ui| {
ui.horizontal_centered(|ui| {
let label = ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text)
.labelled_by(label.id);
if ui.small_button("").clicked() {
ctx.forget_image(&self.current_uri);
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
self.current_uri = self.uri_edit_text.clone();
};
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
ui.horizontal_centered(|ui| {
let label = ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text)
.labelled_by(label.id);
if ui.small_button("").clicked() {
ctx.forget_image(&self.current_uri);
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
self.current_uri = self.uri_edit_text.clone();
}
#[cfg(not(target_arch = "wasm32"))]
if ui.button("file…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.uri_edit_text = format!("file://{}", path.display());
self.current_uri = self.uri_edit_text.clone();
}
}
});
},
);
#[cfg(not(target_arch = "wasm32"))]
if ui.button("file…").clicked()
&& let Some(path) = rfd::FileDialog::new().pick_file()
{
self.uri_edit_text = format!("file://{}", path.display());
self.current_uri = self.uri_edit_text.clone();
}
});
});
egui::Panel::new(PanelSide::Vertical(VerticalSide::Left), "controls").show(ctx, |ui| {
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
// uv
ui.label("UV");
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));

View File

@@ -110,7 +110,7 @@ impl BackendPanel {
if cfg!(debug_assertions) && cfg!(target_arch = "wasm32") {
ui.separator();
// For testing panic handling on web:
#[allow(clippy::manual_assert)]
#[expect(clippy::manual_assert)]
if ui.button("panic!()").clicked() {
panic!("intentional panic!");
}
@@ -146,6 +146,10 @@ impl BackendPanel {
// builds to keep the noise down in the official demo.
if cfg!(debug_assertions) {
ui.collapsing("More…", |ui| {
ui.horizontal(|ui| {
ui.label("Total ui frames:");
ui.monospace(ui.ctx().cumulative_frame_nr().to_string());
});
ui.horizontal(|ui| {
ui.label("Total ui passes:");
ui.monospace(ui.ctx().cumulative_pass_nr().to_string());
@@ -183,7 +187,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.label("egui running inside ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
"https://github.com/emilk/egui/tree/main/crates/eframe",
);
ui.label(".");
});
@@ -333,7 +337,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
ui.ctx()
.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
ui.close_menu();
ui.close();
}
});
}

View File

@@ -53,7 +53,7 @@ impl FrameHistory {
}
fn graph(&self, ui: &mut egui::Ui) -> egui::Response {
use egui::{emath, epaint, pos2, vec2, Pos2, Rect, Sense, Shape, Stroke, TextStyle};
use egui::{Pos2, Rect, Sense, Shape, Stroke, TextStyle, emath, epaint, pos2, vec2};
ui.label("egui CPU usage history");
@@ -90,7 +90,7 @@ impl FrameHistory {
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| {
shapes.push(ui.fonts_mut(|f| {
Shape::text(
f,
pos2(rect.left(), y),

View File

@@ -10,13 +10,14 @@ pub use wrap_app::{Anchor, WrapApp};
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> f64 {
use chrono::Timelike;
use chrono::Timelike as _;
let time = chrono::Local::now().time();
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
}
// ----------------------------------------------------------------------------
#[cfg(feature = "accessibility_inspector")]
pub mod accessibility_inspector;
#[cfg(target_arch = "wasm32")]
mod web;

View File

@@ -4,8 +4,11 @@
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
#![allow(clippy::never_loop)] // False positive
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator, can give 20% speedups: https://github.com/emilk/egui/pull/7029
// When compiling natively:
fn main() -> eframe::Result {
fn main() {
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--profile" => {
@@ -13,7 +16,9 @@ fn main() -> eframe::Result {
start_puffin_server();
#[cfg(not(feature = "puffin"))]
panic!("Unknown argument: {arg} - you need to enable the 'puffin' feature to use this.");
panic!(
"Unknown argument: {arg} - you need to enable the 'puffin' feature to use this."
);
}
_ => {
@@ -36,7 +41,12 @@ fn main() -> eframe::Result {
rust_log += &format!(",{loud_crate}=warn");
}
}
std::env::set_var("RUST_LOG", rust_log);
// SAFETY: we call this from the main thread without any other threads running.
#[expect(unsafe_code)]
unsafe {
std::env::set_var("RUST_LOG", rust_log);
}
}
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
@@ -52,11 +62,27 @@ fn main() -> eframe::Result {
..Default::default()
};
eframe::run_native(
let result = eframe::run_native(
"egui demo app",
options,
Box::new(|cc| Ok(Box::new(egui_demo_app::WrapApp::new(cc)))),
)
);
match result {
Ok(()) => {}
Err(err) => {
// This produces a nicer error message than returning the `Result`:
print_error_and_exit(&err);
}
}
}
fn print_error_and_exit(err: &eframe::Error) -> ! {
#![expect(clippy::print_stderr)]
#![expect(clippy::exit)]
eprintln!("Error: {err}");
std::process::exit(1)
}
#[cfg(feature = "puffin")]
@@ -75,11 +101,11 @@ fn start_puffin_server() {
// We can store the server if we want, but in this case we just want
// it to keep running. Dropping it closes the server, so let's not drop it!
#[allow(clippy::mem_forget)]
#[expect(clippy::mem_forget)]
std::mem::forget(puffin_server);
}
Err(err) => {
log::error!("Failed to start puffin server: {err}");
}
};
}
}

View File

@@ -14,7 +14,7 @@ pub struct WebHandle {
#[wasm_bindgen]
impl WebHandle {
/// Installs a panic hook, then returns.
#[allow(clippy::new_without_default)]
#[allow(clippy::new_without_default, clippy::allow_attributes)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
// Redirect [`log`] message to `console.log` and friends:

View File

@@ -80,9 +80,10 @@ impl eframe::App for ColorTestApp {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Anchor {
#[default]
Demo,
EasyMarkEditor,
@@ -134,13 +135,7 @@ impl std::fmt::Display for Anchor {
impl From<Anchor> for egui::WidgetText {
fn from(value: Anchor) -> Self {
Self::RichText(egui::RichText::new(value.to_string()))
}
}
impl Default for Anchor {
fn default() -> Self {
Self::Demo
Self::from(value.to_string())
}
}
@@ -188,7 +183,11 @@ impl WrapApp {
// This gives us image support:
egui_extras::install_image_loaders(&cc.egui_ctx);
#[allow(unused_mut)]
#[cfg(feature = "accessibility_inspector")]
cc.egui_ctx
.add_plugin(crate::accessibility_inspector::AccessibilityInspectorPlugin::default());
#[allow(unused_mut, clippy::allow_attributes)]
let mut slf = Self {
state: State::default(),
@@ -199,10 +198,10 @@ impl WrapApp {
};
#[cfg(feature = "persistence")]
if let Some(storage) = cc.storage {
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
slf.state = state;
}
if let Some(storage) = cc.storage
&& let Some(state) = eframe::get_value(storage, eframe::APP_KEY)
{
slf.state = state;
}
slf
@@ -384,12 +383,12 @@ impl WrapApp {
.clicked()
{
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
ui.close();
}
if ui.button("Reset everything").clicked() {
*cmd = Command::ResetEverything;
ui.close_menu();
ui.close();
}
});
}
@@ -472,10 +471,10 @@ impl WrapApp {
let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
let content_rect = ctx.content_rect();
painter.rect_filled(content_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
content_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Heading.resolve(&ctx.style()),