1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Merge branch 'main' into lucas/experiments/measure-widget-size

# Conflicts:
#	crates/egui/src/atomics/atom.rs
#	crates/egui/src/atomics/atom_kind.rs
#	crates/egui/src/atomics/atom_layout.rs
#	crates/egui/src/containers/sides.rs
#	crates/egui/src/placer.rs
#	crates/egui/src/widgets/label.rs
#	crates/egui/src/widgets/selected_label.rs
#	crates/egui_demo_lib/src/demo/dancing_strings.rs
#	crates/egui_extras/src/layout.rs
#	crates/epaint/src/text/text_layout_types.rs
This commit is contained in:
lucasmerlin
2025-07-14 15:35:38 +02:00
336 changed files with 3896 additions and 1837 deletions

View File

@@ -39,7 +39,7 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.84.0
toolchain: 1.85.0
override: true
- uses: Swatinem/rust-cache@v2

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:

View File

@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
rust-version: "1.84.0"
rust-version: "1.85.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -170,7 +170,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
targets: aarch64-linux-android
- name: Set up cargo cache
@@ -191,7 +191,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -210,7 +210,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -234,7 +234,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.84.0
toolchain: 1.85.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -18,5 +18,133 @@ teselation = "tessellation"
tessalation = "tessellation"
tesselation = "tessellation"
# Use the more common spelling
adaptor = "adapter"
adaptors = "adapters"
# For consistency we prefer American English:
aeroplane = "airplane"
analogue = "analog"
analyse = "analyze"
appetiser = "appetizer"
arbour = "arbor"
ardour = "arbor"
armour = "armor"
artefact = "artifact"
authorise = "authorize"
behaviour = "behavior"
behavioural = "behavioral"
British = "American"
calibre = "caliber"
# cancelled = "canceled" # winit uses this :(
candour = "candor"
capitalise = "capitalize"
catalogue = "catalog"
centre = "center"
characterise = "characterize"
chequerboard = "checkerboard"
chequered = "checkered"
civilise = "civilize"
clamour = "clamor"
colonise = "colonize"
colour = "color"
coloured = "colored"
cosy = "cozy"
criticise = "criticize"
defence = "defense"
demeanour = "demeanor"
dialogue = "dialog"
distil = "distill"
doughnut = "donut"
dramatise = "dramatize"
draught = "draft"
emphasise = "emphasize"
endeavour = "endeavor"
enrol = "enroll"
epilogue = "epilog"
equalise = "equalize"
favour = "favor"
favourite = "favorite"
fibre = "fiber"
flavour = "flavor"
fulfil = "fufill"
gaol = "jail"
grey = "gray"
greys = "grays"
greyscale = "grayscale"
harbour = "habor"
honour = "honor"
humour = "humor"
instalment = "installment"
instil = "instill"
jewellery = "jewelry"
kerb = "curb"
labour = "labor"
litre = "liter"
lustre = "luster"
meagre = "meager"
metre = "meter"
mobilise = "mobilize"
monologue = "monolog"
naturalise = "naturalize"
neighbour = "neighbor"
neighbourhood = "neighborhood"
normalise = "normalize"
normalised = "normalized"
odour = "odor"
offence = "offense"
organise = "organize"
parlour = "parlor"
plough = "plow"
popularise = "popularize"
pretence = "pretense"
programme = "program"
prologue = "prolog"
rancour = "rancor"
realise = "realize"
recognise = "recognize"
recognised = "recognized"
rigour = "rigor"
rumour = "rumor"
sabre = "saber"
satirise = "satirize"
saviour = "savior"
savour = "savor"
sceptical = "skeptical"
sceptre = "scepter"
sepulchre = "sepulcher"
serialisation = "serialization"
serialise = "serialize"
serialised = "serialized"
skilful = "skillful"
sombre = "somber"
specialisation = "specialization"
specialise = "specialize"
specialised = "specialized"
splendour = "splendor"
standardise = "standardize"
sulphur = "sulfur"
symbolise = "symbolize"
theatre = "theater"
tonne = "ton"
travelogue = "travelog"
tumour = "tumor"
valour = "valor"
vaporise = "vaporize"
vigour = "vigor"
# null-terminated is the name of the wikipedia article!
# https://en.wikipedia.org/wiki/Null-terminated_string
nullterminated = "null-terminated"
zeroterminated = "null-terminated"
zero-terminated = "null-terminated"
[files]
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
[default]
extend-ignore-re = [
"#\\[doc\\(alias = .*", # We suggest "grey" in some doc
]

View File

