1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00
Files
egui/tests/test_ui_stack/src/main.rs
Tau Gärtli b5627c7d40 Make Light & Dark Visuals Customizable When Following The System Theme (#4744)
* Closes <https://github.com/emilk/egui/issues/4490>
* [x] I have followed the instructions in the PR template

---

Unfortunately, this PR contains a bunch of breaking changes because
`Context` no longer has one style, but two. I could try to add some of
the methods back if that's desired.

The most subtle change is probably that `style_mut` mutates both the
dark and the light style (which from the usage in egui itself felt like
the right choice but might be surprising to users).

I decided to deviate a bit from the data structure suggested in the
linked issue.
Instead of this:
```rust
pub theme: Theme, // Dark or Light
pub follow_system_theme: bool, // Change [`Self::theme`] based on `RawInput::system_theme`?
```

I decided to add a `ThemePreference` enum and track the current system
theme separately.
This has a couple of benefits:
* The user's theme choice is not magically overwritten on the next
frame.
* A widget for changing the theme preference only needs to know the
`ThemePreference` and not two values.
* Persisting the `theme_preference` is fine (as opposed to persisting
the `theme` field which may actually be the system theme).

The `small_toggle_button` currently only toggles between dark and light
(so you can never get back to following the system). I think it's easy
to improve on this in a follow-up PR :)
I made the function `pub(crate)` for now because it should eventually be
a method on `ThemePreference`, not `Theme`.

To showcase the new capabilities I added a new example that uses
different "accent" colors in dark and light mode:

<img
src="https://github.com/user-attachments/assets/0bf728c6-2720-47b0-a908-18bd250d15a6"
width="250" alt="A screenshot of egui's widget gallery demo in dark mode
using a purple accent color instead of the default blue accent">

<img
src="https://github.com/user-attachments/assets/e816b380-3e59-4f11-b841-8c20285988d6"
width="250" alt="A screenshot of egui's widget gallery demo in light
mode using a green accent color instead of the default blue accent">

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2024-09-11 17:52:53 +02:00