@@ -14,6 +14,271 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10 - Atoms, popups, and better SVG support
This is a big egui release, with several exciting new features!
* _Atoms_ are new layout primitives in egui, for text and images
* Popups, tooltips and menus have undergone a complete rewrite
* Much improved SVG support
* Crisper graphics (especially text!)
Let's dive in!
### ⚛️ Atoms
`egui::Atom` is the new, indivisible building blocks of egui (hence their name).
An `Atom` is an `enum` that can be either `WidgetText`, `Image`, or `Custom`.
The new `AtomLayout` can be used within widgets to do basic layout.
The initial implementation is as minimal as possible, doing just enough to implement what `Button` could do before.
There is a new `IntoAtoms` trait that works with tuples of `Atom`s. Each atom can be customized with the `AtomExt` trait
which works on everything that implements `Into<Atom>`, so e.g. `RichText` or `Image`.
So to create a `Button` with text and image you can now do:
```rs
let image = include_image!("my_icon.png").atom_size(Vec2::splat(12.0));
ui.button((image, "Click me!"));
```
Anywhere you see `impl IntoAtoms` you can add any number of images and text, in any order.
As of 0.32, we have ported the `Button`, `Checkbox`, `RadioButton` to use atoms
(meaning they support adding Atoms and are built on top of `AtomLayout`).
The `Button` implementation is not only more powerful now, but also much simpler, removing ~130 lines of layout math.
In combination with `ui.read_response`, custom widgets are really simple now, here is a minimal button implementation:
```rs
pub struct ALButton<'a> {
al: AtomLayout<'a>,
}
impl<'a> ALButton<'a> {
pub fn new(content: impl IntoAtoms<'a>) -> Self {
Self {
al: AtomLayout::new(content.into_atoms()).sense(Sense::click()),
}
}
}
impl<'a> Widget for ALButton<'a> {
fn ui(mut self, ui: &mut Ui) -> Response {
let Self { al } = self;
let response = ui.ctx().read_response(ui.next_auto_id());
let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| {
ui.style().interact(&response)
});
let al = al.frame(
Frame::new()
.inner_margin(ui.style().spacing.button_padding)
.fill(visuals.bg_fill)
.stroke(visuals.bg_stroke)
.corner_radius(visuals.corner_radius),
);
al.show(ui).response
}
}
```
You can even use `Atom::custom` to add custom content to Widgets. Here is a button in a button:
https://github.com/user-attachments/assets/8c649784-dcc5-4979-85f8-e735b9cdd090
```rs
let custom_button_id = Id::new("custom_button");
let response = Button::new((
Atom::custom(custom_button_id, Vec2::splat(18.0)),
"Look at my mini button!",
))
.atom_ui(ui);
if let Some(rect) = response.rect(custom_button_id) {
ui.put(rect, Button::new("🔎").frame_when_inactive(false));
}
```
Currently, you need to use `atom_ui` to get a `AtomResponse` which will have the `Rect` to use, but in the future
this could be streamlined, e.g. by adding a `AtomKind::Callback` or by passing the Rects back with `egui::Response`.
Basing our widgets on `AtomLayout` also allowed us to improve `Response::intrinsic_size`, which will now report the
correct size even if widgets are truncated. `intrinsic_size` is the size that a non-wrapped, non-truncated,
non-justified version of the widget would have, and can be useful in advanced layout
calculations like [egui_flex](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
##### Details
* Add `AtomLayout`, abstracting layouting within widgets [#5830](https://github.com/emilk/egui/pull/5830) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `Galley::intrinsic_size` and use it in `AtomLayout` [#7146](https://github.com/emilk/egui/pull/7146) by [@lucasmerlin](https://github.com/lucasmerlin)
### ❕ Improved popups, tooltips, and menus
Introduces a new `egui::Popup` api. Checkout the new demo on https://egui.rs:
https://github.com/user-attachments/assets/74e45243-7d05-4fc3-b446-2387e1412c05
We introduced a new `RectAlign` helper to align a rect relative to an other rect. The `Popup` will by default try to find the best `RectAlign` based on the source widgets position (previously submenus would annoyingly overlap if at the edge of the window):
https://github.com/user-attachments/assets/0c5adb6b-8310-4e0a-b936-646bb4ec02f7
`Tooltip` and `menu` have been rewritten based on the new `Popup` api. They are now compatible with each other, meaning you can just show a `ui.menu_button()` in any `Popup` to get a sub menu. There are now customizable `MenuButton` and `SubMenuButton` structs, to help with customizing your menu buttons. This means menus now also support `PopupCloseBehavior` so you can remove your `close_menu` calls from your click handlers!
The old tooltip and popup apis have been ported to the new api so there should be very little breaking changes. The old menu is still around but deprecated. `ui.menu_button` etc now open the new menu, if you can't update to the new one immediately you can use the old buttons from the deprecated `egui::menu` menu.
We also introduced `ui.close()` which closes the nearest container. So you can now conveniently close `Window`s, `Collapsible`s, `Modal`s and `Popup`s from within. To use this for your own containers, call `UiBuilder::closable` and then check for closing within that ui via `ui.should_close()`.
##### Details
* Add `Popup` and `Tooltip`, unifying the previous behaviours [#5713](https://github.com/emilk/egui/pull/5713) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `Ui::close` and `Response::should_close` [#5729](https://github.com/emilk/egui/pull/5729) by [@lucasmerlin](https://github.com/lucasmerlin)
* ⚠️ Improved menu based on `egui::Popup` [#5716](https://github.com/emilk/egui/pull/5716) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add a toggle for the compact menu style [#5777](https://github.com/emilk/egui/pull/5777) by [@s-nie](https://github.com/s-nie)
* Use the new `Popup` API for the color picker button [#7137](https://github.com/emilk/egui/pull/7137) by [@lucasmerlin](https://github.com/lucasmerlin)
* ⚠️ Close popup if `Memory::keep_popup_open` isn't called [#5814](https://github.com/emilk/egui/pull/5814) by [@juancampa](https://github.com/juancampa)
* Fix tooltips sometimes changing position each frame [#7304](https://github.com/emilk/egui/pull/7304) by [@emilk](https://github.com/emilk)
* Change popup memory to be per-viewport [#6753](https://github.com/emilk/egui/pull/6753) by [@mkalte666](https://github.com/mkalte666)
* Deprecate `Memory::popup` API in favor of new `Popup` API [#7317](https://github.com/emilk/egui/pull/7317) by [@emilk](https://github.com/emilk)
### ▲ Improved SVG support
You can render SVG in egui with
```rs
ui.add(egui::Image::new(egui::include_image!("icon.svg"));
```
(Requires the use of `egui_extras`, with the `svg` feature enabled and a call to [`install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html)).
Previously this would sometimes result in a blurry SVG, epecially if the `Image` was set to be dynamically scale based on the size of the `Ui` that contained it. Now SVG:s are always pixel-perfect, for truly scalable graphics.
![svg-scaling](https://github.com/user-attachments/assets/faf63f0c-0ff7-47a0-a4cb-7210efeccb72)
##### Details
* Support text in SVGs [#5979](https://github.com/emilk/egui/pull/5979) by [@cernec1999](https://github.com/cernec1999)
* Fix sometimes blurry SVGs [#7071](https://github.com/emilk/egui/pull/7071) by [@emilk](https://github.com/emilk)
* Fix incorrect color fringe colors on SVG:s [#7069](https://github.com/emilk/egui/pull/7069) by [@emilk](https://github.com/emilk)
* Make `Image::paint_at` pixel-perfect crisp for SVG images [#7078](https://github.com/emilk/egui/pull/7078) by [@emilk](https://github.com/emilk)
### ✨ Crisper graphics
Non-SVG icons are also rendered better, and text sharpness has been improved, especially in light mode.
![image](https://github.com/user-attachments/assets/7f370aaf-886a-423c-8391-c378849b63ca)
##### Details
* Improve text sharpness [#5838](https://github.com/emilk/egui/pull/5838) by [@emilk](https://github.com/emilk)
* Improve text rendering in light mode [#7290](https://github.com/emilk/egui/pull/7290) by [@emilk](https://github.com/emilk)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
* Make text underline and strikethrough pixel perfect crisp [#5857](https://github.com/emilk/egui/pull/5857) by [@emilk](https://github.com/emilk)
### Migration guide
We have some silently breaking changes (code compiles fine but behavior changed) that require special care:
#### Menus close on click by default
- previously menus would only close on click outside
- either
- remove the `ui.close_menu()` calls from button click handlers since they are obsolete
- if the menu should stay open on clicks, change the `PopupCloseBehavior`:
```rs
// Change this
ui.menu_button("Text", |ui| { /* Menu Content */ });
// To this:
MenuButton::new("Text").config(
MenuConfig::default().close_behavior(PopupCloseBehavior::CloseOnClickOutside),
).ui(ui, |ui| { /* Menu Content */ });
```
You can also change the behavior only for a single SubMenu by using `SubMenuButton`, but by default it should be passed to any submenus when using `MenuButton`.
#### `Memory::is_popup_open` api now requires calls to `Memory::keep_popup_open`
- The popup will immediately close if `keep_popup_open` is not called.
- It's recommended to use the new `Popup` api which handles this for you.
- If you can't switch to the new api for some reason, update the code to call `keep_popup_open`:
```rs
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
ui.memory_mut(|mem| mem.keep_popup_open(popup_id)); // <- add this line
let area_response = Area::new(popup_id).show(...)
}
```
### ⭐ Other improvements
* Add `Label::show_tooltip_when_elided` [#5710](https://github.com/emilk/egui/pull/5710) by [@bryceberger](https://github.com/bryceberger)
* Deprecate `Ui::allocate_new_ui` in favor of `Ui::scope_builder` [#5764](https://github.com/emilk/egui/pull/5764) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `expand_bg` to customize size of text background [#5365](https://github.com/emilk/egui/pull/5365) by [@MeGaGiGaGon](https://github.com/MeGaGiGaGon)
* Add assert messages and print bad argument values in asserts [#5216](https://github.com/emilk/egui/pull/5216) by [@bircni](https://github.com/bircni)
* Use `TextBuffer` for `layouter` in `TextEdit` instead of `&str` [#5712](https://github.com/emilk/egui/pull/5712) by [@kernelkind](https://github.com/kernelkind)
* Add a `Slider::update_while_editing(bool)` API [#5978](https://github.com/emilk/egui/pull/5978) by [@mbernat](https://github.com/mbernat)
* Add `Scene::drag_pan_buttons` option. Allows specifying which pointer buttons pan the scene by dragging [#5892](https://github.com/emilk/egui/pull/5892) by [@mitchmindtree](https://github.com/mitchmindtree)
* Add `Scene::sense` to customize how `Scene` responds to user input [#5893](https://github.com/emilk/egui/pull/5893) by [@mitchmindtree](https://github.com/mitchmindtree)
* Rework `TextEdit` arrow navigation to handle Unicode graphemes [#5812](https://github.com/emilk/egui/pull/5812) by [@MStarha](https://github.com/MStarha)
* `ScrollArea` improvements for user configurability [#5443](https://github.com/emilk/egui/pull/5443) by [@MStarha](https://github.com/MStarha)
* Add `Response::clicked_with_open_in_background` [#7093](https://github.com/emilk/egui/pull/7093) by [@emilk](https://github.com/emilk)
* Add `Modifiers::matches_any` [#7123](https://github.com/emilk/egui/pull/7123) by [@emilk](https://github.com/emilk)
* Add `Context::format_modifiers` [#7125](https://github.com/emilk/egui/pull/7125) by [@emilk](https://github.com/emilk)
* Add `OperatingSystem::is_mac` [#7122](https://github.com/emilk/egui/pull/7122) by [@emilk](https://github.com/emilk)
* Support vertical-only scrolling by holding down Alt [#7124](https://github.com/emilk/egui/pull/7124) by [@emilk](https://github.com/emilk)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Select all text in DragValue when gaining focus via keyboard [#7107](https://github.com/emilk/egui/pull/7107) by [@Azkellas](https://github.com/Azkellas)
* Add `Context::current_pass_index` [#7276](https://github.com/emilk/egui/pull/7276) by [@emilk](https://github.com/emilk)
* Add `Context::cumulative_frame_nr` [#7278](https://github.com/emilk/egui/pull/7278) by [@emilk](https://github.com/emilk)
* Add `Visuals::text_edit_bg_color` [#7283](https://github.com/emilk/egui/pull/7283) by [@emilk](https://github.com/emilk)
* Add `Visuals::weak_text_alpha` and `weak_text_color` [#7285](https://github.com/emilk/egui/pull/7285) by [@emilk](https://github.com/emilk)
* Add support for scrolling via accesskit / kittest [#7286](https://github.com/emilk/egui/pull/7286) by [@lucasmerlin](https://github.com/lucasmerlin)
* Update area struct to allow force resizing [#7114](https://github.com/emilk/egui/pull/7114) by [@blackberryfloat](https://github.com/blackberryfloat)
* Add `egui::Sides` `shrink_left` / `shrink_right` [#7295](https://github.com/emilk/egui/pull/7295) by [@lucasmerlin](https://github.com/lucasmerlin)
* Set intrinsic size for Label [#7328](https://github.com/emilk/egui/pull/7328) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🔧 Changed
* Raise MSRV to 1.85 [#6848](https://github.com/emilk/egui/pull/6848) by [@torokati44](https://github.com/torokati44), [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Set `hint_text` in `WidgetInfo` [#5724](https://github.com/emilk/egui/pull/5724) by [@bircni](https://github.com/bircni)
* Implement `Default` for `ThemePreference` [#5702](https://github.com/emilk/egui/pull/5702) by [@MichaelGrupp](https://github.com/MichaelGrupp)
* Align `available_rect` docs with the new reality after #4590 [#5701](https://github.com/emilk/egui/pull/5701) by [@podusowski](https://github.com/podusowski)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Simplify the text cursor API [#5785](https://github.com/emilk/egui/pull/5785) by [@valadaptive](https://github.com/valadaptive)
* Bump accesskit to 0.19 [#7040](https://github.com/emilk/egui/pull/7040) by [@valadaptive](https://github.com/valadaptive)
* Better define the meaning of `SizeHint` [#7079](https://github.com/emilk/egui/pull/7079) by [@emilk](https://github.com/emilk)
* Move all input-related options into `InputOptions` [#7121](https://github.com/emilk/egui/pull/7121) by [@emilk](https://github.com/emilk)
* `Button` inherits the `alt_text` of the `Image` in it, if any [#7136](https://github.com/emilk/egui/pull/7136) by [@emilk](https://github.com/emilk)
* Change API of `Tooltip` slightly [#7151](https://github.com/emilk/egui/pull/7151) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Change `ui.disable()` to modify opacity [#7282](https://github.com/emilk/egui/pull/7282) by [@emilk](https://github.com/emilk)
* Make the font atlas use a color image [#7298](https://github.com/emilk/egui/pull/7298) by [@valadaptive](https://github.com/valadaptive)
* Implement `BitOr` and `BitOrAssign` for `Rect` [#7319](https://github.com/emilk/egui/pull/7319) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🔥 Removed
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Remove `SelectableLabel` [#7277](https://github.com/emilk/egui/pull/7277) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🐛 Fixed
* `Scene`: make `scene_rect` full size on reset [#5801](https://github.com/emilk/egui/pull/5801) by [@graydenshand](https://github.com/graydenshand)
* `Scene`: `TextEdit` selection when placed in a `Scene` [#5791](https://github.com/emilk/egui/pull/5791) by [@karhu](https://github.com/karhu)
* `Scene`: Set transform layer before calling user content [#5884](https://github.com/emilk/egui/pull/5884) by [@mitchmindtree](https://github.com/mitchmindtree)
* Fix: transform `TextShape` underline width [#5865](https://github.com/emilk/egui/pull/5865) by [@emilk](https://github.com/emilk)
* Fix missing repaint after `consume_key` [#7134](https://github.com/emilk/egui/pull/7134) by [@lucasmerlin](https://github.com/lucasmerlin)
* Update `emoji-icon-font` with fix for fullwidth latin characters [#7067](https://github.com/emilk/egui/pull/7067) by [@emilk](https://github.com/emilk)
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix scroll handle extending outside of `ScrollArea` [#5286](https://github.com/emilk/egui/pull/5286) by [@gilbertoalexsantos](https://github.com/gilbertoalexsantos)
* Fix `Response::clicked_elsewhere` not returning `true` sometimes [#5798](https://github.com/emilk/egui/pull/5798) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix kinetic scrolling on touch devices [#5778](https://github.com/emilk/egui/pull/5778) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix `DragValue` expansion when editing [#5809](https://github.com/emilk/egui/pull/5809) by [@MStarha](https://github.com/MStarha)
* Fix disabled `DragValue` eating focus, causing focus to reset [#5826](https://github.com/emilk/egui/pull/5826) by [@KonaeAkira](https://github.com/KonaeAkira)
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Improve drag-to-select text (add margins) [#5797](https://github.com/emilk/egui/pull/5797) by [@hankjordan](https://github.com/hankjordan)
* Fix bug in pointer movement detection [#5329](https://github.com/emilk/egui/pull/5329) by [@rustbasic](https://github.com/rustbasic)
* Protect against NaN in hit-test code [#6851](https://github.com/emilk/egui/pull/6851) by [@Skgland](https://github.com/Skgland)
* Fix image button panicking with tiny `available_space` [#6900](https://github.com/emilk/egui/pull/6900) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix links and text selection in horizontal_wrapped layout [#6905](https://github.com/emilk/egui/pull/6905) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix `leading_space` sometimes being ignored during paragraph splitting [#7031](https://github.com/emilk/egui/pull/7031) by [@afishhh](https://github.com/afishhh)
* Fix typo in deprecation message for `ComboBox::from_id_source` [#7055](https://github.com/emilk/egui/pull/7055) by [@aelmizeb](https://github.com/aelmizeb)
* Bug fix: make sure `end_pass` is called for all loaders [#7072](https://github.com/emilk/egui/pull/7072) by [@emilk](https://github.com/emilk)
* Report image alt text as text if widget contains no other text [#7142](https://github.com/emilk/egui/pull/7142) by [@lucasmerlin](https://github.com/lucasmerlin)
* Slider: move by at least the next increment when using fixed_decimals [#7066](https://github.com/emilk/egui/pull/7066) by [@0x53A](https://github.com/0x53A)
* Fix crash when using infinite widgets [#7296](https://github.com/emilk/egui/pull/7296) by [@emilk](https://github.com/emilk)
* Fix `debug_assert` triggered by `menu`/`intersect_ray` [#7299](https://github.com/emilk/egui/pull/7299) by [@emilk](https://github.com/emilk)
* Change `Rect::area` to return zero for negative rectangles [#7305](https://github.com/emilk/egui/pull/7305) by [@emilk](https://github.com/emilk)
### 🚀 Performance
* Optimize editing long text by caching each paragraph [#5411](https://github.com/emilk/egui/pull/5411) by [@afishhh](https://github.com/afishhh)
* Make `WidgetText` smaller and faster [#6903](https://github.com/emilk/egui/pull/6903) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.31.1 - 2025-03-05
* Fix sizing bug in `TextEdit::singleline` [#5640](https://github.com/emilk/egui/pull/5640) by [@IaVashik](https://github.com/IaVashik)
* Fix panic when rendering thin textured rectangles [#5692](https://github.com/emilk/egui/pull/5692) by [@PPakalns](https://github.com/PPakalns)

View File

@@ -36,7 +36,7 @@ There are snapshots test that might need to be updated.
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently
rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from
the last CI run of your PR (which will download the `test_results` artefact).
the last CI run of your PR (which will download the `test_results` artifact).
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.

View File

@@ -1197,7 +1197,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
[[package]]
name = "ecolor"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"bytemuck",
"cint",
@@ -1209,7 +1209,7 @@ dependencies = [
[[package]]
name = "eframe"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"ahash",
"bytemuck",
@@ -1249,7 +1249,7 @@ dependencies = [
[[package]]
name = "egui"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"accesskit",
"ahash",
@@ -1269,7 +1269,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"ahash",
"bytemuck",
@@ -1287,7 +1287,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"accesskit_winit",
"ahash",
@@ -1308,7 +1308,7 @@ dependencies = [
[[package]]
name = "egui_demo_app"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"bytemuck",
"chrono",
@@ -1336,7 +1336,7 @@ dependencies = [
[[package]]
name = "egui_demo_lib"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"chrono",
"criterion",
@@ -1353,7 +1353,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"ahash",
"chrono",
@@ -1372,7 +1372,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"ahash",
"bytemuck",
@@ -1392,7 +1392,7 @@ dependencies = [
[[package]]
name = "egui_kittest"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"dify",
"document-features",
@@ -1408,7 +1408,7 @@ dependencies = [
[[package]]
name = "egui_tests"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"egui",
"egui_extras",
@@ -1438,7 +1438,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "emath"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"bytemuck",
"document-features",
@@ -1535,7 +1535,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"ab_glyph",
"ahash",
@@ -1558,7 +1558,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.31.1"
version = "0.32.0"
[[package]]
name = "equivalent"
@@ -2421,8 +2421,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kittest"
version = "0.1.0"
source = "git+https://github.com/rerun-io/kittest?branch=main#679f9ade828021295c5f86f38275d9271d001004"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c1bfc4cb16136b6f00fb85a281e4b53d026401cf5dff9a427c466bde5891f0b"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -3235,7 +3236,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "popups"
version = "0.31.1"
version = "0.32.0"
dependencies = [
"eframe",
"env_logger",
@@ -5480,7 +5481,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xtask"
version = "0.31.1"
version = "0.32.0"
[[package]]
name = "yaml-rust"

View File

@@ -21,10 +21,10 @@ members = [
]
[workspace.package]
edition = "2021"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.84"
version = "0.31.1"
rust-version = "1.85"
version = "0.32.0"
[profile.release]
@@ -55,18 +55,18 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.31.1", path = "crates/emath", default-features = false }
ecolor = { version = "0.31.1", path = "crates/ecolor", default-features = false }
epaint = { version = "0.31.1", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.31.1", path = "crates/epaint_default_fonts" }
egui = { version = "0.31.1", path = "crates/egui", default-features = false }
egui-winit = { version = "0.31.1", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.31.1", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.31.1", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.31.1", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.31.1", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.31.1", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.31.1", path = "crates/eframe", default-features = false }
emath = { version = "0.32.0", path = "crates/emath", default-features = false }
ecolor = { version = "0.32.0", path = "crates/ecolor", default-features = false }
epaint = { version = "0.32.0", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.32.0", path = "crates/epaint_default_fonts" }
egui = { version = "0.32.0", path = "crates/egui", default-features = false }
egui-winit = { version = "0.32.0", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.32.0", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.32.0", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.32.0", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.32.0", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.32.0", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.32.0", path = "crates/eframe", default-features = false }
accesskit = "0.19.0"
accesskit_winit = "0.27"
@@ -85,7 +85,7 @@ glutin = { version = "0.32.0", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
home = "0.5.9"
image = { version = "0.25", default-features = false }
kittest = { version = "0.1.0", git = "https://github.com/rerun-io/kittest", branch = "main" }
kittest = { version = "0.2.0" }
log = { version = "0.4", features = ["std"] }
mimalloc = "0.1.46"
nohash-hasher = "0.2"
@@ -171,7 +171,6 @@ fn_params_excessive_bools = "warn"
fn_to_numeric_cast_any = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
@@ -193,6 +192,7 @@ large_stack_frames = "warn"
large_types_passed_by_value = "warn"
let_unit_value = "warn"
linkedlist = "warn"
literal_string_with_formatting_args = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"

View File

@@ -22,8 +22,11 @@ We don't update the MSRV in a patch release, unless we really, really need to.
# Release process
## Patch release
* [ ] Make a branch off of the latest release
* [ ] copy this checklist to a new egui issue, called "Release 0.xx.y"
* [ ] close all issues in the milestone for this release
## Special steps for patch release
* [ ] make a branch off of the _latest_ release
* [ ] cherry-pick what you want to release
* [ ] run `cargo semver-checks`
@@ -46,43 +49,30 @@ We don't update the MSRV in a patch release, unless we really, really need to.
## Preparation
* [ ] make sure there are no important unmerged PRs
* [ ] Create a branch called `release-0.xx.0` and open a PR for it
* [ ] run `scripts/generate_example_screenshots.sh` if needed
* [ ] write a short release note that fits in a bluesky post
* [ ] record gif for `CHANGELOG.md` release note (and later bluesky post)
* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write`
* [ ] bump version numbers in workspace `Cargo.toml`
* [ ] update changelogs
* [ ] run `scripts/generate_changelog.py --version 0.x.0 --write`
* [ ] read changelogs and clean them up if needed
* [ ] write a good intro with highlight for the main changelog
* [ ] run `typos`
## Actual release
I usually do this all on the `main` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `main` later.
* [ ] Run `typos`
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
* [ ] `cargo publish` (see below)
* [ ] bump version numbers in workspace `Cargo.toml`
* [ ] check that CI for the PR is green
* [ ] publish the crates by running `scripts/publish_crates.sh`
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
* [ ] merge release PR or push to `main`
* [ ] check that CI is green
* [ ] merge release PR as `Release 0.x.0 - <release title>`
* [ ] check that CI for `main` is green
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
* Follow the format of the last release
* [ ] wait for documentation to build: https://docs.rs/releases/queue
* follow the format of the last release
* [ ] wait for the documentation build to finish: https://docs.rs/releases/queue
* [ ] https://docs.rs/egui/ works
* [ ] https://docs.rs/eframe/ works
### `cargo publish`:
```
(cd crates/emath && cargo publish --quiet) && echo "✅ emath"
(cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor"
(cd crates/epaint_default_fonts && cargo publish --quiet) && echo "✅ epaint_default_fonts"
(cd crates/epaint && cargo publish --quiet) && echo "✅ epaint"
(cd crates/egui && cargo publish --quiet) && echo "✅ egui"
(cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit"
(cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu"
(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe"
(cd crates/egui_kittest && cargo publish --quiet) && echo "✅ egui_kittest"
(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras"
(cd crates/egui_demo_lib && cargo publish --quiet) && echo "✅ egui_demo_lib"
(cd crates/egui_glow && cargo publish --quiet) && echo "✅ egui_glow"
```
\<continue with the checklist above\>
## Announcements
* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social)
@@ -91,10 +81,17 @@ I usually do this all on the `main` branch, but doing it in a release branch is
* [ ] [r/programming](https://www.reddit.com/r/programming/comments/1bocsf6/announcing_egui_027_an_easytouse_crossplatform/)
* [ ] [This Week in Rust](https://github.com/rust-lang/this-week-in-rust/pull/5167)
## After release
* [ ] publish new `eframe_template`
* [ ] update `eframe_template`
* [ ] publish new `egui_plot`
* [ ] publish new `egui_table`
* [ ] publish new `egui_tiles`
* [ ] make a PR to `egui_commonmark`
* [ ] make a PR to `rerun`
## Finally
* [ ] close the milestone
* [ ] close this issue
* [ ] improve `RELEASES.md` with what you learned this time around

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.84"
msrv = "1.85"
allow-unwrap-in-tests = true

View File

@@ -6,6 +6,12 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Make `Hsva` derive serde [#7132](https://github.com/emilk/egui/pull/7132) by [@bircni](https://github.com/bircni)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -1,4 +1,4 @@
use super::{linear_f32_from_linear_u8, linear_u8_from_linear_f32, Color32, Hsva, HsvaGamma, Rgba};
use super::{Color32, Hsva, HsvaGamma, Rgba, linear_f32_from_linear_u8, linear_u8_from_linear_f32};
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----

View File

@@ -1,4 +1,4 @@
use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
/// This format is used for space-efficient color representation (32 bits).
///

View File

@@ -1,5 +1,5 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba,
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].

View File

@@ -1,4 +1,4 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
use crate::{Color32, Hsva, Rgba, gamma_from_linear, linear_from_gamma};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.

View File

@@ -7,6 +7,33 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
### ⭐ Added
* Add pointer events and focus handling for apps run in a Shadow DOM [#5627](https://github.com/emilk/egui/pull/5627) by [@xxvvii](https://github.com/xxvvii)
* MacOS: Add `movable_by_window_background` option to viewport [#5412](https://github.com/emilk/egui/pull/5412) by [@jim-ec](https://github.com/jim-ec)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Add external eventloop support [#6750](https://github.com/emilk/egui/pull/6750) by [@wpbrown](https://github.com/wpbrown)
### 🔧 Changed
* Update MSRV to 1.85 [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Rename `should_propagate_event` and add `should_prevent_default` [#5779](https://github.com/emilk/egui/pull/5779) by [@th0rex](https://github.com/th0rex)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Enhance stability on Windows [#5723](https://github.com/emilk/egui/pull/5723) by [@rustbasic](https://github.com/rustbasic)
* Set `web-sys` min version to `0.3.73` [#5862](https://github.com/emilk/egui/pull/5862) by [@wareya](https://github.com/wareya)
* Bump `ron` to `0.10.1` [#6861](https://github.com/emilk/egui/pull/6861) by [@torokati44](https://github.com/torokati44)
* Disallow `accesskit` on Android NativeActivity, making `hello_android` working again [#6855](https://github.com/emilk/egui/pull/6855) by [@podusowski](https://github.com/podusowski)
* Respect and detect `prefers-color-scheme: no-preference` [#7293](https://github.com/emilk/egui/pull/7293) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Mark all keys as up if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Fix text distortion on mobile devices/browsers with `glow` backend [#6893](https://github.com/emilk/egui/pull/6893) by [@wareya](https://github.com/wareya)
* Workaround libpng crash on macOS by not creating `NSImage` from png data [#7252](https://github.com/emilk/egui/pull/7252) by [@Wumpf](https://github.com/Wumpf)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
* Web: only consume copy/cut events if the canvas has focus [#7270](https://github.com/emilk/egui/pull/7270) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -24,7 +24,7 @@ To use on Linux, first run:
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.

View File

@@ -574,7 +574,9 @@ impl Default for Renderer {
fn default() -> Self {
#[cfg(not(feature = "glow"))]
#[cfg(not(feature = "wgpu"))]
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
compile_error!(
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
);
#[cfg(feature = "glow")]
#[cfg(not(feature = "wgpu"))]
@@ -617,7 +619,9 @@ impl std::str::FromStr for Renderer {
#[cfg(feature = "wgpu")]
"wgpu" => Ok(Self::Wgpu),
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
_ => Err(format!(
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
)),
}
}
}

View File

@@ -205,13 +205,18 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};
use objc2_foundation::NSString;
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
Ok(png_bytes) => Some(png_bytes),
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
//
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
// If this dylib isn't the right version, the application will crash with SIGBUS.
// For details see https://github.com/emilk/egui/issues/7155
let image = if let Some(icon_data) = icon_data {
match icon_data.to_image() {
Ok(image) => Some(image),
Err(err) => {
log::warn!("Failed to convert IconData to png: {err}");
log::warn!("Failed to read icon data: {err}");
return AppIconStatus::NotSetIgnored;
}
}
@@ -220,7 +225,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
};
// TODO(madsmtm): Move this into `objc2-app-kit`
extern "C" {
unsafe extern "C" {
static NSApp: Option<&'static NSApplication>;
}
@@ -231,15 +236,41 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
return AppIconStatus::NotSetIgnored;
};
if let Some(png_bytes) = png_bytes {
let data = NSData::from_vec(png_bytes);
if let Some(image) = image {
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
use objc2_foundation::NSSize;
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
log::trace!(
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
);
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
image.width() as isize,
image.height() as isize,
8, // bits per sample
4, // samples per pixel
true, // has alpha
false, // is not planar
NSDeviceRGBColorSpace,
(image.width() * 4) as isize, // bytes per row
32 // bits per pixel
) else {
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
return AppIconStatus::NotSetIgnored;
};
log::trace!("NSImage::initWithSize");
let app_icon = NSImage::initWithSize(
NSImage::alloc(),
NSSize::new(image.width() as f64, image.height() as f64),
);
log::trace!("NSImage::addRepresentation");
app_icon.addRepresentation(&image_rep);
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
app.setApplicationIconImage(Some(&app_icon));
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.

View File

@@ -52,10 +52,10 @@ fn roaming_appdata() -> Option<PathBuf> {
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
extern "C" {
unsafe extern "C" {
fn wcslen(buf: *const u16) -> usize;
}
let mut path_raw = ptr::null_mut();
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
};
let path = if result == S_OK {
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
Some(PathBuf::from(OsString::from_wide(path_slice)))
} else {

View File

@@ -32,13 +32,13 @@ use egui::{
use egui_winit::accesskit_winit;
use crate::{
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
Result, Storage,
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
};
use super::{
epi_integration, event_loop_context,
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
// ----------------------------------------------------------------------------
@@ -959,7 +959,6 @@ impl GlutinWindowContext {
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
event_loop,
viewport_builder.clone(),
)));
@@ -1016,7 +1015,9 @@ impl GlutinWindowContext {
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
log::warn!(
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
);
log::debug!(
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
);
@@ -1113,7 +1114,6 @@ impl GlutinWindowContext {
log::debug!("Creating a window for viewport {viewport_id:?}");
let window_attributes = egui_winit::create_winit_window_attributes(
&self.egui_ctx,
event_loop,
viewport.builder.clone(),
);
if window_attributes.transparent()

View File

@@ -10,9 +10,8 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
epi,
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
Result,
};
// ----------------------------------------------------------------------------

View File

@@ -25,8 +25,8 @@ use egui_winit::accesskit_winit;
use winit_integration::UserEvent;
use crate::{
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};

View File

@@ -1,8 +1,8 @@
use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use crate::{App, epi};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint};
use super::{NeedRepaint, now_sec, text_agent::TextAgent, web_painter::WebPainter as _};
pub struct AppRunner {
#[allow(dead_code, clippy::allow_attributes)]

View File

@@ -1,11 +1,10 @@
use crate::web::string_from_js_value;
use super::{
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner,
DEBUG_RESIZE,
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
push_touches, text_from_keyboard_event, translate_key,
};
use web_sys::{Document, EventTarget, ShadowRoot};
@@ -311,13 +310,17 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if !text.is_empty() && runner.input.raw.focused {
if !text.is_empty() {
let egui_event = egui::Event::Paste(text);
should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
@@ -340,17 +343,19 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
})?;
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
event.stop_propagation();
@@ -362,17 +367,19 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
})?;
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
event.stop_propagation();
@@ -462,16 +469,19 @@ fn install_color_scheme_change_event(
runner_ref: &WebRunner,
window: &web_sys::Window,
) -> Result<(), JsValue> {
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|event, runner| {
let theme = theme_from_dark_mode(event.matches());
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
},
)?;
for theme in [egui::Theme::Dark, egui::Theme::Light] {
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|_event, runner| {
if let Some(theme) = super::system_theme() {
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
}
},
)?;
}
}
Ok(())

View File

@@ -1,4 +1,4 @@
use super::{canvas_content_rect, AppRunner};
use super::{AppRunner, canvas_content_rect};
pub fn pos_from_mouse_event(
canvas: &web_sys::HtmlCanvasElement,

View File

@@ -40,6 +40,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*;
use egui::Theme;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -113,24 +114,31 @@ pub fn native_pixels_per_point() -> f32 {
///
/// `None` means unknown.
pub fn system_theme() -> Option<egui::Theme> {
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
.ok()??
.matches();
Some(theme_from_dark_mode(dark_mode))
}
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
window.match_media("(prefers-color-scheme: dark)")
}
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
if dark_mode {
egui::Theme::Dark
let window = web_sys::window()?;
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
Some(Theme::Dark)
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
Some(Theme::Light)
} else {
egui::Theme::Light
None
}
}
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
Some(prefers_color_scheme(window, theme).ok()??.matches())
}
fn prefers_color_scheme(
window: &web_sys::Window,
theme: Theme,
) -> Result<Option<MediaQueryList>, JsValue> {
let theme = match theme {
Theme::Dark => "dark",
Theme::Light => "light",
};
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
}
/// Returns the canvas in client coordinates.
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
let bounding_rect = canvas.get_bounding_client_rect();

View File

@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
#[test]
fn test_shorten_file_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
assert_eq!(shorten_file_path(before), after);
}
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use super::web_painter::WebPainter;
use crate::WebOptions;
use egui::{Event, UserData, ViewportId};
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use egui_wgpu::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel};
use egui_wgpu::{RenderState, SurfaceErrorAction};
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@@ -279,13 +279,6 @@ impl WebPainter for WebPainterWgpu {
Some((output_frame, capture_buffer))
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
@@ -307,6 +300,16 @@ impl WebPainter for WebPainterWgpu {
frame.present();
}
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
Ok(())
}

View File

@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::*;
use crate::{epi, App};
use crate::{App, epi};
use super::{
AppRunner, PanicHandler,
events::{self, ResizeObserverContext},
text_agent::TextAgent,
AppRunner, PanicHandler,
};
/// This is how `eframe` runs your web application

View File

@@ -6,6 +6,12 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Update to wgpu 25 [#6744](https://github.com/emilk/egui/pull/6744) by [@torokati44](https://github.com/torokati44)
* Free textures after submitting queue instead of before with wgpu renderer on Web [#7291](https://github.com/emilk/egui/pull/7291) by [@Wumpf](https://github.com/Wumpf)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -1,6 +1,6 @@
use egui::{UserData, ViewportId};
use epaint::ColorImage;
use std::sync::{mpsc, Arc};
use std::sync::{Arc, mpsc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
@@ -196,7 +196,10 @@ impl CaptureState {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format);
log::error!(
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}",
format
);
return;
}
};

View File

@@ -97,9 +97,8 @@ fn vs_main(
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
@@ -115,9 +114,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.

View File

@@ -3,7 +3,7 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex};
use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
use wgpu::util::DeviceExt as _;
@@ -564,15 +564,6 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
profiling::scope!("font -> sRGBA");
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@@ -638,9 +629,9 @@ impl Renderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
})
};
let origin = wgpu::Origin3d::ZERO;
@@ -699,7 +690,7 @@ impl Renderer {
///
/// This enables the application to reference the texture inside an image ui element.
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
pub fn register_native_texture(
&mut self,
device: &wgpu::Device,
@@ -747,7 +738,7 @@ impl Renderer {
/// This allows applications to specify individual minification/magnification filters as well as
/// custom mipmap and tiling options.
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
@@ -909,7 +900,11 @@ impl Renderer {
);
let Some(mut index_buffer_staging) = index_buffer_staging else {
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
panic!(
"Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.index_buffer.buffer.size(),
self.index_buffer.capacity
);
};
let mut index_offset = 0;
@@ -948,7 +943,11 @@ impl Renderer {
);
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
panic!(
"Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.vertex_buffer.buffer.size(),
self.vertex_buffer.capacity
);
};
let mut vertex_offset = 0;

View File

@@ -1,8 +1,8 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::undocumented_unsafe_blocks)]
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
use crate::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel};
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
@@ -220,7 +220,9 @@ impl Painter {
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
} else {
log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
@@ -344,7 +346,9 @@ impl Painter {
height_in_pixels,
);
} else {
log::warn!("Ignoring window resize notification with no surface created via Painter::set_window()");
log::warn!(
"Ignoring window resize notification with no surface created via Painter::set_window()"
);
}
}

View File

@@ -5,6 +5,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -123,7 +123,9 @@ impl Clipboard {
return;
}
log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
log::error!(
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
);
_ = image;
}
}

View File

@@ -1566,8 +1566,7 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes =
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1575,28 +1574,10 @@ pub fn create_window(
pub fn create_winit_window_attributes(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
viewport_builder: ViewportBuilder,
) -> winit::window::WindowAttributes {
profiling::function_scope!();
// We set sizes and positions in egui:s own ui points, which depends on the egui
// zoom_factor and the native pixels per point, so we need to know that here.
// We don't know what monitor the window will appear on though, but
// we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`.
let native_pixels_per_point = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
.map_or_else(
|| {
log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0");
1.0
},
|m| m.scale_factor() as f32,
);
let zoom_factor = egui_ctx.zoom_factor();
let pixels_per_point = zoom_factor * native_pixels_per_point;
let ViewportBuilder {
title,
position,
@@ -1672,40 +1653,46 @@ pub fn create_winit_window_attributes(
})
.with_active(active.unwrap_or(true));
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
// zoom factor into account. We don't have a good way to get physical size here,
// and trying to do it anyway leads to weird bugs on Wayland, see:
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
// https://github.com/rust-windowing/winit/issues/4266
#[expect(
clippy::disallowed_types,
reason = "zoom factor is manually accounted for"
)]
#[cfg(not(target_os = "ios"))]
if let Some(size) = inner_size {
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
{
use winit::dpi::{LogicalPosition, LogicalSize};
let zoom_factor = egui_ctx.zoom_factor();
#[cfg(not(target_os = "ios"))]
if let Some(size) = min_inner_size {
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = inner_size {
window_attributes = window_attributes
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(size) = max_inner_size {
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = min_inner_size {
window_attributes = window_attributes
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(pos) = position {
window_attributes = window_attributes.with_position(PhysicalPosition::new(
pixels_per_point * pos.x,
pixels_per_point * pos.y,
));
if let Some(size) = max_inner_size {
window_attributes = window_attributes
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
if let Some(pos) = position {
window_attributes = window_attributes.with_position(LogicalPosition::new(
zoom_factor * pos.x,
zoom_factor * pos.y,
));
}
}
#[cfg(target_os = "ios")]
{
// Unused:
_ = egui_ctx;
_ = pixels_per_point;
_ = position;
_ = inner_size;

View File

@@ -1,6 +1,6 @@
use crate::{
emath::{remap_clamp, NumExt as _},
Id, IdMap, InputState,
emath::{NumExt as _, remap_clamp},
};
#[derive(Clone, Default)]

View File

@@ -81,7 +81,7 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let (intrinsic, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let size = self
.size
@@ -89,7 +89,7 @@ impl<'a> Atom<'a> {
SizedAtom {
size,
preferred_size: preferred.at_least(size),
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
grow: self.grow,
kind,
}

View File

@@ -82,22 +82,9 @@ impl<'a> AtomKind<'a> {
match self {
AtomKind::Text(text) => {
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
let desired_size = matches!(wrap_mode, TextWrapMode::Truncate).then(|| {
text.clone()
.into_galley(
ui,
Some(TextWrapMode::Extend),
available_size.x,
TextStyle::Button,
)
.desired_size()
});
let galley =
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
(
desired_size.unwrap_or_else(|| galley.desired_size()),
SizedAtomKind::Text(galley),
)
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);

View File

@@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> {
let mut desired_width = 0.0;
// Preferred width / height is the ideal size of the widget, e.g. the size where the
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
let mut preferred_width = 0.0;
let mut preferred_height = 0.0;
let mut intrinsic_width = 0.0;
let mut intrinsic_height = 0.0;
let mut height: f32 = 0.0;
@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
preferred_width += gap_space;
intrinsic_width += gap_space;
}
for (idx, item) in atoms.into_iter().enumerate() {
@@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.push(sized);
}
@@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.insert(index, sized);
}
@@ -256,7 +256,7 @@ impl<'a> AtomLayout<'a> {
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let intrinsic_size =
(Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size);
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size);
let (_, rect) = ui.allocate_space(frame_size, intrinsic_size);
let mut response = ui.interact(rect, id, sense);

View File

@@ -12,8 +12,8 @@ pub struct SizedAtom<'a> {
/// size.x + gap.
pub size: Vec2,
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_size: Vec2,
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2,
pub kind: SizedAtomKind<'a>,
}

View File

@@ -204,12 +204,17 @@ fn shorten_source_file_path(path: &std::path::Path) -> String {
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2,
Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
};
/// State of an [`Area`] that is persisted between frames.
@@ -121,6 +121,7 @@ pub struct Area {
new_pos: Option<Pos2>,
fade_in: bool,
layout: Layout,
sizing_pass: bool,
}
impl WidgetWithState for Area {
@@ -147,6 +148,7 @@ impl Area {
anchor: None,
fade_in: true,
layout: Layout::default(),
sizing_pass: false,
}
}
@@ -357,6 +359,27 @@ impl Area {
self.layout = layout;
self
}
/// While true, a sizing pass will be done. This means the area will be invisible
/// and the contents will be laid out to estimate the proper containing size of the area.
/// If false, there will be no change to the default area behavior. This is useful if the
/// area contents area dynamic and you need to need to make sure the area adjusts its size
/// accordingly.
///
/// This should only be set to true during the specific frames you want force a sizing pass.
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
/// visible.
///
/// # Arguments
/// - resize: If true, the area will be resized to fit its contents. False will keep the
/// default area resizing behavior.
///
/// Default: `false`.
#[inline]
pub fn sizing_pass(mut self, resize: bool) -> Self {
self.sizing_pass = resize;
self
}
}
pub(crate) struct Prepared {
@@ -410,6 +433,7 @@ impl Area {
constrain_rect,
fade_in,
layout,
sizing_pass: force_sizing_pass,
} = self;
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
@@ -425,6 +449,10 @@ impl Area {
interactable,
last_became_visible_at: None,
});
if force_sizing_pass {
sizing_pass = true;
state.size = None;
}
state.pivot = pivot;
state.interactable = interactable;
if let Some(new_pos) = new_pos {
@@ -679,7 +707,7 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
// same column
*current_column_bb = current_column_bb.union(rect);
*current_column_bb |= rect;
} else {
// new column
column_bbs.push(rect);

View File

@@ -1,5 +1,13 @@
#[expect(unused_imports)]
use crate::{Ui, UiBuilder};
use std::sync::atomic::AtomicBool;
/// A tag to mark a container as closable.
///
/// Usually set via [`UiBuilder::closable`].
///
/// [`Ui::close`] will find the closest parent [`ClosableTag`] and set its `close` field to `true`.
/// Use [`Ui::should_close`] to check if close has been called.
#[derive(Debug, Default)]
pub struct ClosableTag {
pub close: AtomicBool,

View File

@@ -1,9 +1,9 @@
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt as _, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
WidgetInfo, WidgetText, WidgetType,
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
emath, epaint, pos2, remap, remap_clamp, vec2,
};
use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind};

View File

@@ -1,9 +1,9 @@
use epaint::Shape;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
};
#[expect(unused_imports)] // Documentation
@@ -293,7 +293,7 @@ impl ComboBox {
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
}
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
@@ -315,7 +315,7 @@ fn combo_box_dyn<'c, R>(
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());

View File

@@ -1,8 +1,8 @@
//! Frame container
use crate::{
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
UiStackInfo,
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
layers::ShapeIdx,
};
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
@@ -143,7 +143,8 @@ pub struct Frame {
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(), 32,
std::mem::size_of::<Frame>(),
32,
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(

View File

@@ -1,9 +1,19 @@
//! Popup menus, context menus and menu bars.
//!
//! Show menus via
//! - [`Popup::menu`] and [`Popup::context_menu`]
//! - [`Ui::menu_button`], [`MenuButton`] and [`SubMenuButton`]
//! - [`MenuBar`]
//! - [`Response::context_menu`]
//!
//! See [`MenuBar`] for an example.
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{vec2, Align, RectAlign, Vec2};
use emath::{Align, RectAlign, Vec2, vec2};
use epaint::Stroke;
/// Apply a menu style to the [`Style`].
@@ -50,6 +60,7 @@ pub fn is_in_menu(ui: &Ui) -> bool {
false
}
/// Configuration and style for menus.
#[derive(Clone, Debug)]
pub struct MenuConfig {
/// Is this a menu bar?
@@ -120,8 +131,10 @@ impl MenuConfig {
}
}
/// Holds the state of the menu.
#[derive(Clone)]
pub struct MenuState {
/// The currently open sub menu in this menu.
pub open_item: Option<Id>,
last_visible_pass: u64,
}
@@ -163,13 +176,29 @@ impl MenuState {
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
///
/// ### Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::MenuBar::new().ui(ui, |ui| {
/// ui.menu_button("File", |ui| {
/// if ui.button("Quit").clicked() {
/// ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
/// }
/// });
/// });
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct Bar {
pub struct MenuBar {
config: MenuConfig,
style: StyleModifier,
}
impl Default for Bar {
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {
config: MenuConfig::default(),
@@ -178,7 +207,7 @@ impl Default for Bar {
}
}
impl Bar {
impl MenuBar {
pub fn new() -> Self {
Self::default()
}
@@ -234,8 +263,8 @@ impl Bar {
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
///
/// The only thing this does is search for the current menu config (if set via [`Bar`]).
/// If your menu button is not in a [`Bar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// The only thing this does is search for the current menu config (if set via [`MenuBar`]).
/// If your menu button is not in a [`MenuBar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// directly.
pub struct MenuButton<'a> {
pub button: Button<'a>,
@@ -341,6 +370,10 @@ impl<'a> SubMenuButton<'a> {
}
}
/// Show a submenu in a menu.
///
/// Useful if you want to make custom menu buttons.
/// Usually, just use [`MenuButton`] or [`SubMenuButton`] instead.
#[derive(Clone, Debug, Default)]
pub struct SubMenu {
config: Option<MenuConfig>,

View File

@@ -3,7 +3,7 @@
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area;
pub mod close_tag;
mod close_tag;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
@@ -21,6 +21,7 @@ pub(crate) mod window;
pub use {
area::{Area, AreaState},
close_tag::ClosableTag,
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,

View File

@@ -1,7 +1,8 @@
use emath::{Align2, Vec2};
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
///
@@ -80,13 +81,11 @@ impl Modal {
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
let is_top_modal = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
mem.top_modal_layer() == Some(area.layer())
});
let any_popup_open = crate::Popup::is_any_open(ctx);
let InnerResponse {
inner: (inner, backdrop_response),
response,

View File

@@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer<R>(
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
@@ -78,7 +78,7 @@ pub fn show_tooltip_for<R>(
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, *widget_rect)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
@@ -94,7 +94,7 @@ pub fn show_tooltip_at<R>(
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, suggested_position)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}

View File

@@ -18,8 +18,8 @@
use emath::GuiRounding as _;
use crate::{
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _,
Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef,
Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {

View File

@@ -1,11 +1,15 @@
use crate::containers::menu::{menu_style, MenuConfig, MenuState};
use crate::style::StyleModifier;
#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
use std::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
containers::menu::{MenuConfig, MenuState, menu_style},
style::StyleModifier,
};
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
@@ -64,9 +68,7 @@ impl PopupAnchor {
match self {
Self::ParentRect(rect) => Some(rect),
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
Self::PointerFixed => ctx
.memory(|mem| mem.popup_position(popup_id))
.map(Rect::from_pos),
Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
Self::Position(pos) => Some(Rect::from_pos(pos)),
}
}
@@ -122,12 +124,12 @@ enum OpenKind<'a> {
impl OpenKind<'_> {
/// Returns `true` if the popup should be open
fn is_open(&self, id: Id, ctx: &Context) -> bool {
fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
match self {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
}
}
}
@@ -160,6 +162,8 @@ impl From<PopupKind> for UiKind {
}
}
/// A popup container.
#[must_use = "Call `.show()` to actually display the popup"]
pub struct Popup<'a> {
id: Id,
ctx: Context,
@@ -210,6 +214,57 @@ impl<'a> Popup<'a> {
}
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
Self::default_response_id(response),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup relative to some widget,
/// toggling the open state based on the widget's click state.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_toggle_button_response(button_response: &Response) -> Self {
Self::from_response(button_response)
.open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(button_response: &Response) -> Self {
Self::from_toggle_button_response(button_response)
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
#[inline]
pub fn kind(mut self, kind: PopupKind) -> Self {
@@ -242,49 +297,6 @@ impl<'a> Popup<'a> {
self
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
response.id.with("popup"),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(response: &Response) -> Self {
Self::from_response(response)
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Force the popup to be open or closed.
#[inline]
pub fn open(mut self, open: bool) -> Self {
@@ -446,7 +458,7 @@ impl<'a> Popup<'a> {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
}
}
@@ -484,12 +496,43 @@ impl<'a> Popup<'a> {
self.gap,
expected_popup_size,
)
.unwrap_or_default()
}
/// Show the popup.
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
/// no pointer.
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
let hover_pos = self.ctx.pointer_hover_pos();
let id = self.id;
if let OpenKind::Memory { set } = self.open_kind {
match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
}
_ => Popup::open_id(&self.ctx, id),
}
} else {
Self::close_id(&self.ctx, id);
}
}
Some(SetOpenCommand::Toggle) => {
Self::toggle_id(&self.ctx, id);
}
None => {
self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
}
}
}
if !self.open_kind.is_open(self.id, &self.ctx) {
return None;
}
let best_align = self.get_best_align();
let Popup {
@@ -512,34 +555,6 @@ impl<'a> Popup<'a> {
style,
} = self;
let hover_pos = ctx.pointer_hover_pos();
if let OpenKind::Memory { set, .. } = open_kind {
ctx.memory_mut(|mem| match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
mem.open_popup_at(id, hover_pos);
}
_ => mem.open_popup(id),
}
} else {
mem.close_popup(id);
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
}
None => {
mem.keep_popup_open(id);
}
});
}
if !open_kind.is_open(id, &ctx) {
return None;
}
if kind != PopupKind::Tooltip {
ctx.pass_state_mut(|fs| {
fs.layers
@@ -573,10 +588,9 @@ impl<'a> Popup<'a> {
area = area.default_width(width);
}
let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));
let mut response = area.show(&ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner
});
@@ -616,3 +630,65 @@ impl<'a> Popup<'a> {
Some(response)
}
}
/// ## Static methods
impl Popup<'_> {
/// The default ID when constructing a popup from the [`Response`] of e.g. a button.
pub fn default_response_id(response: &Response) -> Id {
response.id.with("popup")
}
/// Is the given popup open?
///
/// This assumes the use of either:
/// * [`Self::open_memory`]
/// * [`Self::from_toggle_button_response`]
/// * [`Self::menu`]
/// * [`Self::context_menu`]
///
/// The popup id should be the same as either you set with [`Self::id`] or the
/// default one from [`Self::default_response_id`].
pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
ctx.memory(|mem| mem.is_popup_open(popup_id))
}
/// Is any popup open?
///
/// This assumes the egui memory is being used to track the open state of popups.
pub fn is_any_open(ctx: &Context) -> bool {
ctx.memory(|mem| mem.any_popup_open())
}
/// Open the given popup and close all others.
///
/// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id));
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
pub fn toggle_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
}
/// Close all currently open popups.
pub fn close_all(ctx: &Context) {
ctx.memory_mut(|mem| mem.close_all_popups());
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all`] if you want to close any / all currently open popups.
pub fn close_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.close_popup(popup_id));
}
/// Get the position for this popup, if it is open.
pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
ctx.memory(|mem| mem.popup_position(popup_id))
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense,
Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
};
#[derive(Clone, Copy, Debug)]

View File

@@ -3,8 +3,8 @@ use core::f32;
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui,
UiBuilder, Vec2,
InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.

View File

@@ -3,8 +3,8 @@
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id,
NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
};
#[derive(Clone, Copy, Debug)]

View File

@@ -1,4 +1,4 @@
use emath::{Align, Vec2};
use emath::{Align, NumExt as _, Vec2};
use crate::{Layout, Ui, UiBuilder};
@@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder};
///
/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right.
///
/// The left widgets are first added to the ui, left-to-right.
/// Then the right widgets are added, right-to-left.
/// The left widgets are added left-to-right.
/// The right widgets are added right-to-left.
///
/// Which side is first depends on the configuration:
/// - [`Sides::extend`] - left widgets are added first
/// - [`Sides::shrink_left`] - right widgets are added first
/// - [`Sides::shrink_right`] - left widgets are added first
///
/// ```
/// # egui::__run_test_ui(|ui| {
@@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder};
pub struct Sides {
height: Option<f32>,
spacing: Option<f32>,
kind: SidesKind,
wrap_mode: Option<crate::TextWrapMode>,
}
#[derive(Clone, Copy, Debug, Default)]
enum SidesKind {
#[default]
Extend,
ShrinkLeft,
ShrinkRight,
}
impl Sides {
@@ -68,63 +83,184 @@ impl Sides {
self
}
/// Try to shrink widgets on the left side.
///
/// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_left(mut self) -> Self {
self.kind = SidesKind::ShrinkLeft;
self
}
/// Try to shrink widgets on the right side.
///
/// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_right(mut self) -> Self {
self.kind = SidesKind::ShrinkRight;
self
}
/// Extend the left and right sides to fill the available space.
///
/// This is the default behavior.
/// The left widgets will be added first, followed by the right widgets.
#[inline]
pub fn extend(mut self) -> Self {
self.kind = SidesKind::Extend;
self
}
/// The text wrap mode for the shrinking side.
///
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Truncate the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn truncate(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Truncate);
self
}
/// Wrap the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Wrap);
self
}
pub fn show<RetL, RetR>(
self,
ui: &mut Ui,
add_left: impl FnOnce(&mut Ui) -> RetL,
add_right: impl FnOnce(&mut Ui) -> RetR,
) -> (RetL, RetR) {
let Self { height, spacing } = self;
let Self {
height,
spacing,
mut kind,
mut wrap_mode,
} = self;
let height = height.unwrap_or_else(|| ui.spacing().interact_size.y);
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x);
let mut top_rect = ui.available_rect_before_wrap();
top_rect.max.y = top_rect.min.y + height;
let result_left;
let result_right;
let (left_rect, left_preferred_size) = {
let left_max_rect = top_rect;
let mut left_ui = ui.new_child(
UiBuilder::new()
.max_rect(left_max_rect)
.layout(Layout::left_to_right(Align::Center)),
);
result_left = add_left(&mut left_ui);
(left_ui.min_rect(), left_ui.intrinsic_size())
};
let (right_rect, right_preferred_size) = {
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let mut right_ui = ui.new_child(
UiBuilder::new()
.max_rect(right_max_rect)
.layout(Layout::right_to_left(Align::Center)),
);
result_right = add_right(&mut right_ui);
(right_ui.min_rect(), right_ui.intrinsic_size())
};
let mut final_rect = left_rect.union(right_rect);
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
// Make as small as possible:
final_rect.max.x = left_rect.min.x + min_width;
} else {
// If the rects overlap, make sure we expand the allocated rect so that the parent
// ui knows we overflowed, and resizes:
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
kind = SidesKind::Extend;
wrap_mode = None;
}
let preferred_size = Vec2::new(
left_preferred_size.x + spacing + right_preferred_size.x,
left_preferred_size.y.max(right_preferred_size.y),
);
let intrinsic_size =
|left: Vec2, right: Vec2| Vec2::new(left.x + spacing + right.x, left.y.max(right.y));
ui.advance_cursor_after_rect(final_rect, preferred_size);
match kind {
SidesKind::ShrinkLeft => {
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
top_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let available_width = top_rect.width() - right_rect.width() - spacing;
let left_rect_constraint =
top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0));
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
left_rect_constraint,
Layout::left_to_right(Align::Center),
add_left,
wrap_mode,
);
(result_left, result_right)
let intrinsic = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(left_rect | right_rect, intrinsic);
(result_left, result_right)
}
SidesKind::ShrinkRight => {
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing);
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
right_rect_constraint,
Layout::right_to_left(Align::Center),
add_right,
wrap_mode,
);
let intrinsic_size = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(left_rect | right_rect, intrinsic_size);
(result_left, result_right)
}
SidesKind::Extend => {
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
right_max_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let mut final_rect = left_rect | right_rect;
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
final_rect.max.x = left_rect.min.x + min_width;
} else {
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
}
let intrinsic = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(final_rect, intrinsic);
(result_left, result_right)
}
}
}
fn create_ui<Ret>(
ui: &mut Ui,
max_rect: emath::Rect,
layout: Layout,
add_content: impl FnOnce(&mut Ui) -> Ret,
wrap_mode: Option<crate::TextWrapMode>,
) -> (emath::Rect, emath::Vec2, Ret) {
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout));
if let Some(wrap_mode) = wrap_mode {
child_ui.style_mut().wrap_mode = Some(wrap_mode);
}
let result = add_content(&mut child_ui);
(child_ui.min_rect(), child_ui.intrinsic_size(), result)
}
}

View File

@@ -17,7 +17,25 @@ pub struct Tooltip<'a> {
impl Tooltip<'_> {
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
parent_layer: LayerId,
) -> Self {
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,
parent_layer: LayerId,
parent_widget: Id,
@@ -145,7 +163,7 @@ impl Tooltip<'_> {
// The popup might not be shown on at_pointer if there is no pointer.
if let Some(response) = &response {
state.tooltip_count += 1;
state.bounding_rect = state.bounding_rect.union(response.response.rect);
state.bounding_rect |= response.response.rect;
response
.response
.ctx

View File

@@ -9,7 +9,7 @@ use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::{area, resize, Area, Frame, Resize, ScrollArea};
use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
///

View File

@@ -4,16 +4,23 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration
use emath::{GuiRounding as _, OrderedFloat};
use epaint::{
ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
vec2,
};
use crate::{
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
animation_manager::AnimationManager,
containers::{self, area::AreaState},
data::output::PlatformOutput,
@@ -29,12 +36,6 @@ use crate::{
resize, response, scroll_area,
util::IdTypeMap,
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
};
#[cfg(feature = "accesskit")]
@@ -77,7 +78,7 @@ impl Default for WrappedTextureManager {
// Will be filled in later
let font_id = tex_mngr.alloc(
"egui_font_texture".into(),
epaint::FontImage::new([0, 0]).into(),
epaint::ColorImage::filled([0, 0], Color32::TRANSPARENT).into(),
Default::default(),
);
assert_eq!(
@@ -339,6 +340,15 @@ impl RepaintCause {
/// Per-viewport state related to repaint scheduling.
struct ViewportRepaintInfo {
/// Monotonically increasing counter.
///
/// Incremented at the end of [`Context::run`].
/// This can be smaller than [`Self::cumulative_pass_nr`],
/// but never larger.
cumulative_frame_nr: u64,
/// Monotonically increasing counter, counting the number of passes.
/// This can be larger than [`Self::cumulative_frame_nr`],
/// but never smaller.
cumulative_pass_nr: u64,
/// The duration which the backend will poll for new events
@@ -369,6 +379,7 @@ struct ViewportRepaintInfo {
impl Default for ViewportRepaintInfo {
fn default() -> Self {
Self {
cumulative_frame_nr: 0,
cumulative_pass_nr: 0,
// We haven't scheduled a repaint yet.
@@ -599,6 +610,8 @@ impl ContextImpl {
log::trace!("Adding new fonts");
}
let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage;
let mut is_new = false;
let fonts = self
@@ -613,13 +626,14 @@ impl ContextImpl {
Fonts::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
});
{
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side);
fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage);
}
if is_new && self.memory.options.preload_font_glyphs {
@@ -850,7 +864,10 @@ impl Context {
if max_passes <= output.platform_output.num_completed_passes {
#[cfg(feature = "log")]
log::debug!("Ignoring call request_discard, because max_passes={max_passes}. Requested from {:?}", output.platform_output.request_discard_reasons);
log::debug!(
"Ignoring call request_discard, because max_passes={max_passes}. Requested from {:?}",
output.platform_output.request_discard_reasons
);
break;
}
@@ -864,6 +881,7 @@ impl Context {
} else {
viewport.num_multipass_in_row = 0;
}
viewport.repaint.cumulative_frame_nr += 1;
});
output
@@ -1134,7 +1152,7 @@ impl Context {
ID clashes happens when things like Windows or CollapsingHeaders share names,\n\
or when things like Plot and Grid:s aren't given unique id_salt:s.\n\n\
Sometimes the solution is to use ui.push_id.",
if below { "above" } else { "below" })
if below { "above" } else { "below" }),
);
}
}
@@ -1201,6 +1219,51 @@ impl Context {
self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder));
}
#[cfg(feature = "accesskit")]
self.write(|ctx| {
use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation};
let viewport = ctx.viewport_for(ctx.viewport_id());
viewport
.input
.consume_accesskit_action_requests(res.id, |request| {
// TODO(lucasmerlin): Correctly handle the scroll unit:
// https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621
const DISTANCE: f32 = 100.0;
match &request.action {
accesskit::Action::ScrollIntoView => {
viewport.this_pass.scroll_target = [
Some(ScrollTarget::new(
res.rect.x_range(),
Some(Align::Center),
ScrollAnimation::none(),
)),
Some(ScrollTarget::new(
res.rect.y_range(),
Some(Align::Center),
ScrollAnimation::none(),
)),
];
}
accesskit::Action::ScrollDown => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP;
}
accesskit::Action::ScrollUp => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN;
}
accesskit::Action::ScrollLeft => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT;
}
accesskit::Action::ScrollRight => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT;
}
_ => return false,
};
true
});
});
res
}
@@ -1536,9 +1599,34 @@ impl Context {
}
}
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr`].
pub fn cumulative_frame_nr(&self) -> u64 {
self.cumulative_frame_nr_for(self.viewport_id())
}
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr_for`].
pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 {
self.read(|ctx| {
ctx.viewports
.get(&id)
.map_or(0, |v| v.repaint.cumulative_frame_nr)
})
}
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
///
/// If you instead want to know which pass index this is within the current frame,
/// use [`Self::current_pass_index`].
pub fn cumulative_pass_nr(&self) -> u64 {
self.cumulative_pass_nr_for(self.viewport_id())
}
@@ -1554,6 +1642,18 @@ impl Context {
})
}
/// The index of the current pass in the current frame, starting at zero.
///
/// Usually this is zero, but if something called [`Self::request_discard`] to do multi-pass layout,
/// then this will be incremented for each pass.
///
/// This just reads the value of [`PlatformOutput::num_completed_passes`].
///
/// To know the total number of passes ever completed, use [`Self::cumulative_pass_nr`].
pub fn current_pass_index(&self) -> usize {
self.output(|o| o.num_completed_passes)
}
/// Call this if there is need to repaint the UI, i.e. if you are showing an animation.
///
/// If this is called at least once in a frame, then there will be another frame right after this.
@@ -1726,7 +1826,7 @@ impl Context {
/// This means the first pass will look glitchy, and ideally should not be shown to the user.
/// So [`crate::Grid`] calls [`Self::request_discard`] to cover up this glitches.
///
/// There is a limit to how many passes egui will perform, set by [`Options::max_passes`].
/// There is a limit to how many passes egui will perform, set by [`Options::max_passes`] (default=2).
/// Therefore, the request might be declined.
///
/// You can check if the current pass will be discarded with [`Self::will_discard`].
@@ -2305,7 +2405,9 @@ impl Context {
// If you see this message, it means we've been paying the cost of multi-pass for multiple frames in a row.
// This is likely a bug. `request_discard` should only be called in rare situations, when some layout changes.
let mut warning = format!("egui PERF WARNING: request_discard has been called {num_multipass_in_row} frames in a row");
let mut warning = format!(
"egui PERF WARNING: request_discard has been called {num_multipass_in_row} frames in a row"
);
self.viewport(|vp| {
for reason in &vp.output.request_discard_reasons {
warning += &format!("\n {reason}");
@@ -2587,7 +2689,7 @@ impl Context {
self.write(|ctx| {
let mut used = ctx.viewport().this_pass.used_by_panels;
for (_id, window) in ctx.memory.areas().visible_windows() {
used = used.union(window.rect());
used |= window.rect();
}
used.round_ui()
})
@@ -2983,36 +3085,54 @@ impl Context {
pub fn inspection_ui(&self, ui: &mut Ui) {
use crate::containers::CollapsingHeader;
ui.label(format!("Is using pointer: {}", self.is_using_pointer()))
.on_hover_text(
"Is egui currently using the pointer actively (e.g. dragging a slider)?",
);
ui.label(format!("Wants pointer input: {}", self.wants_pointer_input()))
.on_hover_text("Is egui currently interested in the location of the pointer (either because it is in use, or because it is hovering over a window).");
ui.label(format!(
"Wants keyboard input: {}",
self.wants_keyboard_input()
))
.on_hover_text("Is egui currently listening for text input?");
ui.label(format!(
"Keyboard focus widget: {}",
self.memory(|m| m.focused())
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default()
))
.on_hover_text("Is egui currently listening for text input?");
crate::Grid::new("egui-inspection-grid")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.label("Total ui frames:");
ui.monospace(ui.ctx().cumulative_frame_nr().to_string());
ui.end_row();
let pointer_pos = self
.pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label(format!("Pointer pos: {pointer_pos}"));
ui.label("Total ui passes:");
ui.monospace(ui.ctx().cumulative_pass_nr().to_string());
ui.end_row();
let top_layer = self
.pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format());
ui.label(format!("Top layer under mouse: {top_layer}"));
ui.label("Is using pointer")
.on_hover_text("Is egui currently using the pointer actively (e.g. dragging a slider)?");
ui.monospace(self.is_using_pointer().to_string());
ui.end_row();
ui.label("Wants pointer input")
.on_hover_text("Is egui currently interested in the location of the pointer (either because it is in use, or because it is hovering over a window).");
ui.monospace(self.wants_pointer_input().to_string());
ui.end_row();
ui.label("Wants keyboard input").on_hover_text("Is egui currently listening for text input?");
ui.monospace(self.wants_keyboard_input().to_string());
ui.end_row();
ui.label("Keyboard focus widget").on_hover_text("Is egui currently listening for text input?");
ui.monospace(self.memory(|m| m.focused())
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default());
ui.end_row();
let pointer_pos = self
.pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label("Pointer pos");
ui.monospace(pointer_pos);
ui.end_row();
let top_layer = self
.pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format());
ui.label("Top layer under mouse");
ui.monospace(top_layer);
ui.end_row();
});
ui.add_space(16.0);
@@ -3446,6 +3566,7 @@ impl Context {
/// Release all memory and textures related to the given image URI.
///
/// If you attempt to load the image again, it will be reloaded from scratch.
/// Also this cancels any ongoing loading of the image.
pub fn forget_image(&self, uri: &str) {
use load::BytesLoader as _;

View File

@@ -3,8 +3,8 @@
use epaint::ColorImage;
use crate::{
emath::{Pos2, Rect, Vec2},
Key, Theme, ViewportId, ViewportIdMap,
emath::{Pos2, Rect, Vec2},
};
/// What the integrations provides to egui at the start of each frame.

View File

@@ -6,7 +6,7 @@
//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]).
use crate::{
text, Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText,
Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText, text,
};
/// Register this plugin on the given egui context,
@@ -102,7 +102,7 @@ impl State {
let location_rect =
Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size());
painter.galley(location_rect.min, location_galley, color);
bounding_rect = bounding_rect.union(location_rect);
bounding_rect |= location_rect;
}
{
@@ -117,7 +117,7 @@ impl State {
);
let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size());
painter.galley(rect.min, galley, color);
bounding_rect = bounding_rect.union(rect);
bounding_rect |= rect;
}
pos.y = bounding_rect.max.y + 4.0;

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style,
Ui, UiBuilder, Vec2,
Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2, vec2,
};
#[cfg(debug_assertions)]

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, id::IdSet, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects};
use crate::{LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects, ahash, emath, id::IdSet};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -91,6 +91,8 @@ pub fn hit_test(
}
}
close.retain(|rect| !rect.interact_rect.any_nan()); // Protect against bad input and transforms
// When using layer transforms it is common to stack layers close to each other.
// For instance, you may have a resize-separator on a panel, with two
// transform-layers on either side.
@@ -466,7 +468,7 @@ fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool {
#[cfg(test)]
mod tests {
use emath::{pos2, vec2, Rect};
use emath::{Rect, pos2, vec2};
use crate::{Id, Sense};

View File

@@ -1,11 +1,11 @@
mod touch_state;
use crate::data::input::{
Event, EventFilter, KeyboardShortcut, Modifiers, MouseWheelUnit, PointerButton, RawInput,
TouchDeviceId, ViewportInfo, NUM_POINTER_BUTTONS,
Event, EventFilter, KeyboardShortcut, Modifiers, MouseWheelUnit, NUM_POINTER_BUTTONS,
PointerButton, RawInput, TouchDeviceId, ViewportInfo,
};
use crate::{
emath::{vec2, NumExt as _, Pos2, Rect, Vec2},
emath::{NumExt as _, Pos2, Rect, Vec2, vec2},
util::History,
};
use std::{
@@ -824,6 +824,23 @@ impl InputState {
})
}
#[cfg(feature = "accesskit")]
pub fn consume_accesskit_action_requests(
&mut self,
id: crate::Id,
mut consume: impl FnMut(&accesskit::ActionRequest) -> bool,
) {
let accesskit_id = id.accesskit_id();
self.events.retain(|event| {
if let Event::AccessKitActionRequest(request) = event {
if request.target == accesskit_id {
return !consume(request);
}
}
true
});
}
#[cfg(feature = "accesskit")]
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
self.accesskit_action_requests(id, action).next().is_some()
@@ -1448,7 +1465,10 @@ impl PointerState {
}
if let Some(pos) = self.hover_pos() {
return rect.intersects_ray(pos, self.direction());
let dir = self.direction();
if dir != Vec2::ZERO {
return rect.intersects_ray(pos, self.direction());
}
}
false
}

View File

@@ -1,9 +1,9 @@
use std::{collections::BTreeMap, fmt::Debug};
use crate::{
data::input::TouchDeviceId,
emath::{normalized_angle, Pos2, Vec2},
Event, RawInput, TouchId, TouchPhase,
data::input::TouchDeviceId,
emath::{Pos2, Vec2, normalized_angle},
};
/// All you probably need to know about a multi-touch gesture.
@@ -174,7 +174,7 @@ impl TouchState {
if added_or_removed_touches {
// Adding or removing fingers makes the average values "jump". We better forget
// about the previous values, and don't create delta information for this frame:
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
state.previous = None;
}
}
@@ -224,7 +224,7 @@ impl TouchState {
fn update_gesture(&mut self, time: f64, pointer_pos: Option<Pos2>) {
if let Some(dyn_state) = self.calc_dynamic_state() {
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
// updating an ongoing gesture
state.previous = Some(state.current);
state.current = dyn_state;

View File

@@ -1,6 +1,6 @@
//! How mouse and touch interzcts with widgets.
use crate::{hit_test, id, input_state, memory, Id, InputState, Key, WidgetRects};
use crate::{Id, InputState, Key, WidgetRects, hit_test, id, input_state, memory};
use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState};

View File

@@ -1,8 +1,8 @@
//! Showing UI:s for egui/epaint types.
use crate::{
epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh,
NumExt as _, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
Color32, CursorIcon, FontFamily, FontId, Label, Mesh, NumExt as _, Rect, Response, Sense,
Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, epaint, memory, pos2, remap_clamp, vec2,
};
use emath::Vec2;

View File

@@ -1,8 +1,8 @@
//! Handles paint layers, i.e. how things
//! are sometimes painted behind or in front of other things.
use crate::{ahash, epaint, Id, IdMap, Rect};
use epaint::{emath::TSTransform, ClippedShape, Shape};
use crate::{Id, IdMap, Rect, ahash, epaint};
use epaint::{ClippedShape, Shape, emath::TSTransform};
/// Different layer categories
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2},
Align,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -54,8 +54,8 @@ pub(crate) struct Region {
impl Region {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.min_rect = self.min_rect.union(rect);
self.max_rect = self.max_rect.union(rect);
self.min_rect |= rect;
self.max_rect |= rect;
}
/// Ensure we are big enough to contain the given X-coordinate.
@@ -738,7 +738,7 @@ impl Layout {
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary
*cursor = cursor.union(frame_rect);
*cursor |= frame_rect;
} else {
// this is a new row or column. We temporarily use NAN for what will be filled in later.
match self.main_dir {

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.84.0 or later to use `egui`.
//! You need to have rust 1.85.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
@@ -161,12 +161,10 @@
//!
//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
//! * egui prefers linear color spaces for all blending so:
//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
//! * Otherwise: remember to decode gamma in the fragment shader.
//! * Decode the gamma of the incoming vertex colors in your vertex shader.
//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
//! * Otherwise: gamma-encode the colors before you write them again.
//! * egui prefers gamma color spaces for all blending so:
//! * Do NOT use an sRGBA-aware texture (NOT `GL_SRGB8_ALPHA8`).
//! * Multiply texture and vertex colors in gamma space
//! * Turn OFF sRGBA/gamma framebuffer (NO `GL_FRAMEBUFFER_SRGB`).
//!
//!
//! # Understanding immediate mode
@@ -463,36 +461,35 @@ pub use epaint::emath;
pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign,
Vec2, Vec2b,
Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign, Vec2, Vec2b, lerp, pos2, remap,
remap_clamp, vec2,
};
pub use epaint::{
mutex,
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId,
};
pub mod text {
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE,
TextFormat, TextWrapping, cursor::CCursor,
};
}
pub use self::{
atomics::*,
containers::*,
containers::{menu::MenuBar, *},
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
Key, UserData,
input::*,
output::{
self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput,
UserAttentionType, WidgetInfo,
},
Key, UserData,
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,

View File

@@ -65,7 +65,7 @@ use std::{
use ahash::HashMap;
use emath::{Float as _, OrderedFloat};
use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2};
use epaint::{ColorImage, TextureHandle, TextureId, Vec2, mutex::Mutex, textures::TextureOptions};
use crate::Context;

View File

@@ -1,6 +1,6 @@
use super::{
generate_loader_id, Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap,
LoadError, Mutex,
Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap, LoadError, Mutex,
generate_loader_id,
};
/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`.

View File

@@ -6,8 +6,8 @@ use ahash::{HashMap, HashSet};
use epaint::emath::TSTransform;
use crate::{
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2,
ViewportId, ViewportIdMap, ViewportIdSet,
EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2, ViewportId,
ViewportIdMap, ViewportIdSet, area, vec2,
};
mod theme;
@@ -377,8 +377,8 @@ impl Options {
reduce_texture_memory,
} = self;
use crate::containers::CollapsingHeader;
use crate::Widget as _;
use crate::containers::CollapsingHeader;
CollapsingHeader::new("⚙ Options")
.default_open(false)
@@ -408,11 +408,11 @@ impl Options {
.show(ui, |ui| {
theme_preference.radio_buttons(ui);
std::sync::Arc::make_mut(match theme {
let style = std::sync::Arc::make_mut(match theme {
Theme::Dark => dark_style,
Theme::Light => light_style,
})
.ui(ui);
});
style.ui(ui);
});
CollapsingHeader::new("✒ Painting")
@@ -1012,11 +1012,11 @@ impl OpenPopup {
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be open at a time.
/// ## Deprecated popup API
/// Use [`crate::Popup`] instead.
impl Memory {
/// Is the given popup open?
#[deprecated = "Use Popup::is_id_open instead"]
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups
.get(&self.viewport_id)
@@ -1025,6 +1025,7 @@ impl Memory {
}
/// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
@@ -1032,6 +1033,7 @@ impl Memory {
/// Open the given popup and close all others.
///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
#[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
@@ -1042,6 +1044,7 @@ impl Memory {
/// This is needed because in some cases popups can go away without `close_popup` being
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
#[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if state.id == popup_id {
@@ -1051,12 +1054,14 @@ impl Memory {
}
/// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
self.popups
.get(&self.viewport_id)
@@ -1064,6 +1069,7 @@ impl Memory {
}
/// Close any currently open popup.
#[deprecated = "Use Popup::close_all instead"]
pub fn close_all_popups(&mut self) {
self.popups.clear();
}
@@ -1071,7 +1077,9 @@ impl Memory {
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
@@ -1080,14 +1088,18 @@ impl Memory {
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.close_popup(popup_id);
} else {
self.open_popup(popup_id);
}
}
}
impl Memory {
/// If true, all windows, menus, tooltips, etc., will be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
@@ -1250,8 +1262,11 @@ impl Areas {
///
/// The two layers must have the same [`LayerId::order`].
pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) {
debug_assert_eq!(parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order);
debug_assert_eq!(
parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui",
parent.order, child.order
);
self.sublayers.entry(parent).or_default().insert(child);

View File

@@ -30,11 +30,7 @@ impl Theme {
/// Chooses between [`Self::Dark`] or [`Self::Light`] based on a boolean value.
pub fn from_dark_mode(dark_mode: bool) -> Self {
if dark_mode {
Self::Dark
} else {
Self::Light
}
if dark_mode { Self::Dark } else { Self::Light }
}
}
@@ -93,9 +89,32 @@ impl ThemePreference {
/// Show radio-buttons to switch between light mode, dark mode and following the system theme.
pub fn radio_buttons(&mut self, ui: &mut crate::Ui) {
ui.horizontal(|ui| {
ui.selectable_value(self, Self::Light, "☀ Light");
ui.selectable_value(self, Self::Dark, "🌙 Dark");
ui.selectable_value(self, Self::System, "💻 System");
let system_theme = ui.ctx().input(|i| i.raw.system_theme);
ui.selectable_value(self, Self::System, "💻 System")
.on_hover_ui(|ui| {
ui.label("Follow the system theme preference.");
ui.add_space(4.0);
if let Some(system_theme) = system_theme {
ui.label(format!(
"The current system theme is: {}",
match system_theme {
Theme::Dark => "dark",
Theme::Light => "light",
}
));
} else {
ui.label("The system theme is unknown.");
}
});
ui.selectable_value(self, Self::Dark, "🌙 Dark")
.on_hover_text("Use the dark mode theme");
ui.selectable_value(self, Self::Light, "☀ Light")
.on_hover_text("Use the light mode theme");
});
}
}

View File

@@ -17,14 +17,13 @@
//! ```
use super::{
style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
Sense, TextStyle, Ui, Vec2,
Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, TextStyle, Ui,
Vec2, style::WidgetVisuals,
};
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText,
TextWrapMode, UiKind, WidgetText, epaint, vec2,
widgets::{Button, ImageButton},
};
use epaint::mutex::RwLock;
use std::sync::Arc;
@@ -88,7 +87,7 @@ fn set_menu_style(style: &mut Style) {
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
#[deprecated = "Use `crate::containers::menu::Bar` instead"]
#[deprecated = "Use `egui::MenuBar::new().ui(` instead"]
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());

View File

@@ -71,7 +71,8 @@ impl OperatingSystem {
#[cfg(feature = "log")]
log::warn!(
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
user_agent);
user_agent
);
Self::Unknown
}

View File

@@ -2,14 +2,14 @@ use std::sync::Arc;
use emath::GuiRounding as _;
use epaint::{
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
text::{Fonts, Galley, LayoutJob},
};
use crate::{
Color32, Context, FontId,
emath::{Align2, Pos2, Rangef, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
/// Helper to paint shapes and text to a specific region on a specific layer.
@@ -83,6 +83,7 @@ impl Painter {
}
/// If set, colors will be modified to look like this
#[deprecated = "Use `multiply_opacity` instead"]
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}

View File

@@ -1,9 +1,9 @@
use ahash::HashMap;
use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects};
use crate::{Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects, id::IdSet, style};
#[cfg(debug_assertions)]
use crate::{pos2, Align2, Color32, FontId, NumExt as _, Painter};
use crate::{Align2, Color32, FontId, NumExt as _, Painter, pos2};
/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)]
@@ -318,7 +318,7 @@ impl PassState {
);
self.available_rect.min.x = panel_rect.max.x;
self.unused_rect.min.x = panel_rect.max.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -329,7 +329,7 @@ impl PassState {
);
self.available_rect.max.x = panel_rect.min.x;
self.unused_rect.max.x = panel_rect.min.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -340,7 +340,7 @@ impl PassState {
);
self.available_rect.min.y = panel_rect.max.y;
self.unused_rect.min.y = panel_rect.max.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -351,13 +351,13 @@ impl PassState {
);
self.available_rect.max.y = panel_rect.min.y;
self.unused_rect.max.y = panel_rect.min.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.
self.unused_rect = Rect::NOTHING; // Nothing left unused after this
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
}

View File

@@ -258,7 +258,7 @@ impl Placer {
let region = &mut self.region;
region.max_rect.min.x = rect.min.x;
region.max_rect.max.x = rect.max.x;
region.max_rect = region.max_rect.union(region.min_rect); // make sure we didn't shrink too much
region.max_rect |= region.min_rect; // make sure we didn't shrink too much
region.cursor.min.x = region.max_rect.min.x;
region.cursor.max.x = region.max_rect.max.x;
@@ -275,7 +275,7 @@ impl Placer {
let region = &mut self.region;
region.max_rect.min.y = rect.min.y;
region.max_rect.max.y = rect.max.y;
region.max_rect = region.max_rect.union(region.min_rect); // make sure we didn't shrink too much
region.max_rect |= region.min_rect; // make sure we didn't shrink too much
region.cursor.min.y = region.max_rect.min.y;
region.cursor.max.y = region.max_rect.max.y;

View File

@@ -1,9 +1,10 @@
use std::{any::Any, sync::Arc};
use crate::{
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
WidgetRect, WidgetText,
emath::{Align, Pos2, Rect, Vec2},
pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip,
Ui, WidgetRect, WidgetText,
pass_state,
};
// ----------------------------------------------------------------------------
@@ -58,8 +59,8 @@ pub struct Response {
/// The intrinsic / desired size of the widget.
///
/// For a button, this will be the size of the label + the frames padding,
/// even if the button is laid out in a justified layout and the actual size will be larger.
/// This is the size that a non-wrapped, non-truncated, non-justified version of the widget
/// would have.
///
/// If this is `None`, use [`Self::rect`] instead.
///

View File

@@ -3,14 +3,15 @@
#![allow(clippy::if_same_then_else)]
use emath::Align;
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{
ecolor::Color32,
emath::{pos2, vec2, Rangef, Rect, Vec2},
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
WidgetText,
ecolor::Color32,
emath::{Rangef, Rect, Vec2, pos2, vec2},
reset_button_with,
};
/// How to format numbers in e.g. a [`crate::DragValue`].
@@ -920,6 +921,9 @@ pub struct Visuals {
/// this is more to provide a convenient summary of the rest of the settings.
pub dark_mode: bool,
/// ADVANCED: Controls how we render text.
pub text_alpha_from_coverage: AlphaFromCoverage,
/// Override default text color for all text.
///
/// This is great for setting the color of text for any widget.
@@ -935,6 +939,17 @@ pub struct Visuals {
/// it is disabled, non-interactive, hovered etc.
pub override_text_color: Option<Color32>,
/// How strong "weak" text is.
///
/// Ignored if [`Self::weak_text_color`] is set.
pub weak_text_alpha: f32,
/// Color of "weak" text.
///
/// If `None`, the color is [`Self::text_color`]
/// multiplied by [`Self::weak_text_alpha`].
pub weak_text_color: Option<Color32>,
/// Visual styles of widgets
pub widgets: Widgets,
@@ -952,6 +967,11 @@ pub struct Visuals {
/// that needs to look different from other interactive stuff.
pub extreme_bg_color: Color32,
/// The background color of [`crate::TextEdit`].
///
/// Defaults to [`Self::extreme_bg_color`].
pub text_edit_bg_color: Option<Color32>,
/// Background color behind code-styled monospaced labels.
pub code_bg_color: Color32,
@@ -1019,6 +1039,9 @@ pub struct Visuals {
/// How to display numeric color values.
pub numeric_color_space: NumericColorSpace,
/// How much to modify the alpha of a disabled widget.
pub disabled_alpha: f32,
}
impl Visuals {
@@ -1034,7 +1057,8 @@ impl Visuals {
}
pub fn weak_text_color(&self) -> Color32 {
self.gray_out(self.text_color())
self.weak_text_color
.unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha))
}
#[inline(always)]
@@ -1042,6 +1066,11 @@ impl Visuals {
self.widgets.active.text_color()
}
/// The background color of [`crate::TextEdit`].
pub fn text_edit_bg_color(&self) -> Color32 {
self.text_edit_bg_color.unwrap_or(self.extreme_bg_color)
}
/// Window background color.
#[inline(always)]
pub fn window_fill(&self) -> Color32 {
@@ -1054,17 +1083,32 @@ impl Visuals {
}
/// When fading out things, we fade the colors towards this.
// TODO(emilk): replace with an alpha
#[inline(always)]
#[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill
}
/// Returned a "grayed out" version of the given color.
/// Disabled widgets have their alpha modified by this.
#[inline(always)]
pub fn disabled_alpha(&self) -> f32 {
self.disabled_alpha
}
/// Returns a "disabled" version of the given color.
///
/// This function modifies the opcacity of the given color.
/// If this is undesirable use [`gray_out`](Self::gray_out).
#[inline(always)]
pub fn disable(&self, color: Color32) -> Color32 {
color.gamma_multiply(self.disabled_alpha())
}
/// Returns a "grayed out" version of the given color.
#[doc(alias = "grey_out")]
#[inline(always)]
pub fn gray_out(&self, color: Color32) -> Color32 {
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill)
}
}
@@ -1333,12 +1377,16 @@ impl Visuals {
pub fn dark() -> Self {
Self {
dark_mode: true,
text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
override_text_color: None,
weak_text_alpha: 0.6,
weak_text_color: None,
widgets: Widgets::default(),
selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
text_edit_bg_color: None, // use `extreme_bg_color` by default
code_bg_color: Color32::from_gray(64),
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
error_fg_color: Color32::from_rgb(255, 0, 0), // red
@@ -1384,6 +1432,7 @@ impl Visuals {
image_loading_spinners: true,
numeric_color_space: NumericColorSpace::GammaByte,
disabled_alpha: 0.5,
}
}
@@ -1391,6 +1440,7 @@ impl Visuals {
pub fn light() -> Self {
Self {
dark_mode: false,
text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
@@ -1557,8 +1607,8 @@ impl Default for Widgets {
// ----------------------------------------------------------------------------
use crate::{
widgets::{reset_button, DragValue, Slider, Widget},
Ui,
widgets::{DragValue, Slider, Widget, reset_button},
};
impl Style {
@@ -1680,11 +1730,11 @@ impl Style {
ui.end_row();
});
ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("🔠 Text styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.collapsing("🔄 Scroll Animation", |ui| scroll_animation.ui(ui));
ui.collapsing("🔄 Scroll animation", |ui| scroll_animation.ui(ui));
#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
@@ -2022,13 +2072,17 @@ impl WidgetVisuals {
impl Visuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
dark_mode: _,
dark_mode,
text_alpha_from_coverage,
override_text_color: _,
weak_text_alpha,
weak_text_color,
widgets,
selection,
hyperlink_color,
faint_bg_color,
extreme_bg_color,
text_edit_bg_color,
code_bg_color,
warn_fg_color,
error_fg_color,
@@ -2063,44 +2117,115 @@ impl Visuals {
image_loading_spinners,
numeric_color_space,
disabled_alpha,
} = self;
ui.collapsing("Background Colors", |ui| {
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
"Used for faint accentuation of interactive things, like striped grids.",
);
ui_color(ui, extreme_bg_color, "Extreme")
.on_hover_text("Background of plots and paintings");
fn ui_optional_color(
ui: &mut Ui,
color: &mut Option<Color32>,
default_value: Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.horizontal(|ui| {
let mut set = color.is_some();
ui.checkbox(&mut set, "");
if set {
let color = color.get_or_insert(default_value);
ui.color_edit_button_srgba(color);
} else {
*color = None;
};
});
ui.end_row();
label_response
}
ui.collapsing("Background colors", |ui| {
Grid::new("background_colors")
.num_columns(2)
.show(ui, |ui| {
fn ui_color(
ui: &mut Ui,
color: &mut Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.color_edit_button_srgba(color);
ui.end_row();
label_response
}
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
"Used for faint accentuation of interactive things, like striped grids.",
);
ui_color(ui, extreme_bg_color, "Extreme")
.on_hover_text("Background of plots and paintings");
ui_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit")
.on_hover_text("Background of TextEdit");
});
});
ui.collapsing("Text color", |ui| {
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
ui_text_color(
ui,
&mut widgets.inactive.fg_stroke.color,
"Unhovered button",
);
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
ui.label(label.into().color(*color));
ui.color_edit_button_srgba(color);
ui.end_row();
}
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
Grid::new("text_color").num_columns(2).show(ui, |ui| {
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
ui_text_color(ui, hyperlink_color, "hyperlink_color");
ui_text_color(
ui,
&mut widgets.inactive.fg_stroke.color,
"Unhovered button",
);
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(
|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("For monospaced inlined text ");
ui.code("like this");
ui.label(".");
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
ui_text_color(ui, hyperlink_color, "hyperlink_color");
ui.label(RichText::new("Code background").code())
.on_hover_ui(|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("For monospaced inlined text ");
ui.code("like this");
ui.label(".");
});
});
},
);
ui.color_edit_button_srgba(code_bg_color);
ui.end_row();
ui.label("Weak text alpha");
ui.add_enabled(
weak_text_color.is_none(),
DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0),
);
ui.end_row();
ui_optional_color(
ui,
weak_text_color,
widgets.noninteractive.text_color(),
"Weak text color",
);
});
ui.add_space(4.0);
text_alpha_from_coverage_ui(ui, text_alpha_from_coverage);
});
ui.collapsing("Text cursor", |ui| {
@@ -2191,12 +2316,60 @@ impl Visuals {
ui.label("Color picker type");
numeric_color_space.toggle_button_ui(ui);
});
ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha"));
});
ui.vertical_centered(|ui| reset_button(ui, self, "Reset visuals"));
let dark_mode = *dark_mode;
ui.vertical_centered(|ui| {
reset_button_with(
ui,
self,
"Reset visuals",
if dark_mode {
Self::dark()
} else {
Self::light()
},
);
});
}
}
fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) {
let mut dark_mode_special =
*text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
ui.horizontal(|ui| {
ui.label("Text rendering:");
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
if dark_mode_special {
*text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq;
} else {
let mut gamma = match text_alpha_from_coverage {
AlphaFromCoverage::Linear => 1.0,
AlphaFromCoverage::Gamma(gamma) => *gamma,
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
};
ui.add(
DragValue::new(&mut gamma)
.speed(0.01)
.range(0.1..=4.0)
.prefix("Gamma: "),
);
if gamma == 1.0 {
*text_alpha_from_coverage = AlphaFromCoverage::Linear;
} else {
*text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
}
}
});
}
impl TextCursorStyle {
fn ui(&mut self, ui: &mut Ui) {
let Self {
@@ -2310,22 +2483,6 @@ fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> im
}
}
fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into<WidgetText>) -> Response {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
ui.label(label);
})
.response
}
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) -> Response {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
ui.label(label.into().color(*color));
})
.response
}
impl HandleShape {
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {

View File

@@ -2,7 +2,7 @@ use emath::TSTransform;
use crate::{Context, Galley, Id};
use super::{text_cursor_state::is_word_char, CCursorRange};
use super::{CCursorRange, text_cursor_state::is_word_char};
/// Update accesskit with the current text state.
pub fn update_accesskit_for_text_widget(

View File

@@ -1,6 +1,6 @@
use epaint::{text::cursor::CCursor, Galley};
use epaint::{Galley, text::cursor::CCursor};
use crate::{os::OperatingSystem, Event, Id, Key, Modifiers};
use crate::{Event, Id, Key, Modifiers, os::OperatingSystem};
use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range};

View File

@@ -3,14 +3,14 @@ use std::sync::Arc;
use emath::TSTransform;
use crate::{
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
Galley, Id, LayerId, Pos2, Rect, Response, Ui,
Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, layers::ShapeIdx,
text::CCursor, text_selection::CCursorRange,
};
use super::{
text_cursor_state::cursor_rect,
visuals::{paint_text_selection, RowVertexIndices},
TextCursorState,
text_cursor_state::cursor_rect,
visuals::{RowVertexIndices, paint_text_selection},
};
/// Turn on to help debug this
@@ -546,7 +546,7 @@ impl LabelSelectionState {
if let Some(mut cursor_range) = cursor_state.range(galley) {
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
self.selection_bbox_this_frame |= galley_rect;
if let Some(selection) = &self.selection {
if selection.primary.widget_id == response.id {

View File

@@ -1,9 +1,9 @@
//! Text cursor changes/interaction, without modifying the text.
use epaint::text::{cursor::CCursor, Galley};
use epaint::text::{Galley, cursor::CCursor};
use unicode_segmentation::UnicodeSegmentation as _;
use crate::{epaint, NumExt as _, Rect, Response, Ui};
use crate::{NumExt as _, Rect, Response, Ui, epaint};
use super::CCursorRange;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals};
use crate::{Galley, Painter, Rect, Ui, Visuals, pos2, vec2};
use super::CCursorRange;

View File

@@ -5,11 +5,15 @@ use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use std::{any::Any, hash::Hash, sync::Arc};
use crate::close_tag::ClosableTag;
use crate::containers::menu;
use crate::ClosableTag;
#[cfg(debug_assertions)]
use crate::Stroke;
use crate::containers::menu;
use crate::{
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
WidgetRect, WidgetText,
containers::{CollapsingHeader, CollapsingResponse, Frame},
ecolor::Hsva,
emath, epaint,
@@ -22,13 +26,9 @@ use crate::{
util::IdTypeMap,
vec2, widgets,
widgets::{
color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link,
RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget,
Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton,
Separator, Spinner, TextEdit, Widget, color_picker,
},
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
WidgetRect, WidgetText,
};
// ----------------------------------------------------------------------------
@@ -522,7 +522,7 @@ impl Ui {
self.enabled = false;
if self.is_visible() {
self.painter
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
.multiply_opacity(self.visuals().disabled_alpha());
}
}
@@ -2101,13 +2101,13 @@ impl Ui {
Checkbox::new(checked, atoms).ui(self)
}
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
/// Acts like a checkbox, but looks like a [`Button::selectable`].
///
/// Click to toggle to bool.
///
/// See also [`Self::checkbox`].
pub fn toggle_value(&mut self, selected: &mut bool, text: impl Into<WidgetText>) -> Response {
let mut response = self.selectable_label(*selected, text);
pub fn toggle_value<'a>(&mut self, selected: &mut bool, atoms: impl IntoAtoms<'a>) -> Response {
let mut response = self.selectable_label(*selected, atoms);
if response.clicked() {
*selected = !*selected;
response.mark_changed();
@@ -2158,10 +2158,10 @@ impl Ui {
/// Show a label which can be selected or not.
///
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
/// See also [`Button::selectable`] and [`Self::toggle_value`].
#[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "]
pub fn selectable_label(&mut self, checked: bool, text: impl Into<WidgetText>) -> Response {
SelectableLabel::new(checked, text).ui(self)
pub fn selectable_label<'a>(&mut self, checked: bool, text: impl IntoAtoms<'a>) -> Response {
Button::selectable(checked, text).ui(self)
}
/// Show selectable text. It is selected if `*current_value == selected_value`.
@@ -2169,12 +2169,12 @@ impl Ui {
///
/// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`.
///
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
pub fn selectable_value<Value: PartialEq>(
/// See also [`Button::selectable`] and [`Self::toggle_value`].
pub fn selectable_value<'a, Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
text: impl Into<WidgetText>,
text: impl IntoAtoms<'a>,
) -> Response {
let mut response = self.selectable_label(*current_value == selected_value, text);
if response.clicked() && *current_value != selected_value {
@@ -2998,8 +2998,8 @@ impl Ui {
if is_anything_being_dragged && !can_accept_what_is_being_dragged {
// When dragging something else, show that it can't be dropped here:
fill = self.visuals().gray_out(fill);
stroke.color = self.visuals().gray_out(stroke.color);
fill = self.visuals().disable(fill);
stroke.color = self.visuals().disable(stroke.color);
}
frame.frame.fill = fill;

View File

@@ -1,6 +1,6 @@
use std::{hash::Hash, sync::Arc};
use crate::close_tag::ClosableTag;
use crate::ClosableTag;
#[expect(unused_imports)] // Used for doclinks
use crate::Ui;
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};

View File

@@ -438,7 +438,7 @@ impl ViewportBuilder {
}
/// macOS: Set to `true` to allow the window to be moved by dragging the background.
/// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders.
/// Enabling this feature can result in unexpected behavior with draggable UI widgets such as sliders.
#[inline]
pub fn with_movable_by_background(mut self, value: bool) -> Self {
self.movable_by_window_background = Some(value);

View File

@@ -4,8 +4,8 @@ use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
use crate::{
text::{LayoutJob, TextWrapping},
Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
text::{LayoutJob, TextWrapping},
};
/// Text and optional style choices for it.

View File

@@ -29,6 +29,7 @@ pub struct Button<'a> {
stroke: Option<Stroke>,
small: bool,
frame: Option<bool>,
frame_when_inactive: bool,
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: bool,
@@ -44,6 +45,7 @@ impl<'a> Button<'a> {
stroke: None,
small: false,
frame: None,
frame_when_inactive: true,
min_size: Vec2::ZERO,
corner_radius: None,
selected: false,
@@ -52,6 +54,27 @@ impl<'a> Button<'a> {
}
}
/// Show a selectable button.
///
/// Equivalent to:
/// ```rust
/// # use egui::{Button, IntoAtoms, __run_test_ui};
/// # __run_test_ui(|ui| {
/// let selected = true;
/// ui.add(Button::new("toggle me").selected(selected).frame_when_inactive(!selected).frame(true));
/// # });
/// ```
///
/// See also:
/// - [`Ui::selectable_value`]
/// - [`Ui::selectable_label`]
pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
Self::new(atoms)
.selected(selected)
.frame_when_inactive(selected)
.frame(true)
}
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
///
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
@@ -138,6 +161,18 @@ impl<'a> Button<'a> {
self
}
/// If `false`, the button will not have a frame when inactive.
///
/// Default: `true`.
///
/// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting
/// has no effect.
#[inline]
pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
self.frame_when_inactive = frame_when_inactive;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
#[inline]
@@ -220,6 +255,7 @@ impl<'a> Button<'a> {
stroke,
small,
frame,
frame_when_inactive,
mut min_size,
corner_radius,
selected,
@@ -243,9 +279,9 @@ impl<'a> Button<'a> {
let text = layout.text().map(String::from);
let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = if has_frame {
let mut button_padding = if has_frame_margin {
ui.spacing().button_padding
} else {
Vec2::ZERO
@@ -262,13 +298,22 @@ impl<'a> Button<'a> {
let response = if ui.is_rect_visible(prepared.response.rect) {
let visuals = ui.style().interact_selectable(&prepared.response, selected);
let visible_frame = if frame_when_inactive {
has_frame_margin
} else {
has_frame_margin
&& (prepared.response.hovered()
|| prepared.response.is_pointer_button_down_on()
|| prepared.response.has_focus())
};
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(visuals.text_color()));
}
prepared.fallback_text_color = visuals.text_color();
if has_frame {
if visible_frame {
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let fill = fill.unwrap_or(visuals.weak_bg_fill);
prepared.frame = prepared

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui,
Vec2, Widget, WidgetInfo, WidgetType,
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
WidgetInfo, WidgetType, epaint, pos2,
};
// TODO(emilk): allow checkbox without a text label

View File

@@ -2,12 +2,13 @@
use crate::util::fixed_cache::FixedCache;
use crate::{
epaint, lerp, remap_clamp, Area, Context, DragValue, Frame, Id, Key, Order, Painter, Response,
Sense, Ui, UiKind, Widget as _, WidgetInfo, WidgetType,
Context, DragValue, Id, Painter, Popup, PopupCloseBehavior, Response, Sense, Ui, Widget as _,
WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
};
use epaint::{
Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
ecolor::{Color32, Hsva, HsvaGamma, Rgba},
pos2, vec2, Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
pos2, vec2,
};
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
@@ -492,41 +493,23 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let popup_id = ui.auto_id_with("popup");
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
let open = Popup::is_id_open(ui.ctx(), popup_id);
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");
}
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
const COLOR_SLIDER_WIDTH: f32 = 275.0;
// TODO(lucasmerlin): Update this to use new Popup struct
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
ui.memory_mut(|mem| mem.keep_popup_open(popup_id));
let area_response = Area::new(popup_id)
.kind(UiKind::Picker)
.order(Order::Foreground)
.fixed_pos(button_response.rect.max)
.show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
Frame::popup(ui.style()).show(ui, |ui| {
if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed();
}
});
})
.response;
if !button_response.clicked()
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
ui.memory_mut(|mem| mem.close_popup(popup_id));
}
}
Popup::menu(&button_response)
.id(popup_id)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed();
}
});
button_response
}

View File

@@ -3,8 +3,8 @@
use std::{cmp::Ordering, ops::RangeInclusive};
use crate::{
emath, text, Button, CursorIcon, Id, Key, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR,
Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text,
};
// ----------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, text_selection, CursorIcon, Label, Response, Sense, Stroke, Ui, Widget, WidgetInfo,
WidgetText, WidgetType,
CursorIcon, Label, Response, Sense, Stroke, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
epaint, text_selection,
};
use self::text_selection::LabelSelectionState;

View File

@@ -2,14 +2,15 @@ use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
use epaint::{
text::{LayoutJob, TextFormat, TextWrapping},
RectShape,
text::{LayoutJob, TextFormat, TextWrapping},
};
use crate::{
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2, Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2,
};
/// A widget which displays an image.
@@ -277,7 +278,8 @@ impl<'a> Image<'a> {
}
/// Set alt text for the image. This will be shown when the image fails to load.
/// It will also be read to screen readers.
///
/// It will also be used for accessibility (e.g. read by screen readers).
#[inline]
pub fn alt_text(mut self, label: impl Into<String>) -> Self {
self.alt_text = Some(label.into());
@@ -499,7 +501,7 @@ impl ImageSize {
let point_size = match fit {
ImageFit::Original { scale } => {
return SizeHint::Scale((pixels_per_point * scale).ord())
return SizeHint::Scale((pixels_per_point * scale).ord());
}
ImageFit::Fraction(fract) => available_size * fract,
ImageFit::Exact(size) => size,
@@ -671,7 +673,7 @@ pub fn paint_texture_load_result(
rect: Rect,
show_loading_spinner: Option<bool>,
options: &ImageOptions,
alt: Option<&str>,
alt_text: Option<&str>,
) {
match tlr {
Ok(TexturePoll::Ready { texture }) => {
@@ -696,9 +698,9 @@ pub fn paint_texture_load_result(
0.0,
TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
);
if let Some(alt) = alt {
if let Some(alt_text) = alt_text {
job.append(
alt,
alt_text,
ui.spacing().item_spacing.x,
TextFormat::simple(font_id, ui.visuals().text_color()),
);

View File

@@ -1,6 +1,6 @@
use crate::{
widgets, Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo,
WidgetType,
Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType,
widgets,
};
/// A clickable image within a frame.

Some files were not shown because too many files have changed in this diff Show More