387 lines
14 KiB
Rust

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
use eframe::egui;
use eframe::egui::{Rangef, Shape, UiKind};
use egui_extras::Column;
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
..Default::default()
};
eframe::run_native(
"Stack Frame Demo",
options,
Box::new(|cc| {
// This gives us image support:
egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::<MyApp>::default())
}),
)
}
#[derive(Default)]
struct MyApp {
show_settings: bool,
show_inspection: bool,
show_memory: bool,
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.all_styles_mut(|style| style.interaction.tooltip_delay = 0.0);
egui::SidePanel::left("side_panel_left").show(ctx, |ui| {
ui.heading("Information");
ui.label(
"This is a demo/test environment of the `UiStack` feature. The tables display \
the UI stack in various contexts. You can hover on the IDs to display the \
corresponding origin/`max_rect`.\n\n\
The \"Full span test\" labels showcase an implementation of full-span \
highlighting. Hover to see them in action!",
);
ui.add_space(10.0);
ui.checkbox(&mut self.show_settings, "🔧 Settings");
ui.checkbox(&mut self.show_inspection, "🔍 Inspection");
ui.checkbox(&mut self.show_memory, "📝 Memory");
ui.add_space(10.0);
if ui.button("Reset egui memory").clicked() {
ctx.memory_mut(|mem| *mem = Default::default());
}
ui.add_space(20.0);
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
stack_ui(ui);
// full span test
ui.add_space(20.0);
full_span_widget(ui, false);
// nested frames test
ui.add_space(20.0);
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(4.0),
outer_margin: egui::Margin::same(4.0),
..Default::default()
}
.show(ui, |ui| {
full_span_widget(ui, false);
stack_ui(ui);
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(8.0),
outer_margin: egui::Margin::same(6.0),
..Default::default()
}
.show(ui, |ui| {
full_span_widget(ui, false);
stack_ui(ui);
});
});
});
});
egui::SidePanel::right("side_panel_right").show(ctx, |ui| {
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
stack_ui(ui);
// full span test
ui.add_space(20.0);
full_span_widget(ui, false);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
ui.label("stack here:");
stack_ui(ui);
// full span test
ui.add_space(20.0);
full_span_widget(ui, false);
// tooltip test
ui.add_space(20.0);
ui.label("Hover me").on_hover_ui(|ui| {
full_span_widget(ui, true);
ui.add_space(20.0);
stack_ui(ui);
});
// combobox test
ui.add_space(20.0);
egui::ComboBox::from_id_salt("combo_box")
.selected_text("click me")
.show_ui(ui, |ui| {
full_span_widget(ui, true);
ui.add_space(20.0);
stack_ui(ui);
});
// Ui nesting test
ui.add_space(20.0);
ui.label("UI nesting test:");
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(4.0),
..Default::default()
}
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.scope(stack_ui);
});
});
});
// table test
let mut cell_stack = None;
ui.add_space(20.0);
ui.label("Table test:");
egui_extras::TableBuilder::new(ui)
.vscroll(false)
.column(Column::auto())
.column(Column::auto())
.header(20.0, |mut header| {
header.col(|ui| {
ui.strong("column 1");
});
header.col(|ui| {
ui.strong("column 2");
});
})
.body(|mut body| {
body.row(20.0, |mut row| {
row.col(|ui| {
full_span_widget(ui, false);
});
row.col(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.label("See stack below");
cell_stack = Some(ui.stack().clone());
});
});
});
if let Some(cell_stack) = cell_stack {
ui.label("Cell's stack:");
stack_ui_impl(ui, &cell_stack);
}
});
});
egui::TopBottomPanel::bottom("bottom_panel")
.resizable(true)
.show(ctx, |ui| {
egui::ScrollArea::vertical()
.auto_shrink(false)
.show(ui, |ui| {
stack_ui(ui);
// full span test
ui.add_space(20.0);
full_span_widget(ui, false);
});
});
egui::Window::new("Window")
.pivot(egui::Align2::RIGHT_TOP)
.show(ctx, |ui| {
full_span_widget(ui, false);
ui.add_space(20.0);
stack_ui(ui);
});
egui::Window::new("🔧 Settings")
.open(&mut self.show_settings)
.vscroll(true)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
egui::Window::new("🔍 Inspection")
.open(&mut self.show_inspection)
.vscroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
egui::Window::new("📝 Memory")
.open(&mut self.show_memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
}
}
/// Demo of a widget that highlights its background all the way to the edge of its container when
/// hovered.
fn full_span_widget(ui: &mut egui::Ui, permanent: bool) {
let bg_shape_idx = ui.painter().add(Shape::Noop);
let response = ui.label("Full span test");
let ui_stack = ui.stack();
let rect = egui::Rect::from_x_y_ranges(
full_span_horizontal_range(ui_stack),
response.rect.y_range(),
);
if permanent || response.hovered() {
ui.painter().set(
bg_shape_idx,
Shape::rect_filled(rect, 0.0, ui.visuals().selection.bg_fill),
);
}
}
/// Find the horizontal range of the enclosing container.
fn full_span_horizontal_range(ui_stack: &egui::UiStack) -> Rangef {
for node in ui_stack.iter() {
if node.has_visible_frame()
|| node.is_panel_ui()
|| node.is_root_ui()
|| node.kind() == Some(UiKind::TableCell)
{
return (node.max_rect + node.frame().inner_margin).x_range();
}
}
// should never happen
Rangef::EVERYTHING
}
fn stack_ui(ui: &mut egui::Ui) {
let ui_stack = ui.stack().clone();
ui.scope(|ui| {
stack_ui_impl(ui, &ui_stack);
});
}
fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
egui::Frame {
stroke: ui.style().noninteractive().fg_stroke,
inner_margin: egui::Margin::same(4.0),
..Default::default()
}
.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
egui_extras::TableBuilder::new(ui)
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.header(20.0, |mut header| {
header.col(|ui| {
ui.strong("id");
});
header.col(|ui| {
ui.strong("kind");
});
header.col(|ui| {
ui.strong("stroke");
});
header.col(|ui| {
ui.strong("inner");
});
header.col(|ui| {
ui.strong("outer");
});
header.col(|ui| {
ui.strong("direction");
});
})
.body(|mut body| {
for node in stack.iter() {
body.row(20.0, |mut row| {
row.col(|ui| {
if ui.label(format!("{:?}", node.id)).hovered() {
ui.ctx().debug_painter().debug_rect(
node.max_rect,
egui::Color32::GREEN,
"max",
);
ui.ctx().debug_painter().circle_filled(
node.min_rect.min,
2.0,
egui::Color32::RED,
);
}
});
row.col(|ui| {
let s = if let Some(kind) = node.kind() {
format!("{kind:?}")
} else {
"-".to_owned()
};
ui.label(s);
});
row.col(|ui| {
let frame = node.frame();
if frame.stroke == egui::Stroke::NONE {
ui.label("-");
} else {
let mut layout_job = egui::text::LayoutJob::default();
layout_job.append(
"",
0.0,
egui::TextFormat::simple(
egui::TextStyle::Body.resolve(ui.style()),
frame.stroke.color,
),
);
layout_job.append(
format!("{}px", frame.stroke.width).as_str(),
0.0,
egui::TextFormat::simple(
egui::TextStyle::Body.resolve(ui.style()),
ui.style().visuals.text_color(),
),
);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.label(layout_job);
}
});
row.col(|ui| {
ui.label(print_margin(&node.frame().inner_margin));
});
row.col(|ui| {
ui.label(print_margin(&node.frame().outer_margin));
});
row.col(|ui| {
ui.label(format!("{:?}", node.layout_direction));
});
});
}
});
});
}
fn print_margin(margin: &egui::Margin) -> String {
if margin.is_same() {
format!("{}px", margin.left)
} else {
let s1 = if margin.left == margin.right {
format!("H: {}px", margin.left)
} else {
format!("L: {}px R: {}px", margin.left, margin.right)
};
let s2 = if margin.top == margin.bottom {
format!("V: {}px", margin.top)
} else {
format!("T: {}px B: {}px", margin.top, margin.bottom)
};
format!("{s1} / {s2}")
}
}