diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 8b7833366..278549e1e 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -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 diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index 8550cbeed..437108ab6 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -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: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d603051d5..493cc2fcf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/.typos.toml b/.typos.toml index b9d882beb..d4c716172 100644 --- a/.typos.toml +++ b/.typos.toml @@ -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 +] diff --git a/CHANGELOG.md b/CHANGELOG.md index 09faecc52..385550843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,271 @@ This file is updated upon each release. Changes since the last release can be found at 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`, 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ddedc378..110f92ddd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 9a441cb30..b1ed18559 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index bf5b27d30..498aefc08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/RELEASES.md b/RELEASES.md index 788070762..1634ad13d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -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 - '` -* [ ] `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 - '` * [ ] `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 - ` +* [ ] 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" -``` - -\ ## 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 diff --git a/clippy.toml b/clippy.toml index 24a804037..0f40ef38d 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.84" +msrv = "1.85" allow-unwrap-in-tests = true diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 88d510dba..535f8b18e 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,12 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/ecolor/src/cint_impl.rs b/crates/ecolor/src/cint_impl.rs index 5d34e5e36..3d9c9d2a6 100644 --- a/crates/ecolor/src/cint_impl.rs +++ b/crates/ecolor/src/cint_impl.rs @@ -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 ---- diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 55e3f232e..eaa1ce2c3 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -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). /// diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 8388e4139..97aed1b59 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -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]. diff --git a/crates/ecolor/src/hsva_gamma.rs b/crates/ecolor/src/hsva_gamma.rs index 67e167677..19e31f337 100644 --- a/crates/ecolor/src/hsva_gamma.rs +++ b/crates/ecolor/src/hsva_gamma.rs @@ -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. diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index df1697281..aa0b3436c 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,33 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/eframe/README.md b/crates/eframe/README.md index 3d787c572..9dbf42caf 100644 --- a/crates/eframe/README.md +++ b/crates/eframe/README.md @@ -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`. diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index eb0087b12..3e6023270 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -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." + )), } } } diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 1f1bf52f2..b94d8f83b 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -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. diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 5a3b1af32..fd502f1a8 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -52,10 +52,10 @@ fn roaming_appdata() -> Option { 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 { }; 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 { diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 946210c86..c83a6f14b 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -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() diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 8edfdbe2e..9c31aefe6 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -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, }; // ---------------------------------------------------------------------------- diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 96e93300e..f387779d7 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -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}; diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 675c57bec..9f2d2beba 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -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)] diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 1782a6797..168d6123a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -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::( - &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::( + &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(()) diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index eecf7c1f5..c27897090 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -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, diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 2bdd3af63..fdc9d2123 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -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 { - 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, 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 { + Some(prefers_color_scheme(window, theme).ok()??.matches()) +} + +fn prefers_color_scheme( + window: &web_sys::Window, + theme: Theme, +) -> Result, 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(); diff --git a/crates/eframe/src/web/web_logger.rs b/crates/eframe/src/web/web_logger.rs index fe0eaaebe..01c347b7e 100644 --- a/crates/eframe/src/web/web_logger.rs +++ b/crates/eframe/src/web/web_logger.rs @@ -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); } } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 3cfc53f1c..735c94d73 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -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(()) } diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 099be7aeb..c9449ab99 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -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 diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 7209c91a1..eda975de2 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,12 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index 38595bb6f..d47a828b4 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -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; } }; diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl index b60d9de9e..2921be74f 100644 --- a/crates/egui-wgpu/src/egui.wgsl +++ b/crates/egui-wgpu/src/egui.wgsl @@ -97,9 +97,8 @@ fn vs_main( @fragment fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4 { - // 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 { @fragment fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4 { - // 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. diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 7badefda4..012613aeb 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -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::>()) - } }; 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; diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index e0eafee71..cd02b59d8 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -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()" + ); } } diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index efdc9c23d..da51386e9 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,14 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index a0221294e..cec4b43c2 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -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; } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index f205c3108..b07d47b0e 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1566,8 +1566,7 @@ pub fn create_window( ) -> Result { 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; diff --git a/crates/egui/src/animation_manager.rs b/crates/egui/src/animation_manager.rs index 15002141f..50f97e992 100644 --- a/crates/egui/src/animation_manager.rs +++ b/crates/egui/src/animation_manager.rs @@ -1,6 +1,6 @@ use crate::{ - emath::{remap_clamp, NumExt as _}, Id, IdMap, InputState, + emath::{NumExt as _, remap_clamp}, }; #[derive(Clone, Default)] diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index b3336b263..ee5ff30d4 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -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, } diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 75b03c04a..34cac4ceb 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -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); diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 008b9cce8..62584c4e4 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -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); diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs index 50fa443a9..f1ae0f81b 100644 --- a/crates/egui/src/atomics/sized_atom.rs +++ b/crates/egui/src/atomics/sized_atom.rs @@ -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>, } diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index 03eeaf5fc..3fe0a7a59 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -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); diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4cf5ee07f..7237c980e 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -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, 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); diff --git a/crates/egui/src/containers/close_tag.rs b/crates/egui/src/containers/close_tag.rs index 3d5e3f434..3e93dbbd2 100644 --- a/crates/egui/src/containers/close_tag.rs +++ b/crates/egui/src/containers/close_tag.rs @@ -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, diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 404163fab..66dbc6446 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -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}; diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index f4282008f..6432f8d0f 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -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> { 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()); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 2f8e7cbe8..e5f8146e3 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -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::(), 32, + std::mem::size_of::(), + 32, "Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 4fe06477d..1e2cbfa2e 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -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, 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, diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 31898838e..4312385da 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -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, diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 2edc628e9..e36ad6e1b 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -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, diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs index cc75494e1..3ddf77bf6 100644 --- a/crates/egui/src/containers/old_popup.rs +++ b/crates/egui/src/containers/old_popup.rs @@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer( widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - 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( widget_rect: &Rect, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - 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( suggested_position: Pos2, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - 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) } diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index db0e8a7f9..2869f598c 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -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 { diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 47e800d22..9c2804138 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -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 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(self, content: impl FnOnce(&mut Ui) -> R) -> Option> { + 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 { + ctx.memory(|mem| mem.popup_position(popup_id)) + } +} diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 11af0e62d..3f22b374d 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -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)] diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index aaca37291..4c8c9f64b 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -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. diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 4a2f5013c..2d5bdc66f 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -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)] diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs index a7275ad54..692803ee0 100644 --- a/crates/egui/src/containers/sides.rs +++ b/crates/egui/src/containers/sides.rs @@ -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, spacing: Option, + kind: SidesKind, + wrap_mode: Option, +} + +#[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( 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( + ui: &mut Ui, + max_rect: emath::Rect, + layout: Layout, + add_content: impl FnOnce(&mut Ui) -> Ret, + wrap_mode: Option, + ) -> (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) } } diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs index a6cb3199e..99bc95d5c 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -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, + 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 diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index b34332ea1..e93b046e5 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -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). /// diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 27a3fcd9b..9594f03e9 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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 _; diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 1e2580678..05b93616d 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -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. diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index bb9487bd3..2cd1a2755 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -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; diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index dbc22a09d..31f9e7a71 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -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)] diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 2f0edcfaf..1361c1c49 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -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}; diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7fd073167..a3ebf532e 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -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 } diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index b4c789a8e..102b91fff 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -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) { 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; diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index b6b63e19d..9cd76b3a0 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -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}; diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 927810ba6..19d6c2525 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -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; diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 81a60812e..927ffc36b 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -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)] diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 1ada1cc00..c90b209be 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -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 { diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 4ff7db230..c2908df19 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `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, diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 9c9df5a9c..43aa1785e 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -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; diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs index 5f3e0d3bc..9f0c60356 100644 --- a/crates/egui/src/load/bytes_loader.rs +++ b/crates/egui/src/load/bytes_loader.rs @@ -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!`. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 9bf482b83..d4912f9d8 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -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>) { 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 { 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); diff --git a/crates/egui/src/memory/theme.rs b/crates/egui/src/memory/theme.rs index 4a63ecd56..555edaedd 100644 --- a/crates/egui/src/memory/theme.rs +++ b/crates/egui/src/memory/theme.rs @@ -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"); }); } } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 384a4d0de..efd0c684f 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -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(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { ui.horizontal(|ui| { set_menu_style(ui.style_mut()); diff --git a/crates/egui/src/os.rs b/crates/egui/src/os.rs index 283e33863..a9b4a874c 100644 --- a/crates/egui/src/os.rs +++ b/crates/egui/src/os.rs @@ -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 } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index c17fbb272..fe273970e 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -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) { self.fade_to_color = fade_to_color; } diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 1f629253c..ca0d15720 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -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; } } diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 2bd0314c0..1812ba1bb 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -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; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index b54cb10e0..85dc8a607 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -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. /// diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 93c44db00..8a34e0b6a 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -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, + /// 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, + /// 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, + /// 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, + default_value: Color32, + label: impl Into, + ) -> 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, + ) -> 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) { + 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) -> im } } -fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into) -> 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) -> 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| { diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index e04a54d18..b18995542 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -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( diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index 05351e0ac..10980c581 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -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}; diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index a315c2354..8297bc429 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -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 { diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index d2158c6bd..2a02e4577 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -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; diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index deee5690b..e3054b19d 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -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; diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 60c59e8f5..1c68380bf 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -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) -> 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) -> 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( + /// 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, + 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; diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 4aade7d64..549170182 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -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}; diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 9f5bb1e31..bf09fc845 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -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); diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index d0edb6a26..75bf36d83 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -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. diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index aa75eabd8..d836c0701 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -29,6 +29,7 @@ pub struct Button<'a> { stroke: Option, small: bool, frame: Option, + frame_when_inactive: bool, min_size: Vec2, corner_radius: Option, 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 diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index f7498de5a..c90cca292 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -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 diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 157157a05..cc8ce296e 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -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) -> 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 } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index a9d971916..9515726c2 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -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, }; // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index 3e5ff88dc..989643304 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -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; diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 07b08e53c..08b377843 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -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) -> 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, 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()), ); diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index 7962e5a42..a765a745a 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -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. diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 504435a00..4ad301b4e 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection::LabelSelectionState, Align, Direction, FontSelection, Galley, - Pos2, Response, Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + Align, Direction, FontSelection, Galley, Pos2, Response, Sense, Stroke, TextWrapMode, Ui, + Widget, WidgetInfo, WidgetText, WidgetType, epaint, pos2, text_selection::LabelSelectionState, }; /// Static text. @@ -165,7 +165,7 @@ impl Label { }; select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key. - sense = sense.union(select_sense); + sense |= select_sense; } if let WidgetText::Galley(galley) = self.text { @@ -219,11 +219,12 @@ impl Label { let rect = galley.rows[0] .rect_without_leading_space() .translate(pos.to_vec2()); - dbg!(galley.desired_size()); - let mut response = ui.allocate_rect(rect, sense, galley.desired_size()); + dbg!(galley.intrinsic_size()); + let mut response = ui.allocate_rect(rect, sense, galley.intrinsic_size()); + response.intrinsic_size = Some(galley.intrinsic_size()); for placed_row in galley.rows.iter().skip(1) { let rect = placed_row.rect().translate(pos.to_vec2()); - response |= ui.allocate_rect(rect, sense, galley.desired_size()); + response |= ui.allocate_rect(rect, sense, galley.intrinsic_size()); } (pos, galley, response) } else { @@ -253,8 +254,8 @@ impl Label { }; let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); - let (rect, response) = - ui.allocate_at_least(galley.size(), sense, galley.desired_size()); // TODO: Change back to allocate_exact_size + let (rect, mut response) = ui.allocate_at_least(galley.size(), sense, galley.intrinsic_size()); + response.intrinsic_size = Some(galley.intrinsic_size()); let galley_pos = match galley.job.halign { Align::LEFT => rect.left_top(), Align::Center => rect.center_top(), diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index a4a40ec66..9cf003c94 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -4,7 +4,7 @@ //! * `ui.add(Label::new("Text").text_color(color::red));` //! * `if ui.add(Button::new("Click me")).clicked() { … }` -use crate::{epaint, Response, Ui}; +use crate::{Response, Ui, epaint}; mod button; mod checkbox; @@ -22,20 +22,21 @@ mod slider; mod spinner; pub mod text_edit; +#[expect(deprecated)] +pub use self::selected_label::SelectableLabel; pub use self::{ button::Button, checkbox::Checkbox, drag_value::DragValue, hyperlink::{Hyperlink, Link}, image::{ - decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at, FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource, + decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at, }, image_button::ImageButton, label::Label, progress_bar::ProgressBar, radio_button::RadioButton, - selected_label::SelectableLabel, separator::Separator, slider::{Slider, SliderClamping, SliderOrientation}, spinner::Spinner, diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 6739c0e2e..bba6be8ef 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -1,6 +1,6 @@ use crate::{ - lerp, vec2, Color32, CornerRadius, NumExt as _, Pos2, Rect, Response, Rgba, Sense, Shape, - Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + Color32, CornerRadius, NumExt as _, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke, + TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, lerp, vec2, }; enum ProgressBarText { diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 53dda399f..8b1f7cc4b 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget, - WidgetInfo, WidgetType, + Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget, + WidgetInfo, WidgetType, epaint, }; /// One out of several alternatives, either selected or not. diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index f8915ee3a..536ef43da 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -1,89 +1,13 @@ -use crate::{ - NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType, -}; +#![expect(deprecated, clippy::new_ret_no_self)] -/// One out of several alternatives, either selected or not. -/// Will mark selected items with a different background color. -/// An alternative to [`crate::RadioButton`] and [`crate::Checkbox`]. -/// -/// Usually you'd use [`Ui::selectable_value`] or [`Ui::selectable_label`] instead. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// #[derive(PartialEq)] -/// enum Enum { First, Second, Third } -/// let mut my_enum = Enum::First; -/// -/// ui.selectable_value(&mut my_enum, Enum::First, "First"); -/// -/// // is equivalent to: -/// -/// if ui.add(egui::SelectableLabel::new(my_enum == Enum::First, "First")).clicked() { -/// my_enum = Enum::First -/// } -/// # }); -/// ``` -#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] -pub struct SelectableLabel { - selected: bool, - text: WidgetText, -} +use crate::WidgetText; + +#[deprecated = "Use `Button::selectable()` instead"] +pub struct SelectableLabel {} impl SelectableLabel { - pub fn new(selected: bool, text: impl Into) -> Self { - Self { - selected, - text: text.into(), - } - } -} - -impl Widget for SelectableLabel { - fn ui(self, ui: &mut Ui) -> Response { - let Self { selected, text } = self; - - let button_padding = ui.spacing().button_padding; - let total_extra = button_padding + button_padding; - - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); - - let mut desired_size = total_extra + galley.size(); - desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - let (rect, response) = - ui.allocate_at_least(desired_size, Sense::click(), galley.desired_size()); - response.widget_info(|| { - WidgetInfo::selected( - WidgetType::SelectableLabel, - ui.is_enabled(), - selected, - galley.text(), - ) - }); - - if ui.is_rect_visible(response.rect) { - let text_pos = ui - .layout() - .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) - .min; - - let visuals = ui.style().interact_selectable(&response, selected); - - if selected || response.hovered() || response.highlighted() || response.has_focus() { - let rect = rect.expand(visuals.expansion); - - ui.painter().rect( - rect, - visuals.corner_radius, - visuals.weak_bg_fill, - visuals.bg_stroke, - epaint::StrokeKind::Inside, - ); - } - - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - - response + #[deprecated = "Use `Button::selectable()` instead"] + pub fn new(selected: bool, text: impl Into) -> super::Button<'static> { + crate::Button::selectable(selected, text) } } diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index eabd98d3e..e5f064440 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -1,4 +1,4 @@ -use crate::{vec2, Response, Sense, Ui, Vec2, Widget}; +use crate::{Response, Sense, Ui, Vec2, Widget, vec2}; /// A visual separator. A horizontal or vertical line (depending on [`crate::Layout`]). /// diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index a75b869d5..e47a593ae 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -3,9 +3,9 @@ use std::ops::RangeInclusive; use crate::{ - emath, epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2, Color32, - DragValue, EventFilter, Key, Label, NumExt as _, Pos2, Rangef, Rect, Response, Sense, - TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR, + Color32, DragValue, EventFilter, Key, Label, MINUS_CHAR_STR, NumExt as _, Pos2, Rangef, Rect, + Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, emath, + epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2, }; use super::drag_value::clamp_value_to_range; @@ -134,11 +134,7 @@ impl<'a> Slider<'a> { value.to_f64() }); - if Num::INTEGRAL { - slf.integer() - } else { - slf - } + if Num::INTEGRAL { slf.integer() } else { slf } } pub fn from_get_set( @@ -736,7 +732,7 @@ impl Slider<'_> { let prev_value = self.get_value(); let prev_position = self.position_from_value(prev_value, position_range); let new_position = prev_position + ui_point_per_step * kb_step; - let new_value = match self.step { + let mut new_value = match self.step { Some(step) => prev_value + (kb_step as f64 * step), None if self.smart_aim => { let aim_radius = 0.49 * ui_point_per_step; // Chosen so we don't include `prev_value` in the search. @@ -747,6 +743,19 @@ impl Slider<'_> { } _ => self.value_from_position(new_position, position_range), }; + if let Some(max_decimals) = self.max_decimals { + // self.set_value rounds, so ensure we reach at the least the next breakpoint + // note: we give it a little bit of leeway due to floating point errors. (0.1 isn't representable in binary) + // 'set_value' will round it to the nearest value. + let min_increment = 1.0 / (10.0_f64.powi(max_decimals as i32)); + new_value = if new_value > prev_value { + f64::max(new_value, prev_value + min_increment * 1.001) + } else if new_value < prev_value { + f64::min(new_value, prev_value - min_increment * 1.001) + } else { + new_value + }; + } self.set_value(new_value); } diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index abb4b27bc..573517dcb 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,4 +1,4 @@ -use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; +use epaint::{Color32, Pos2, Rect, Shape, Stroke, emath::lerp, vec2}; use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType}; diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index a05e7ffda..921dae9af 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -2,19 +2,19 @@ use std::sync::Arc; use emath::{Rect, TSTransform}; use epaint::{ - text::{cursor::CCursor, Galley, LayoutJob}, StrokeKind, + text::{Galley, LayoutJob, cursor::CCursor}, }; use crate::{ - epaint, + Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent, + Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer, + TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint, os::OperatingSystem, output::OutputEvent, response, text_selection, - text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange}, - vec2, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, - ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, - TextBuffer, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, + text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection}, + vec2, }; use super::{TextEditOutput, TextEditState}; @@ -63,7 +63,7 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { text: &'t mut dyn TextBuffer, @@ -207,7 +207,7 @@ impl<'t> TextEdit<'t> { self } - /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::extreme_bg_color`]. + /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::text_edit_bg_color`]. // TODO(bircni): remove this once #3284 is implemented #[inline] pub fn background_color(mut self, color: Color32) -> Self { @@ -428,7 +428,7 @@ impl TextEdit<'_> { let where_to_put_background = ui.painter().add(Shape::Noop); let background_color = self .background_color - .unwrap_or(ui.visuals().extreme_bg_color); + .unwrap_or_else(|| ui.visuals().text_edit_bg_color()); let output = self.show_content(ui); if frame { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 0051ea8e7..11304e700 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use crate::mutex::Mutex; use crate::{ - text_selection::{CCursorRange, TextCursorState}, Context, Id, + text_selection::{CCursorRange, TextCursorState}, }; pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>; diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index ebf33b097..a67dc1b38 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, ops::Range}; use epaint::{ - text::{cursor::CCursor, TAB_SIZE}, Galley, + text::{TAB_SIZE, cursor::CCursor}, }; use crate::{ diff --git a/crates/egui_demo_app/src/apps/fractal_clock.rs b/crates/egui_demo_app/src/apps/fractal_clock.rs index 3eeddd7be..50d9ae5ef 100644 --- a/crates/egui_demo_app/src/apps/fractal_clock.rs +++ b/crates/egui_demo_app/src/apps/fractal_clock.rs @@ -1,8 +1,8 @@ use egui::{ + Color32, Painter, Pos2, Rect, Shape, Stroke, Ui, Vec2, containers::{CollapsingHeader, Frame}, emath, pos2, widgets::Slider, - Color32, Painter, Pos2, Rect, Shape, Stroke, Ui, Vec2, }; use std::f32::consts::TAU; diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index 326718a23..75ff27190 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -1,9 +1,9 @@ -use egui::emath::Rot2; -use egui::panel::Side; -use egui::panel::TopBottomSide; use egui::ImageFit; use egui::Slider; use egui::Vec2; +use egui::emath::Rot2; +use egui::panel::Side; +use egui::panel::TopBottomSide; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ImageViewer { diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index 1fb0c6553..e98d5179d 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -146,6 +146,10 @@ impl BackendPanel { // builds to keep the noise down in the official demo. if cfg!(debug_assertions) { ui.collapsing("More…", |ui| { + ui.horizontal(|ui| { + ui.label("Total ui frames:"); + ui.monospace(ui.ctx().cumulative_frame_nr().to_string()); + }); ui.horizontal(|ui| { ui.label("Total ui passes:"); ui.monospace(ui.ctx().cumulative_pass_nr().to_string()); diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index 30b3458ab..594e70425 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -54,7 +54,7 @@ impl FrameHistory { } fn graph(&self, ui: &mut egui::Ui) -> egui::Response { - use egui::{emath, epaint, pos2, vec2, Pos2, Rect, Sense, Shape, Stroke, TextStyle}; + use egui::{Pos2, Rect, Sense, Shape, Stroke, TextStyle, emath, epaint, pos2, vec2}; ui.label("egui CPU usage history"); diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index cf391ee99..099d16f59 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -16,7 +16,9 @@ fn main() -> eframe::Result { start_puffin_server(); #[cfg(not(feature = "puffin"))] - panic!("Unknown argument: {arg} - you need to enable the 'puffin' feature to use this."); + panic!( + "Unknown argument: {arg} - you need to enable the 'puffin' feature to use this." + ); } _ => { @@ -39,7 +41,12 @@ fn main() -> eframe::Result { rust_log += &format!(",{loud_crate}=warn"); } } - std::env::set_var("RUST_LOG", rust_log); + + // SAFETY: we call this from the main thread without any other threads running. + #[expect(unsafe_code)] + unsafe { + std::env::set_var("RUST_LOG", rust_log); + } } env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 2b0bfc6c1..31bb331ef 100644 --- a/crates/egui_demo_app/tests/snapshots/clock.png +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61db3807f755ac832ba069e1adaf8aeb550c88737b4907748667a271ae29863d -size 334792 +oid sha256:0bd688ff74f9a096edab545fbcbf61b61a464183da066ae4a120ce1e2abf3e7b +size 334969 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index ce19412f7..2458cd8ba 100644 --- a/crates/egui_demo_app/tests/snapshots/custom3d.png +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e0a6cdf175606a513ddf410ae1b873a9817305ecad403116fad3c6ff795fa3 -size 92185 +oid sha256:c80c4ae4c2bfbc5c91e9cd94213a4f87646fe910b4a7c747531a1efcf23def47 +size 92364 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 7666b658e..f06e16cba 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e6a383dca7e91d07df4bf501e2de13d046f04546a08d026efe3f82fc96b6e29 -size 178887 +oid sha256:fc3dbdcd483d4da7a9c1a00f0245a7882997fbcd2d26f8d6a6d2d855f3382063 +size 179724 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index d5bde1f98..e6f108e98 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006 -size 100780 +oid sha256:c8ad2c2d494e2287b878049091688069e4d86b69ae72b89cb7ecbe47d8c35e33 +size 100766 diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 8b0b272fa..63b3b2a89 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -1,8 +1,8 @@ -use egui::accesskit::Role; use egui::Vec2; +use egui::accesskit::Role; use egui_demo_app::{Anchor, WrapApp}; -use egui_kittest::kittest::Queryable as _; use egui_kittest::SnapshotResults; +use egui_kittest::kittest::Queryable as _; #[test] fn test_demo_app() { @@ -55,7 +55,7 @@ fn test_demo_app() { harness .get_by_role_and_label(Role::TextInput, "URI:") .focus(); - harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); harness .get_by_role_and_label(Role::TextInput, "URI:") @@ -69,6 +69,6 @@ fn test_demo_app() { // Can't use Harness::run because fractal clock keeps requesting repaints harness.run_steps(4); - results.add(harness.try_snapshot(&anchor.to_string())); + results.add(harness.try_snapshot(anchor.to_string())); } } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index b511f0de8..02f098e67 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,6 +1,6 @@ use std::fmt::Write as _; -use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; use egui::epaint::TextShape; use egui::load::SizedTexture; @@ -168,13 +168,14 @@ pub fn criterion_benchmark(c: &mut Criterion) { let fonts = egui::epaint::text::Fonts::new( pixels_per_point, max_texture_side, + egui::epaint::AlphaFromCoverage::default(), egui::FontDefinitions::default(), ); { let mut locked_fonts = fonts.lock(); c.bench_function("text_layout_uncached", |b| { b.iter(|| { - use egui::epaint::text::{layout, LayoutJob}; + use egui::epaint::text::{LayoutJob, layout}; let job = LayoutJob::simple( LOREM_IPSUM_LONG.to_owned(), @@ -210,7 +211,11 @@ pub fn criterion_benchmark(c: &mut Criterion) { let mut rng = rand::rng(); b.iter(|| { - fonts.begin_pass(pixels_per_point, max_texture_side); + fonts.begin_pass( + pixels_per_point, + max_texture_side, + egui::epaint::AlphaFromCoverage::default(), + ); // Delete a random character, simulating a user making an edit in a long file: let mut new_string = string.clone(); diff --git a/crates/egui_demo_lib/data/ring.png b/crates/egui_demo_lib/data/ring.png new file mode 100644 index 000000000..f82db9196 Binary files /dev/null and b/crates/egui_demo_lib/data/ring.png differ diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 89a8cdafa..52cafad0c 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -62,6 +62,7 @@ impl CodeExample { } ui.end_row(); + #[expect(clippy::literal_string_with_formatting_args)] show_code(ui, r#"ui.label(format!("{name} is {age}"));"#); ui.label(format!("{name} is {age}")); ui.end_row(); diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index c385f10e6..aec2acbc2 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,8 +1,9 @@ use egui::{ + Color32, Context, Pos2, Rect, Ui, Vec2, containers::{Frame, Window}, emath, epaint, epaint::PathStroke, - hex_color, lerp, pos2, remap, vec2, Color32, Context, Pos2, Rect, Ui, Vec2, + hex_color, lerp, pos2, remap, vec2, }; #[derive(Default)] diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index cc43645c6..9ee660896 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -1,9 +1,9 @@ use std::collections::BTreeSet; use super::About; -use crate::is_mobile; use crate::Demo; use crate::View as _; +use crate::is_mobile; use egui::containers::menu; use egui::style::StyleModifier; use egui::{Context, Modifiers, ScrollArea, Ui}; @@ -237,7 +237,7 @@ impl DemoWindows { fn mobile_top_bar(&mut self, ctx: &Context) { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - menu::Bar::new() + menu::MenuBar::new() .config(menu::MenuConfig::new().style(StyleModifier::default())) .ui(ui, |ui| { let font_size = 16.5; @@ -290,7 +290,7 @@ impl DemoWindows { }); egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - menu::Bar::new().ui(ui, |ui| { + menu::MenuBar::new().ui(ui, |ui| { file_menu_button(ui); }); }); @@ -370,10 +370,10 @@ fn file_menu_button(ui: &mut Ui) { #[cfg(test)] mod tests { - use crate::{demo::demo_app_windows::DemoGroups, Demo as _}; - use egui::Vec2; - use egui_kittest::kittest::Queryable as _; - use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; + use crate::{Demo as _, demo::demo_app_windows::DemoGroups}; + + use egui_kittest::kittest::{NodeT as _, Queryable as _}; + use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults}; #[test] fn demos_should_match_snapshot() { @@ -399,23 +399,24 @@ mod tests { demo.show(ctx, &mut true); }); - let window = harness.node().children().next().unwrap(); + let window = harness.queryable_node().children().next().unwrap(); // TODO(lucasmerlin): Windows should probably have a label? //let window = harness.get_by_label(name); - let size = window.raw_bounds().expect("window bounds").size(); - harness.set_size(Vec2::new(size.width as f32, size.height as f32)); + let size = window.rect().size(); + harness.set_size(size); // Run the app for some more frames... harness.run_ok(); let mut options = SnapshotOptions::default(); - // The Bézier Curve demo needs a threshold of 2.1 to pass on linux + if name == "Bézier Curve" { - options.threshold = 2.1; + // The Bézier Curve demo needs a threshold of 2.1 to pass on linux: + options = options.threshold(OsThreshold::new(0.0).linux(2.1)); } - results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options)); + results.add(harness.try_snapshot_options(format!("demos/{name}"), &options)); } } diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 7fa3f01bb..d483ced3f 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -1,4 +1,4 @@ -use egui::{vec2, Color32, Context, Frame, Id, Ui, Window}; +use egui::{Color32, Context, Frame, Id, Ui, Window, vec2}; #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index 0aeb09ddf..24cedd1aa 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -1,8 +1,8 @@ use super::{Demo, View}; use egui::{ - vec2, Align, Align2, Checkbox, CollapsingHeader, Color32, ComboBox, Context, FontId, Resize, - RichText, Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, + Align, Align2, Checkbox, CollapsingHeader, Color32, ComboBox, Context, FontId, Resize, + RichText, Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, vec2, }; /// Showcase some ui code diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index fcb33f0bb..a916c8bdf 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -162,10 +162,10 @@ impl crate::View for Modals { #[cfg(test)] mod tests { - use crate::demo::modals::Modals; use crate::Demo as _; + use crate::demo::modals::Modals; use egui::accesskit::Role; - use egui::Key; + use egui::{Key, Popup}; use egui_kittest::kittest::Queryable as _; use egui_kittest::{Harness, SnapshotResults}; @@ -187,12 +187,12 @@ mod tests { // Harness::run would fail because we keep requesting repaints to simulate progress. harness.run_ok(); - assert!(harness.ctx.memory(|mem| mem.any_popup_open())); + assert!(Popup::is_any_open(&harness.ctx)); assert!(harness.state().user_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run_ok(); - assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); + assert!(!Popup::is_any_open(&harness.ctx)); assert!(harness.state().user_modal_open); } @@ -214,7 +214,7 @@ mod tests { assert!(harness.state().user_modal_open); assert!(harness.state().save_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run(); assert!(harness.state().user_modal_open); @@ -267,7 +267,7 @@ mod tests { harness.run_ok(); - harness.get_by_label("Yes Please").simulate_click(); + harness.get_by_label("Yes Please").click(); harness.run_ok(); diff --git a/crates/egui_demo_lib/src/demo/multi_touch.rs b/crates/egui_demo_lib/src/demo/multi_touch.rs index b6580c5f8..0c2d98202 100644 --- a/crates/egui_demo_lib/src/demo/multi_touch.rs +++ b/crates/egui_demo_lib/src/demo/multi_touch.rs @@ -1,6 +1,7 @@ use egui::{ + Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2, emath::{RectTransform, Rot2}, - vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2, + vec2, }; pub struct MultiTouch { diff --git a/crates/egui_demo_lib/src/demo/paint_bezier.rs b/crates/egui_demo_lib/src/demo/paint_bezier.rs index df85e4377..7017560a5 100644 --- a/crates/egui_demo_lib/src/demo/paint_bezier.rs +++ b/crates/egui_demo_lib/src/demo/paint_bezier.rs @@ -1,8 +1,8 @@ use egui::{ - emath, + Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, + Widget as _, Window, emath, epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape}, - pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, - Widget as _, Window, + pos2, }; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/demo/painting.rs b/crates/egui_demo_lib/src/demo/painting.rs index 8e1df7ae7..5a4942f68 100644 --- a/crates/egui_demo_lib/src/demo/painting.rs +++ b/crates/egui_demo_lib/src/demo/painting.rs @@ -1,4 +1,4 @@ -use egui::{emath, vec2, Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window}; +use egui::{Color32, Context, Frame, Pos2, Rect, Sense, Stroke, Ui, Window, emath, vec2}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/crates/egui_demo_lib/src/demo/password.rs b/crates/egui_demo_lib/src/demo/password.rs index f22b5aa8a..04b3c6f37 100644 --- a/crates/egui_demo_lib/src/demo/password.rs +++ b/crates/egui_demo_lib/src/demo/password.rs @@ -27,7 +27,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response { let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Toggle the `show_plaintext` bool with a button: let response = ui - .add(egui::SelectableLabel::new(show_plaintext, "👁")) + .selectable_label(show_plaintext, "👁") .on_hover_text("Show/hide password"); if response.clicked() { diff --git a/crates/egui_demo_lib/src/demo/popups.rs b/crates/egui_demo_lib/src/demo/popups.rs index 2da624411..5b6aee6df 100644 --- a/crates/egui_demo_lib/src/demo/popups.rs +++ b/crates/egui_demo_lib/src/demo/popups.rs @@ -1,9 +1,9 @@ use crate::rust_view_ui; -use egui::color_picker::{color_picker_color32, Alpha}; +use egui::color_picker::{Alpha, color_picker_color32}; use egui::containers::menu::{MenuConfig, SubMenuButton}; use egui::{ - include_image, Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, - RectAlign, RichText, Tooltip, Ui, UiBuilder, + Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, RectAlign, RichText, + Tooltip, Ui, UiBuilder, include_image, }; /// Showcase [`Popup`]. diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index e8d483264..e19a2d048 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -1,6 +1,6 @@ use egui::{ - pos2, scroll_area::ScrollBarVisibility, Align, Align2, Color32, DragValue, NumExt as _, Rect, - ScrollArea, Sense, Slider, TextStyle, TextWrapMode, Ui, Vec2, Widget as _, + Align, Align2, Color32, DragValue, NumExt as _, Rect, ScrollArea, Sense, Slider, TextStyle, + TextWrapMode, Ui, Vec2, Widget as _, pos2, scroll_area::ScrollBarVisibility, }; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -222,7 +222,7 @@ fn huge_content_painter(ui: &mut egui::Ui) { font_id.clone(), ui.visuals().text_color(), ); - used_rect = used_rect.union(text_rect); + used_rect |= text_rect; } ui.allocate_rect(used_rect, Sense::hover(), Vec2::ZERO); // TODO // make sure it is visible! diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index ef8bdb0cd..6b512e1ea 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -1,4 +1,4 @@ -use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui}; +use egui::{Slider, SliderClamping, SliderOrientation, Ui, style::HandleShape}; /// Showcase sliders #[derive(PartialEq)] diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index 17e19d01d..5a55569b7 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -330,7 +330,9 @@ fn expanding_content(ui: &mut egui::Ui) { } fn long_text(row_index: usize) -> String { - format!("Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!") + format!( + "Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!" + ) } fn thick_row(row_index: usize) -> bool { diff --git a/crates/egui_demo_lib/src/demo/tests/layout_test.rs b/crates/egui_demo_lib/src/demo/tests/layout_test.rs index f58369121..f63eff0a9 100644 --- a/crates/egui_demo_lib/src/demo/tests/layout_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/layout_test.rs @@ -1,4 +1,4 @@ -use egui::{vec2, Align, Direction, Layout, Resize, Slider, Ui}; +use egui::{Align, Direction, Layout, Resize, Slider, Ui, vec2}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 20e8d21bf..cb08cf24e 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -1,7 +1,8 @@ use egui::{ + Color32, Pos2, Rect, Sense, StrokeKind, Vec2, emath::{GuiRounding as _, TSTransform}, epaint::{self, RectShape}, - vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2, + vec2, }; #[derive(Clone, Debug, PartialEq)] @@ -373,7 +374,7 @@ mod tests { harness.fit_contents(); harness.run(); - harness.snapshot(&format!("tessellation_test/{name}")); + harness.snapshot(format!("tessellation_test/{name}")); } } } diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 685a9c38f..a36ad6837 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -113,9 +113,9 @@ impl crate::View for TextEditDemo { #[cfg(test)] mod tests { - use egui::{accesskit, CentralPanel}; - use egui_kittest::kittest::{Key, Queryable as _}; + use egui::{CentralPanel, Key, Modifiers, accesskit}; use egui_kittest::Harness; + use egui_kittest::kittest::Queryable as _; #[test] pub fn should_type() { @@ -133,8 +133,9 @@ mod tests { let text_edit = harness.get_by_role(accesskit::Role::TextInput); assert_eq!(text_edit.value().as_deref(), Some("Hello, world!")); + text_edit.focus(); - text_edit.key_combination(&[Key::Command, Key::A]); + harness.key_press_modifiers(Modifiers::COMMAND, Key::A); text_edit.type_text("Hi "); harness.run(); diff --git a/crates/egui_demo_lib/src/demo/undo_redo.rs b/crates/egui_demo_lib/src/demo/undo_redo.rs index 525e26c6f..04610031c 100644 --- a/crates/egui_demo_lib/src/demo/undo_redo.rs +++ b/crates/egui_demo_lib/src/demo/undo_redo.rs @@ -1,4 +1,4 @@ -use egui::{util::undoer::Undoer, Button}; +use egui::{Button, util::undoer::Undoer}; #[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 31f5d279a..214646d49 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -319,16 +319,27 @@ mod tests { date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), ..Default::default() }; - let mut harness = Harness::builder() - .with_pixels_per_point(2.0) - .with_size(Vec2::new(380.0, 550.0)) - .build_ui(|ui| { - egui_extras::install_image_loaders(ui.ctx()); - demo.ui(ui); - }); - harness.fit_contents(); + for pixels_per_point in [1, 2] { + for theme in [egui::Theme::Light, egui::Theme::Dark] { + let mut harness = Harness::builder() + .with_pixels_per_point(pixels_per_point as f32) + .with_theme(theme) + .with_size(Vec2::new(380.0, 550.0)) + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + demo.ui(ui); + }); - harness.snapshot("widget_gallery"); + harness.fit_contents(); + + let theme_name = match theme { + egui::Theme::Light => "light", + egui::Theme::Dark => "dark", + }; + let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}"); + harness.snapshot(&image_name); + } + } } } diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 292e5f0aa..d17385a68 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -1,5 +1,5 @@ use egui::{ - text::CCursorRange, Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui, + Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui, text::CCursorRange, }; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs index 17d3858f7..13ff7da03 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs @@ -1,7 +1,7 @@ use super::easy_mark_parser as easy_mark; use egui::{ - vec2, Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle, - Ui, + Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle, Ui, + vec2, }; /// Parse and display a VERY simple and small subset of Markdown. diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 41de808d2..18f5154f8 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use egui::{ - emath::GuiRounding as _, epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2, - Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke, - TextureHandle, TextureOptions, Ui, Vec2, + Align2, Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, + Stroke, TextureHandle, TextureOptions, Ui, Vec2, emath::GuiRounding as _, epaint, lerp, pos2, + vec2, widgets::color_picker::show_color, }; const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0); @@ -159,7 +159,7 @@ impl ColorTest { ui.separator(); // TODO(emilk): test color multiplication (image tint), - // to make sure vertex and texture color multiplication is done in linear space. + // to make sure vertex and texture color multiplication is done in gamma space. ui.label("Gamma interpolation:"); self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); @@ -191,8 +191,8 @@ impl ColorTest { ui.separator(); - ui.label("Linear interpolation (texture sampling):"); - self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear); + ui.label("Texture interpolation (texture sampling) should be in gamma space:"); + self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); } fn show_gradients( @@ -245,11 +245,10 @@ impl ColorTest { let g = Gradient::endpoints(left, right); match interpolation { - Interpolation::Linear => { - // texture sampler is sRGBA aware, and should therefore be linear - self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); - } + Interpolation::Linear => {} Interpolation::Gamma => { + self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); + // vertex shader uses gamma self.vertex_gradient( ui, @@ -330,7 +329,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon #[derive(Clone, Copy)] enum Interpolation { + /// egui used to want Linear interpolation for some things, but now we're always in gamma space. + #[expect(unused)] Linear, + Gamma, } @@ -722,8 +724,8 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { #[cfg(test)] mod tests { use crate::ColorTest; - use egui_kittest::kittest::Queryable as _; use egui_kittest::SnapshotResults; + use egui_kittest::kittest::Queryable as _; #[test] pub fn rendering_test() { @@ -737,14 +739,15 @@ mod tests { }); { - // Expand color-test collapsing header - harness.get_by_label("Color test").click(); + // Expand color-test collapsing header. We accesskit-click since collapsing header + // might not be on screen at this point. + harness.get_by_label("Color test").click_accesskit(); harness.run(); } harness.fit_contents(); - results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"))); + results.add(harness.try_snapshot(format!("rendering_test/dpi_{dpi:.2}"))); } } } diff --git a/crates/egui_demo_lib/tests/image_blending.rs b/crates/egui_demo_lib/tests/image_blending.rs new file mode 100644 index 000000000..c8e5775a8 --- /dev/null +++ b/crates/egui_demo_lib/tests/image_blending.rs @@ -0,0 +1,25 @@ +use egui::{hex_color, include_image}; +use egui_kittest::Harness; + +#[test] +fn test_image_blending() { + for pixels_per_point in [1.0, 2.0] { + let mut harness = Harness::builder() + .with_pixels_per_point(pixels_per_point) + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + egui::Frame::new() + .fill(hex_color!("#5981FF")) + .show(ui, |ui| { + ui.add( + egui::Image::new(include_image!("../data/ring.png")) + .max_height(18.0) + .tint(egui::Color32::GRAY), + ); + }); + }); + harness.run(); + harness.fit_contents(); + harness.snapshot(format!("image_blending/image_x{pixels_per_point}")); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 8bceea77e..0bf7d928c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbe9f58cce2466360b4b93b03afaaee36711b3017ddff1b2b56bfe49ea91a076 -size 31306 +oid sha256:13262df01a7f2cd5655b8b0bb9379ae02a851c877314375f047a7d749908125c +size 31368 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index ec9510008..449c88683 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4f807098e0bc56eaacabb76d646a76036cc66a7a6e54b1c934fa9fecb5b0170 -size 26470 +oid sha256:27d5aa7b7e6bd5f59c1765e98ca4588545284456e4cc255799ea797950e09850 +size 26461 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 64c6b76ec..41d3995db 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b999914adab3d44c614bdf3b28abd268a4ff6162c5680b43035b3f71cb69bb -size 23999 +oid sha256:6d5f3129e34e22b15245212904e0a3537a0c7e70f1d35fd3e9c784af707038b5 +size 24018 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index 394bea644..7dbb397fa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816 -size 99087 +oid sha256:5d05c74583024825d82f1fe8dbeb2a793e366016e87a639f51d46945831de82a +size 99106 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png index 91548c427..aca535ad1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fc9e2ec3253a30ac9649995b019b6b23d745dba07a327886f574a15c0e99e84 -size 50082 +oid sha256:e0a49139611dd5f4e97874e8f7b0e12b649da5f373ff7ee80a7ff678f7f8ecc7 +size 50321 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png index 857cd2d6c..0e2bdbf80 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3110fab8444cb41dffe8b27277fa5dafd0d335aaf13dca511bcccc8b53fb25c8 -size 24046 +oid sha256:17f7065c47712f140e4a9fd9eed61a7118fe12cd79cf0745642a02921eaa596b +size 24065 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 9953ac6c1..e87c842a1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df1e4a1e355100056713e751a8979d4201d0e4aab5513ba2f7a3e4852e1347dd -size 264340 +oid sha256:cfc5dd77728ee0b3d319c5851698305851b6713eb054a6eb5b618e9670f58ae5 +size 277018 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index ea8f9c857..760c84e8f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e -size 35121 +oid sha256:aabc0e3821a2d9b21708e9b8d9be02ad55055ccabe719a93af921dba2384b4b3 +size 34297 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 49b223e7d..f13fc54db 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88 -size 179653 +oid sha256:1bd15215f3ec1b365b8c51987f629d5653e4f40e84c34756aea0dc863af27c1e +size 179906 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 92e94b78f..c26e7e4f6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301 -size 115320 +oid sha256:7e80bf8c79e6e431806c85385a0bd9262796efc0a1e74d431a1b896dde0b8651 +size 115338 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index 723bb5995..462a40ad9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90d56d40004f61628e3f66cfac817c426cd18eb4b9c69ea1b3a6fe5e75e3f05 -size 70354 +oid sha256:a3f8873c9cfb80ddeb1ccc0fa04c1c84ea936db1685238f5d29ee6e89f55e457 +size 68814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 53d6c8a3d..b3dbb2ea1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4d6a15094eee5d96a8af5c44ea9d0c962d650ee9b867344c86d1229e526dcb5 -size 12822 +oid sha256:26e4828e42f54da24d032f384f8829e42bcebaee072923f277db582f84302911 +size 12847 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index a45e2be68..217419e00 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02abc0cbab97e572218f422f4b167957869d4e2b4b388355444c20148d998015 -size 35200 +oid sha256:4a4520aa68d6752992fd2f87090a317e6e5e24b5cdb5ee2e82daf07f9471ca80 +size 35251 diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png new file mode 100644 index 000000000..7ef5676bb --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e057c0bba4ec4c30e890c39153bd6dd17c511f410bfb894e66ef3ef9973d8fd4 +size 807 diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png new file mode 100644 index 000000000..89fad98f9 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8b573f58a41efe26a0bf335e27cc123ffd4c13b24576e46d96ddedfed68b606 +size 2027 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index 51b8d8540..200de9835 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f6cf5b14056522d06f0cb1e56bafd7e5ab7a9033eb358748d43d748bb0ceef1 -size 553177 +oid sha256:39bd11647241521c0ad5c7163a1af4f1aa86792018657091a2d47bb7f2c48b47 +size 598408 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index 3e73d0abb..ea9298ad6 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd3bd1f64995db34a14dbc860ae8b8e269073ed7b8f10d10ce8f99b613cfc999 -size 769357 +oid sha256:080a59163ab60d60738cfab5afac7cfbddfb585d8a0ee7d03540924b458badea +size 833822 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 4b9a5194e..86ec338c7 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f12e6145f3a1c3fda6dede3daeb0e52ed2bffb35531d823133224a477798a14a -size 907800 +oid sha256:216d3d028f48f4bfbd6aca0a25500655d4eb4d5581a387397ed0b048d57fc0c3 +size 984737 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index c5f324368..3b239324a 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05bdcfd2c34b6d7badede14f5495dce34e5e9cfe421314f40dcea15e9f865736 -size 1024735 +oid sha256:399fc3d64e7ff637425effe6c80d684be1cf8bb9b555458352d0ae6c62bddb5a +size 1109505 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 8e4481d06..8d4a1b365 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8365c89f6b823f01464a9310bab7717bf25305b335cdeecf21711c7dca9f053f -size 1140082 +oid sha256:30ce4874a1adb8acf8c8e404f3e19e683eca3133cdef15befbc50d6c09618094 +size 1241773 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index de8c8b321..854ee6b29 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b38021057ec6b5bb39c41bd4afaf5e9ff38687216d52d5bba8cbf7b6fdfe9a4f -size 1291518 +oid sha256:135fbe5f4ee485ee671931b64c16410b9073d40bedb23dc2545fc863448e8c63 +size 1398091 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 2fdbaff3d..852bc6bb2 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac90da596084a880487035b276177e98d711854143373d59860f01733b1c0cd -size 45592 +oid sha256:1b0fe7aa33506c59142aff764c6b229e8c55b35c8038312b22ea2659987a081a +size 45578 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 5eb8bf536..49ba9ad07 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e412d424aac7b9cbdfdb8e36bd598e6cbc77183da7733c94c5f20e70699b8b4a -size 87263 +oid sha256:3a3512ea7235640db79e86aa84039621784270201c9213c910f15e7451e5600b +size 87336 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index e9e1a078d..6130a530e 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:222a32da21c69ee46e847e29fb05fd5e1d2de6bb7a22358549bc426f8243fdcb -size 119671 +oid sha256:dc4918a534f26b72d42ef20221e26c0f91a0252870a1987c1fe4cc4aa3c74872 +size 119406 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index a08a658eb..7969d6bee 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d42e11f50a9522dd5ae73e8f8336bfb01493751705055a63abea3f5258f7c9c1 -size 51626 +oid sha256:71182570a65693839fd7cd7390025731ab3f3f88ab55bc67d8be6466fe5a2c11 +size 51843 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index 677783cc5..49141c40d 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b567d4038fd73986c80d2bd12197a6df037fde043545993fa9fe4160d0af446c -size 54829 +oid sha256:a0dc0294f990730da34fcbbc53f44280306ec6179826c20a6c9ee946e1148b61 +size 55042 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index 7816cfdb0..e8f61ae77 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf40a1f56a6e280002719c6556fe477c93fa7fe88d398372ed36efaa1b83a62 -size 55282 +oid sha256:3004adfe5a864bdc768ceb42d1f09b6debcaf5413f8fea4d36b0aff99e4584f9 +size 55511 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 6005e865a..139648c38 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33621731155ebb463fb01ea41ab20272885250efcd7d5c7683c10936b296e14d -size 36446 +oid sha256:b99360833f59a212a965a13d52485ab8ad0e6420b9288b2d6936507067c22a85 +size 36395 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 713e01fcc..10ad7603b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:186bd8a3146ad8f1977955e3f7fa593877ad1bf1e8376d32f446c67f36a2aafe -size 36493 +oid sha256:82aa004f668f0ac6b493717b4bff8436ccc1e991c7fb3fcde5b5f3a123c06b9f +size 36428 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png deleted file mode 100644 index bcb09fe26..000000000 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07 -size 153136 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png new file mode 100644 index 000000000..9cd2d630e --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e21bb01ae6e4226402a97b7086b49604cdde6b41a6770199df68dc940cd9a45 +size 64748 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png new file mode 100644 index 000000000..f881f639c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0626bc45888ad250bf4b49c7f7f462a93ab91e3a2817fd7d0902411043c97132 +size 153289 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png new file mode 100644 index 000000000..5b88cc531 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:919a82c95468300bcd09471eb31d53d25d50cdcb02c27ddbc759d24e65da92b6 +size 59398 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png new file mode 100644 index 000000000..a1971cad6 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a55e39a640b0e2cc992286a86dcf38460f1abcc7b964df9022549ca1a94c4df5 +size 146408 diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index bfadf84fb..2c2ae8ba4 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,29 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.32.0 - 2025-07-10 - Improved SVG support +### ⭐ Added +* Allow loading multi-MIME formats using the image_loader [#5769](https://github.com/emilk/egui/pull/5769) by [@MYDIH](https://github.com/MYDIH) +* Make ImageLoader use background thread [#5394](https://github.com/emilk/egui/pull/5394) by [@bircni](https://github.com/bircni) +* Add overline option for Table rows [#5637](https://github.com/emilk/egui/pull/5637) by [@akx](https://github.com/akx) +* Support text in SVGs [#5979](https://github.com/emilk/egui/pull/5979) by [@cernec1999](https://github.com/cernec1999) +* Enable setting DatePickerButton start and end year explicitly [#7061](https://github.com/emilk/egui/pull/7061) by [@zachbateman](https://github.com/zachbateman) +* Support custom syntect settings in syntax highlighter [#7084](https://github.com/emilk/egui/pull/7084) by [@mkeeter](https://github.com/mkeeter) + +### 🔧 Changed +* Use enum-map serde feature only when serde is enabled [#5748](https://github.com/emilk/egui/pull/5748) by [@tyssyt](https://github.com/tyssyt) +* Better define the meaning of `SizeHint` [#7079](https://github.com/emilk/egui/pull/7079) by [@emilk](https://github.com/emilk) + +### 🔥 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) + +### 🐛 Fixed +* Refactor MIME type support detection in image loader to allow for deferred handling and appended encoding info [#5686](https://github.com/emilk/egui/pull/5686) by [@markusdd](https://github.com/markusdd) +* Fix incorrect color fringe colors on SVG:s [#7069](https://github.com/emilk/egui/pull/7069) by [@emilk](https://github.com/emilk) +* Fix sometimes blurry SVGs [#7071](https://github.com/emilk/egui/pull/7071) by [@emilk](https://github.com/emilk) +* Fix crash in `egui_extras::FileLoader` after `forget_image` [#6995](https://github.com/emilk/egui/pull/6995) by [@bircni](https://github.com/bircni) + + ## 0.31.1 - 2025-03-05 * Fix image_loader for animated image types [#5688](https://github.com/emilk/egui/pull/5688) by [@BSteffaniak](https://github.com/BSteffaniak) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 601c16f67..0ec0680f8 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -1,6 +1,7 @@ use super::popup::DatePickerPopup; use chrono::NaiveDate; use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget}; +use std::ops::RangeInclusive; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -19,6 +20,7 @@ pub struct DatePickerButton<'a> { show_icon: bool, format: String, highlight_weekends: bool, + start_end_years: Option>, } impl<'a> DatePickerButton<'a> { @@ -33,6 +35,7 @@ impl<'a> DatePickerButton<'a> { show_icon: true, format: "%Y-%m-%d".to_owned(), highlight_weekends: true, + start_end_years: None, } } @@ -101,6 +104,17 @@ impl<'a> DatePickerButton<'a> { self.highlight_weekends = highlight_weekends; self } + + /// Set the start and end years for the date picker. (Default: today's year - 100 to today's year + 10) + /// This will limit the years you can choose from in the dropdown to the specified range. + /// + /// For example, if you want to provide the range of years from 2000 to 2035, you can use: + /// `start_end_years(2000..=2035)`. + #[inline] + pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { + self.start_end_years = Some(start_end_years); + self + } } impl Widget for DatePickerButton<'_> { @@ -167,6 +181,7 @@ impl Widget for DatePickerButton<'_> { calendar: self.calendar, calendar_week: self.calendar_week, highlight_weekends: self.highlight_weekends, + start_end_years: self.start_end_years, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 79f3d37f6..c63de3a91 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -35,6 +35,7 @@ pub(crate) struct DatePickerPopup<'a> { pub calendar: bool, pub calendar_week: bool, pub highlight_weekends: bool, + pub start_end_years: Option>, } impl DatePickerPopup<'_> { @@ -84,7 +85,11 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_year") .selected_text(popup_state.year.to_string()) .show_ui(ui, |ui| { - for year in today.year() - 100..today.year() + 10 { + let (start_year, end_year) = match &self.start_end_years { + Some(range) => (*range.start(), *range.end()), + None => (today.year() - 100, today.year() + 10), + }; + for year in start_year..=end_year { if ui .selectable_value( &mut popup_state.year, diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 0cd751b52..b27362ddb 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -162,7 +162,7 @@ impl<'l> StripLayout<'l> { } else if flags.clip { max_rect } else { - max_rect.union(used_rect) + max_rect | used_rect }; self.set_pos(allocation_rect); diff --git a/crates/egui_extras/src/loaders/ehttp_loader.rs b/crates/egui_extras/src/loaders/ehttp_loader.rs index 22785eedb..abe5d96f1 100644 --- a/crates/egui_extras/src/loaders/ehttp_loader.rs +++ b/crates/egui_extras/src/loaders/ehttp_loader.rs @@ -19,13 +19,13 @@ impl File { return Err(format!( "failed to load {uri:?}: {} {} {response_text}", response.status, response.status_text - )) + )); } None => { return Err(format!( "failed to load {uri:?}: {} {}", response.status, response.status_text - )) + )); } } } diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 9feaebf56..001e988c2 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -95,10 +95,15 @@ impl BytesLoader for FileLoader { } Err(err) => Err(err.to_string()), }; - let prev = cache.lock().insert(uri.clone(), Poll::Ready(result)); - assert!(matches!(prev, Some(Poll::Pending)), "unexpected state"); - ctx.request_repaint(); - log::trace!("finished loading {uri:?}"); + let mut cache = cache.lock(); + if let std::collections::hash_map::Entry::Occupied(mut entry) = cache.entry(uri.clone()) { + let entry = entry.get_mut(); + *entry = Poll::Ready(result); + ctx.request_repaint(); + log::trace!("Finished loading {uri:?}"); + } else { + log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading."); + } } }) .expect("failed to spawn thread"); diff --git a/crates/egui_extras/src/loaders/gif_loader.rs b/crates/egui_extras/src/loaders/gif_loader.rs index a92cbc33e..9f0786cfb 100644 --- a/crates/egui_extras/src/loaders/gif_loader.rs +++ b/crates/egui_extras/src/loaders/gif_loader.rs @@ -1,9 +1,8 @@ use ahash::HashMap; use egui::{ - decode_animated_image_uri, has_gif_magic_header, + ColorImage, FrameDurations, Id, decode_animated_image_uri, has_gif_magic_header, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, - ColorImage, FrameDurations, Id, }; use image::AnimationDecoder as _; use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 7f472dcc4..18e1e483b 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -1,9 +1,8 @@ use ahash::HashMap; use egui::{ - decode_animated_image_uri, + ColorImage, decode_animated_image_uri, load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, - ColorImage, }; use image::ImageFormat; use std::{mem::size_of, path::Path, sync::Arc, task::Poll}; diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index fab70151f..4b778ff9e 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -1,17 +1,17 @@ use std::{ mem::size_of, sync::{ - atomic::{AtomicU64, Ordering::Relaxed}, Arc, + atomic::{AtomicU64, Ordering::Relaxed}, }, }; use ahash::HashMap; use egui::{ + ColorImage, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, - ColorImage, }; struct Entry { diff --git a/crates/egui_extras/src/loaders/webp_loader.rs b/crates/egui_extras/src/loaders/webp_loader.rs index ef1a5d527..f0dc32ae4 100644 --- a/crates/egui_extras/src/loaders/webp_loader.rs +++ b/crates/egui_extras/src/loaders/webp_loader.rs @@ -1,11 +1,10 @@ use ahash::HashMap; use egui::{ - decode_animated_image_uri, has_webp_header, + ColorImage, FrameDurations, Id, decode_animated_image_uri, has_webp_header, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, - ColorImage, FrameDurations, Id, }; -use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba}; +use image::{AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba, codecs::webp::WebPDecoder}; use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; #[derive(Clone)] @@ -55,7 +54,7 @@ impl WebP { unreachable => { return Err(format!( "Unreachable WebP color type, expected Rgb8/Rgba8, got {unreachable:?}" - )) + )); } }; diff --git a/crates/egui_extras/src/strip.rs b/crates/egui_extras/src/strip.rs index 00fc65774..da64b4f9e 100644 --- a/crates/egui_extras/src/strip.rs +++ b/crates/egui_extras/src/strip.rs @@ -1,7 +1,7 @@ use crate::{ + Size, layout::{CellDirection, CellSize, StripLayout, StripLayoutFlags}, sizing::Sizing, - Size, }; use egui::{Response, Ui}; diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 8d688ae8e..894f2cc24 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -5,8 +5,8 @@ #![allow(clippy::mem_forget)] // False positive from enum_map macro -use egui::text::LayoutJob; use egui::TextStyle; +use egui::text::LayoutJob; /// View some code with syntax highlighting and selection. pub fn code_view_ui( @@ -28,18 +28,65 @@ pub fn highlight( theme: &CodeTheme, code: &str, language: &str, +) -> LayoutJob { + highlight_inner(ctx, style, theme, code, language, None) +} + +/// Add syntax highlighting to a code string, with custom `syntect` settings +/// +/// The results are memoized, so you can call this every frame without performance penalty. +/// +/// The `syntect` settings are memoized by *address*, so a stable reference should +/// be used to avoid unnecessary recomputation. +#[cfg(feature = "syntect")] +pub fn highlight_with( + ctx: &egui::Context, + style: &egui::Style, + theme: &CodeTheme, + code: &str, + language: &str, + settings: &SyntectSettings, +) -> LayoutJob { + highlight_inner( + ctx, + style, + theme, + code, + language, + Some(HighlightSettings(settings)), + ) +} + +fn highlight_inner( + ctx: &egui::Context, + style: &egui::Style, + theme: &CodeTheme, + code: &str, + language: &str, + settings: Option>, ) -> LayoutJob { // We take in both context and style so that in situations where ui is not available such as when // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used #[expect(non_local_definitions)] - impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { + impl + egui::cache::ComputerMut< + (&egui::FontId, &CodeTheme, &str, &str, HighlightSettings<'_>), + LayoutJob, + > for Highlighter + { fn compute( &mut self, - (font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str), + (font_id, theme, code, lang, settings): ( + &egui::FontId, + &CodeTheme, + &str, + &str, + HighlightSettings<'_>, + ), ) -> LayoutJob { - self.highlight(font_id.clone(), theme, code, lang) + Self::highlight(font_id.clone(), theme, code, lang, settings) } } @@ -50,10 +97,27 @@ pub fn highlight( .clone() .unwrap_or_else(|| TextStyle::Monospace.resolve(style)); + // Private type, so that users can't interfere with it in the `IdTypeMap` + #[cfg(feature = "syntect")] + #[derive(Clone, Default)] + struct PrivateSettings(std::sync::Arc); + + // Dummy private settings, to minimize code changes without `syntect` + #[cfg(not(feature = "syntect"))] + #[derive(Clone, Default)] + struct PrivateSettings(std::sync::Arc<()>); + ctx.memory_mut(|mem| { + let settings = settings.unwrap_or_else(|| { + HighlightSettings( + &mem.data + .get_temp_mut_or_default::(egui::Id::NULL) + .0, + ) + }); mem.caches .cache::() - .get((&font_id, theme, code, language)) + .get((&font_id, theme, code, language, settings)) }) } @@ -396,13 +460,13 @@ impl CodeTheme { // ---------------------------------------------------------------------------- #[cfg(feature = "syntect")] -struct Highlighter { - ps: syntect::parsing::SyntaxSet, - ts: syntect::highlighting::ThemeSet, +pub struct SyntectSettings { + pub ps: syntect::parsing::SyntaxSet, + pub ts: syntect::highlighting::ThemeSet, } #[cfg(feature = "syntect")] -impl Default for Highlighter { +impl Default for SyntectSettings { fn default() -> Self { profiling::function_scope!(); Self { @@ -412,15 +476,33 @@ impl Default for Highlighter { } } +/// Highlight settings are memoized by reference address, rather than value +#[cfg(feature = "syntect")] +#[derive(Copy, Clone)] +struct HighlightSettings<'a>(&'a SyntectSettings); + +#[cfg(not(feature = "syntect"))] +#[derive(Copy, Clone)] +struct HighlightSettings<'a>(&'a ()); + +impl std::hash::Hash for HighlightSettings<'_> { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.0, state); + } +} + +#[derive(Default)] +struct Highlighter; + impl Highlighter { fn highlight( - &self, font_id: egui::FontId, theme: &CodeTheme, code: &str, lang: &str, + settings: HighlightSettings<'_>, ) -> LayoutJob { - self.highlight_impl(theme, code, lang).unwrap_or_else(|| { + Self::highlight_impl(theme, code, lang, settings).unwrap_or_else(|| { // Fallback: LayoutJob::simple( code.into(), @@ -436,19 +518,25 @@ impl Highlighter { } #[cfg(feature = "syntect")] - fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { + fn highlight_impl( + theme: &CodeTheme, + text: &str, + language: &str, + highlighter: HighlightSettings<'_>, + ) -> Option { profiling::function_scope!(); use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; - let syntax = self + let syntax = highlighter + .0 .ps .find_syntax_by_name(language) - .or_else(|| self.ps.find_syntax_by_extension(language))?; + .or_else(|| highlighter.0.ps.find_syntax_by_extension(language))?; let syn_theme = theme.syntect_theme.syntect_key_name(); - let mut h = HighlightLines::new(syntax, &self.ts.themes[syn_theme]); + let mut h = HighlightLines::new(syntax, &highlighter.0.ts.themes[syn_theme]); use egui::text::{LayoutSection, TextFormat}; @@ -458,7 +546,7 @@ impl Highlighter { }; for line in LinesWithEndings::from(text) { - for (style, range) in h.highlight_line(line, &self.ps).ok()? { + for (style, range) in h.highlight_line(line, &highlighter.0.ps).ok()? { let fg = style.foreground; let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b); let italics = style.font_style.contains(FontStyle::ITALIC); @@ -505,18 +593,13 @@ fn as_byte_range(whole: &str, range: &str) -> std::ops::Range { // ---------------------------------------------------------------------------- -#[cfg(not(feature = "syntect"))] -#[derive(Default)] -struct Highlighter {} - #[cfg(not(feature = "syntect"))] impl Highlighter { - #[expect(clippy::unused_self)] fn highlight_impl( - &self, theme: &CodeTheme, mut text: &str, language: &str, + _settings: HighlightSettings<'_>, ) -> Option { profiling::function_scope!(); diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 2d64f2570..9d77c60c8 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -4,13 +4,13 @@ //! Takes all available height, so if you want something below the table, put it in a strip. use egui::{ - scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource}, Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b, + scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource}, }; use crate::{ - layout::{CellDirection, CellSize, StripLayoutFlags}, StripLayout, + layout::{CellDirection, CellSize, StripLayoutFlags}, }; // -----------------------------------------------------------------=---------- diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 86c77c942..4969b5569 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,11 @@ Changes since the last release can be found at {{ - $crate::check_for_gl_error_impl($gl, file!(), line!(), "") - }}; - ($gl: expr, $context: literal) => {{ - $crate::check_for_gl_error_impl($gl, file!(), line!(), $context) - }}; + ($gl: expr) => {{ $crate::check_for_gl_error_impl($gl, file!(), line!(), "") }}; + ($gl: expr, $context: literal) => {{ $crate::check_for_gl_error_impl($gl, file!(), line!(), $context) }}; } #[doc(hidden)] diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 0646b560d..98fe25a45 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -172,12 +172,7 @@ impl Painter { let supported_extensions = gl.supported_extensions(); log::trace!("OpenGL extensions: {supported_extensions:?}"); - let srgb_textures = shader_version == ShaderVersion::Es300 // WebGL2 always support sRGB - || supported_extensions.iter().any(|extension| { - // EXT_sRGB, GL_ARB_framebuffer_sRGB, GL_EXT_sRGB, GL_EXT_texture_sRGB_decode, … - extension.contains("sRGB") - }); - log::debug!("SRGB texture Support: {:?}", srgb_textures); + let srgb_textures = false; // egui wants normal sRGB-unaware textures let supports_srgb_framebuffer = !cfg!(target_arch = "wasm32") && supported_extensions.iter().any(|extension| { @@ -202,11 +197,10 @@ impl Painter { &gl, glow::FRAGMENT_SHADER, &format!( - "{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n#define SRGB_TEXTURES {}\n{}\n{}", + "{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n{}\n{}", shader_version_declaration, shader_version.is_new_shader_interface() as i32, dithering as i32, - srgb_textures as i32, shader_prefix, FRAG_SRC ), @@ -445,7 +439,9 @@ impl Painter { if let Some(callback) = callback.callback.downcast_ref::() { (callback.f)(info, self); } else { - log::warn!("Warning: Unsupported render callback. Expected egui_glow::CallbackFn"); + log::warn!( + "Warning: Unsupported render callback. Expected egui_glow::CallbackFn" + ); } check_for_gl_error!(&self.gl, "callback"); @@ -532,23 +528,6 @@ impl Painter { self.upload_texture_srgb(delta.pos, image.size, delta.options, data); } - egui::ImageData::Font(image) => { - assert_eq!( - image.width() * image.height(), - image.pixels.len(), - "Mismatch between texture size and texel count" - ); - - let data: Vec = { - profiling::scope!("font -> sRGBA"); - image - .srgba_pixels(None) - .flat_map(|a| a.to_array()) - .collect() - }; - - self.upload_texture_srgb(delta.pos, image.size, delta.options, &data); - } }; } diff --git a/crates/egui_glow/src/shader/fragment.glsl b/crates/egui_glow/src/shader/fragment.glsl index f2792ed04..07a931b53 100644 --- a/crates/egui_glow/src/shader/fragment.glsl +++ b/crates/egui_glow/src/shader/fragment.glsl @@ -43,25 +43,8 @@ vec3 dither_interleaved(vec3 rgb, float levels) { return rgb + noise / (levels - 1.0); } -// 0-1 sRGB gamma from 0-1 linear -vec3 srgb_gamma_from_linear(vec3 rgb) { - bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); - vec3 lower = rgb * vec3(12.92); - vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); - return mix(higher, lower, vec3(cutoff)); -} - -// 0-1 sRGBA gamma from 0-1 linear -vec4 srgba_gamma_from_linear(vec4 rgba) { - return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a); -} - void main() { -#if SRGB_TEXTURES - vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc)); -#else vec4 texture_in_gamma = texture2D(u_sampler, v_tc); -#endif // We multiply the colors in gamma space, because that's the only way to get text to look right. vec4 frag_color_gamma = v_rgba_in_gamma * texture_in_gamma; diff --git a/crates/egui_glow/src/shader_version.rs b/crates/egui_glow/src/shader_version.rs index 249cda369..b655c567c 100644 --- a/crates/egui_glow/src/shader_version.rs +++ b/crates/egui_glow/src/shader_version.rs @@ -47,11 +47,7 @@ impl ShaderVersion { .try_into() .unwrap(); if es { - if maj >= 3 { - Self::Es300 - } else { - Self::Es100 - } + if maj >= 3 { Self::Es300 } else { Self::Es100 } } else if maj > 1 || (maj == 1 && min >= 40) { Self::Gl140 } else { diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index c4569015a..2fe15dcd0 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -1,8 +1,8 @@ use ahash::HashSet; use egui::{ViewportId, ViewportOutput}; pub use egui_winit; -use egui_winit::winit; pub use egui_winit::EventResponse; +use egui_winit::winit; use crate::shader_version::ShaderVersion; diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md index 547abd06e..b4beedf7e 100644 --- a/crates/egui_kittest/CHANGELOG.md +++ b/crates/egui_kittest/CHANGELOG.md @@ -6,6 +6,19 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.32.0 - 2025-07-10 +### ⭐ Added +* Add `ImageLoader::has_pending` and `wait_for_pending_images` [#7030](https://github.com/emilk/egui/pull/7030) by [@lucasmerlin](https://github.com/lucasmerlin) +* Create custom `egui_kittest::Node` [#7138](https://github.com/emilk/egui/pull/7138) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `HarnessBuilder::theme` [#7289](https://github.com/emilk/egui/pull/7289) 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) +* Add `failed_pixel_count_threshold` [#7092](https://github.com/emilk/egui/pull/7092) by [@bircni](https://github.com/bircni) + +### 🔧 Changed +* More ergonomic functions taking `Impl Into` [#7307](https://github.com/emilk/egui/pull/7307) by [@emlik](https://github.com/emilk) +* Update kittest to 0.2 [#7332](https://github.com/emilk/egui/pull/7332) by [@lucasmerlin](https://github.com/lucasmerlin) + + ## 0.31.1 - 2025-03-05 * Fix modifiers not working in kittest [#5693](https://github.com/emilk/egui/pull/5693) by [@lucasmerlin](https://github.com/lucasmerlin) * Enable all features for egui_kittest docs [#5711](https://github.com/emilk/egui/pull/5711) by [@YgorSouza](https://github.com/YgorSouza) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index ff071c9f4..31a55f618 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -10,7 +10,7 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt ## Example usage ```rust use egui::accesskit::Toggled; -use egui_kittest::{Harness, kittest::Queryable}; +use egui_kittest::{Harness, kittest::{Queryable, NodeT}}; fn main() { let mut checked = false; @@ -21,13 +21,13 @@ fn main() { let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::False)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::False)); checkbox.click(); harness.run(); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::True)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::True)); // Shrink the window size to the smallest size possible harness.fit_contents(); diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 3b10ba37a..021019a89 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -7,6 +7,7 @@ use std::marker::PhantomData; pub struct HarnessBuilder { pub(crate) screen_rect: Rect, pub(crate) pixels_per_point: f32, + pub(crate) theme: egui::Theme, pub(crate) max_steps: u64, pub(crate) step_dt: f32, pub(crate) state: PhantomData, @@ -19,6 +20,7 @@ impl Default for HarnessBuilder { Self { screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), pixels_per_point: 1.0, + theme: egui::Theme::Dark, state: PhantomData, renderer: Box::new(LazyRenderer::default()), max_steps: 4, @@ -45,6 +47,13 @@ impl HarnessBuilder { self } + /// Set the desired theme (dark or light). + #[inline] + pub fn with_theme(mut self, theme: egui::Theme) -> Self { + self.theme = theme; + self + } + /// Set the maximum number of steps to run when calling [`Harness::run`]. /// /// Default is 4. diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs deleted file mode 100644 index e756d4dc9..000000000 --- a/crates/egui_kittest/src/event.rs +++ /dev/null @@ -1,194 +0,0 @@ -use egui::Event::PointerButton; -use egui::{Event, Modifiers, Pos2}; -use kittest::{ElementState, MouseButton, SimulatedEvent}; - -#[derive(Default)] -pub(crate) struct EventState { - last_mouse_pos: Pos2, -} - -impl EventState { - /// Map the kittest event to an egui event, add it to the input and update the modifiers. - /// This function accesses `egui::RawInput::modifiers`. Make sure it is not reset after each - /// frame (Since we use [`egui::RawInput::take`], this should be fine). - pub fn update(&mut self, event: kittest::Event, input: &mut egui::RawInput) { - if let Some(event) = self.kittest_event_to_egui(&mut input.modifiers, event) { - input.events.push(event); - } - } - - fn kittest_event_to_egui( - &mut self, - modifiers: &mut Modifiers, - event: kittest::Event, - ) -> Option { - match event { - kittest::Event::ActionRequest(e) => Some(Event::AccessKitActionRequest(e)), - kittest::Event::Simulated(e) => match e { - SimulatedEvent::CursorMoved { position } => { - self.last_mouse_pos = Pos2::new(position.x as f32, position.y as f32); - Some(Event::PointerMoved(Pos2::new( - position.x as f32, - position.y as f32, - ))) - } - SimulatedEvent::MouseInput { state, button } => { - pointer_button_to_egui(button).map(|button| PointerButton { - button, - modifiers: *modifiers, - pos: self.last_mouse_pos, - pressed: matches!(state, ElementState::Pressed), - }) - } - SimulatedEvent::Ime(text) => Some(Event::Text(text)), - SimulatedEvent::KeyInput { state, key } => { - match key { - kittest::Key::Alt => { - modifiers.alt = matches!(state, ElementState::Pressed); - } - kittest::Key::Command => { - modifiers.command = matches!(state, ElementState::Pressed); - } - kittest::Key::Control => { - modifiers.ctrl = matches!(state, ElementState::Pressed); - } - kittest::Key::Shift => { - modifiers.shift = matches!(state, ElementState::Pressed); - } - _ => {} - } - kittest_key_to_egui(key).map(|key| Event::Key { - key, - modifiers: *modifiers, - pressed: matches!(state, ElementState::Pressed), - repeat: false, - physical_key: None, - }) - } - }, - } - } -} - -fn kittest_key_to_egui(value: kittest::Key) -> Option { - use egui::Key as EKey; - use kittest::Key; - match value { - Key::ArrowDown => Some(EKey::ArrowDown), - Key::ArrowLeft => Some(EKey::ArrowLeft), - Key::ArrowRight => Some(EKey::ArrowRight), - Key::ArrowUp => Some(EKey::ArrowUp), - Key::Escape => Some(EKey::Escape), - Key::Tab => Some(EKey::Tab), - Key::Backspace => Some(EKey::Backspace), - Key::Enter => Some(EKey::Enter), - Key::Space => Some(EKey::Space), - Key::Insert => Some(EKey::Insert), - Key::Delete => Some(EKey::Delete), - Key::Home => Some(EKey::Home), - Key::End => Some(EKey::End), - Key::PageUp => Some(EKey::PageUp), - Key::PageDown => Some(EKey::PageDown), - Key::Copy => Some(EKey::Copy), - Key::Cut => Some(EKey::Cut), - Key::Paste => Some(EKey::Paste), - Key::Colon => Some(EKey::Colon), - Key::Comma => Some(EKey::Comma), - Key::Backslash => Some(EKey::Backslash), - Key::Slash => Some(EKey::Slash), - Key::Pipe => Some(EKey::Pipe), - Key::Questionmark => Some(EKey::Questionmark), - Key::OpenBracket => Some(EKey::OpenBracket), - Key::CloseBracket => Some(EKey::CloseBracket), - Key::Backtick => Some(EKey::Backtick), - Key::Minus => Some(EKey::Minus), - Key::Period => Some(EKey::Period), - Key::Plus => Some(EKey::Plus), - Key::Equals => Some(EKey::Equals), - Key::Semicolon => Some(EKey::Semicolon), - Key::Quote => Some(EKey::Quote), - Key::Num0 => Some(EKey::Num0), - Key::Num1 => Some(EKey::Num1), - Key::Num2 => Some(EKey::Num2), - Key::Num3 => Some(EKey::Num3), - Key::Num4 => Some(EKey::Num4), - Key::Num5 => Some(EKey::Num5), - Key::Num6 => Some(EKey::Num6), - Key::Num7 => Some(EKey::Num7), - Key::Num8 => Some(EKey::Num8), - Key::Num9 => Some(EKey::Num9), - Key::A => Some(EKey::A), - Key::B => Some(EKey::B), - Key::C => Some(EKey::C), - Key::D => Some(EKey::D), - Key::E => Some(EKey::E), - Key::F => Some(EKey::F), - Key::G => Some(EKey::G), - Key::H => Some(EKey::H), - Key::I => Some(EKey::I), - Key::J => Some(EKey::J), - Key::K => Some(EKey::K), - Key::L => Some(EKey::L), - Key::M => Some(EKey::M), - Key::N => Some(EKey::N), - Key::O => Some(EKey::O), - Key::P => Some(EKey::P), - Key::Q => Some(EKey::Q), - Key::R => Some(EKey::R), - Key::S => Some(EKey::S), - Key::T => Some(EKey::T), - Key::U => Some(EKey::U), - Key::V => Some(EKey::V), - Key::W => Some(EKey::W), - Key::X => Some(EKey::X), - Key::Y => Some(EKey::Y), - Key::Z => Some(EKey::Z), - Key::F1 => Some(EKey::F1), - Key::F2 => Some(EKey::F2), - Key::F3 => Some(EKey::F3), - Key::F4 => Some(EKey::F4), - Key::F5 => Some(EKey::F5), - Key::F6 => Some(EKey::F6), - Key::F7 => Some(EKey::F7), - Key::F8 => Some(EKey::F8), - Key::F9 => Some(EKey::F9), - Key::F10 => Some(EKey::F10), - Key::F11 => Some(EKey::F11), - Key::F12 => Some(EKey::F12), - Key::F13 => Some(EKey::F13), - Key::F14 => Some(EKey::F14), - Key::F15 => Some(EKey::F15), - Key::F16 => Some(EKey::F16), - Key::F17 => Some(EKey::F17), - Key::F18 => Some(EKey::F18), - Key::F19 => Some(EKey::F19), - Key::F20 => Some(EKey::F20), - Key::F21 => Some(EKey::F21), - Key::F22 => Some(EKey::F22), - Key::F23 => Some(EKey::F23), - Key::F24 => Some(EKey::F24), - Key::F25 => Some(EKey::F25), - Key::F26 => Some(EKey::F26), - Key::F27 => Some(EKey::F27), - Key::F28 => Some(EKey::F28), - Key::F29 => Some(EKey::F29), - Key::F30 => Some(EKey::F30), - Key::F31 => Some(EKey::F31), - Key::F32 => Some(EKey::F32), - Key::F33 => Some(EKey::F33), - Key::F34 => Some(EKey::F34), - Key::F35 => Some(EKey::F35), - _ => None, - } -} - -fn pointer_button_to_egui(value: MouseButton) -> Option { - match value { - MouseButton::Left => Some(egui::PointerButton::Primary), - MouseButton::Right => Some(egui::PointerButton::Secondary), - MouseButton::Middle => Some(egui::PointerButton::Middle), - MouseButton::Back => Some(egui::PointerButton::Extra1), - MouseButton::Forward => Some(egui::PointerButton::Extra2), - MouseButton::Other(_) => None, - } -} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 0963a9491..6adefe53b 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -4,7 +4,6 @@ #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] mod builder; -mod event; #[cfg(feature = "snapshot")] mod snapshot; @@ -14,6 +13,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::time::Duration; mod app_kind; +mod node; mod renderer; #[cfg(feature = "wgpu")] mod texture_to_image; @@ -23,13 +23,14 @@ pub mod wgpu; pub use kittest; use crate::app_kind::AppKind; -use crate::event::EventState; pub use builder::*; +pub use node::*; pub use renderer::*; -use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; -use kittest::{Node, Queryable}; +use egui::style::ScrollAnimation; +use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; +use kittest::Queryable; #[derive(Debug, Clone)] pub struct ExceededMaxStepsError { @@ -55,19 +56,23 @@ impl Display for ExceededMaxStepsError { /// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure. /// In _most cases_ it should be fine to just store the state in the closure itself. /// The state functions are useful if you need to access the state after the harness has been created. +/// +/// Some egui style options are changed from the defaults: +/// - The cursor blinking is disabled +/// - The scroll animation is disabled pub struct Harness<'a, State = ()> { pub ctx: egui::Context, input: egui::RawInput, kittest: kittest::State, output: egui::FullOutput, app: AppKind<'a, State>, - event_state: EventState, response: Option, state: State, renderer: Box, max_steps: u64, step_dt: f32, wait_for_pending_images: bool, + queued_events: EventQueue, } impl Debug for Harness<'_, State> { @@ -86,6 +91,7 @@ impl<'a, State> Harness<'a, State> { let HarnessBuilder { screen_rect, pixels_per_point, + theme, max_steps, step_dt, state: _, @@ -93,9 +99,14 @@ impl<'a, State> Harness<'a, State> { wait_for_pending_images, } = builder; let ctx = ctx.unwrap_or_default(); + ctx.set_theme(theme); ctx.enable_accesskit(); - // Disable cursor blinking so it doesn't interfere with snapshots - ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false); + ctx.all_styles_mut(|style| { + // Disable cursor blinking so it doesn't interfere with snapshots + style.visuals.text_cursor.blink = false; + style.scroll_animation = ScrollAnimation::none(); + style.animation_time = 0.0; + }); let mut input = egui::RawInput { screen_rect: Some(screen_rect), ..Default::default() @@ -126,12 +137,12 @@ impl<'a, State> Harness<'a, State> { ), output, response, - event_state: EventState::default(), state, renderer, max_steps, step_dt, wait_for_pending_images, + queued_events: Default::default(), }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done harness.run_ok(); @@ -227,12 +238,19 @@ impl<'a, State> Harness<'a, State> { /// This will call the app closure with each queued event and /// update the Harness. pub fn step(&mut self) { - let events = self.kittest.take_events(); + let events = std::mem::take(&mut *self.queued_events.lock()); if events.is_empty() { self._step(false); } for event in events { - self.event_state.update(event, &mut self.input); + match event { + EventType::Event(event) => { + self.input.events.push(event); + } + EventType::Modifiers(modifiers) => { + self.input.modifiers = modifiers; + } + } self._step(false); } } @@ -414,52 +432,128 @@ impl<'a, State> Harness<'a, State> { &mut self.state } - /// Press a key. - /// This will create a key down event and a key up event. - pub fn press_key(&mut self, key: egui::Key) { - self.input.events.push(egui::Event::Key { + fn event(&self, event: egui::Event) { + self.queued_events.lock().push(EventType::Event(event)); + } + + fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) { + let mut queue = self.queued_events.lock(); + queue.push(EventType::Modifiers(modifiers)); + queue.push(EventType::Event(event)); + queue.push(EventType::Modifiers(Modifiers::default())); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queued_events + .lock() + .push(EventType::Modifiers(modifiers)); + } + + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: true, - modifiers: self.input.modifiers, - repeat: false, - physical_key: None, - }); - self.input.events.push(egui::Event::Key { - key, - pressed: false, - modifiers: self.input.modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); } - /// Press a key with modifiers. - /// This will create a key-down event, a key-up event, and update the modifiers. - /// - /// NOTE: In contrast to the event fns on [`Node`], this will call [`Harness::step`], in - /// order to properly update modifiers. - pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) { - // Combine the modifiers with the current modifiers - let previous_modifiers = self.input.modifiers; - self.input.modifiers |= modifiers; - - self.input.events.push(egui::Event::Key { - key, - pressed: true, + pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: true, + modifiers, + repeat: false, + physical_key: None, + }, modifiers, - repeat: false, - physical_key: None, - }); - self.step(); - self.input.events.push(egui::Event::Key { + ); + } + + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: false, - modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); + } - self.input.modifiers = previous_modifiers; + pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: false, + modifiers, + repeat: false, + physical_key: None, + }, + modifiers, + ); + } + + /// Press the given keys in combination. + /// + /// For e.g. [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + pub fn key_combination(&self, keys: &[Key]) { + for key in keys { + self.key_down(*key); + } + for key in keys.iter().rev() { + self.key_up(*key); + } + } + + /// Press the given keys in combination, with modifiers. + /// + /// For e.g. [`Modifiers::COMMAND`] + [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Modifiers::COMMAND`] + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + /// - Release [`Modifiers::COMMAND`] + pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) { + self.modifiers(modifiers); + + for pressed in [true, false] { + for key in keys { + self.event(egui::Event::Key { + key: *key, + pressed, + modifiers, + repeat: false, + physical_key: None, + }); + } + } + + self.modifiers(Modifiers::default()); + } + + /// Press a key. + /// + /// This will create a key down event and a key up event. + pub fn key_press(&self, key: egui::Key) { + self.key_combination(&[key]); + } + + /// Press a key with modifiers. + /// + /// This will + /// - set the modifiers + /// - create a key down event + /// - create a key up event + /// - reset the modifiers + pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.key_combination_modifiers(modifiers, &[key]); } /// Render the last output to an image. @@ -478,6 +572,19 @@ impl<'a, State> Harness<'a, State> { .get(&ViewportId::ROOT) .expect("Missing root viewport") } + + /// The root node of the test harness. + pub fn root(&self) -> Node<'_> { + Node { + accesskit_node: self.kittest.root(), + queue: &self.queued_events, + } + } + + #[deprecated = "Use `Harness::root` instead."] + pub fn node(&self) -> Node<'_> { + self.root() + } } /// Utilities for stateless harnesses. @@ -526,11 +633,11 @@ impl<'a> Harness<'a> { } } -impl<'t, 'n, State> Queryable<'t, 'n> for Harness<'_, State> +impl<'tree, 'node, State> Queryable<'tree, 'node, Node<'tree>> for Harness<'_, State> where - 'n: 't, + 'node: 'tree, { - fn node(&'n self) -> Node<'t> { - self.kittest_state().node() + fn queryable_node(&'node self) -> Node<'tree> { + self.root() } } diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs new file mode 100644 index 000000000..94940ffff --- /dev/null +++ b/crates/egui_kittest/src/node.rs @@ -0,0 +1,207 @@ +use egui::accesskit::ActionRequest; +use egui::mutex::Mutex; +use egui::{Modifiers, PointerButton, Pos2, accesskit}; +use kittest::{AccessKitNode, NodeT, debug_fmt_node}; +use std::fmt::{Debug, Formatter}; + +pub(crate) enum EventType { + Event(egui::Event), + Modifiers(Modifiers), +} + +pub(crate) type EventQueue = Mutex>; + +#[derive(Clone, Copy)] +pub struct Node<'tree> { + pub(crate) accesskit_node: AccessKitNode<'tree>, + pub(crate) queue: &'tree EventQueue, +} + +impl Debug for Node<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + debug_fmt_node(self, f) + } +} + +impl<'tree> NodeT<'tree> for Node<'tree> { + fn accesskit_node(&self) -> AccessKitNode<'tree> { + self.accesskit_node + } + + fn new_related(&self, child_node: AccessKitNode<'tree>) -> Self { + Self { + queue: self.queue, + accesskit_node: child_node, + } + } +} + +impl Node<'_> { + fn event(&self, event: egui::Event) { + self.queue.lock().push(EventType::Event(event)); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queue.lock().push(EventType::Modifiers(modifiers)); + } + + pub fn hover(&self) { + self.event(egui::Event::PointerMoved(self.rect().center())); + } + + /// Click at the node center with the primary button. + pub fn click(&self) { + self.click_button(PointerButton::Primary); + } + + #[deprecated = "Use `click()` instead."] + pub fn simulate_click(&self) { + self.click(); + } + + pub fn click_secondary(&self) { + self.click_button(PointerButton::Secondary); + } + + pub fn click_button(&self, button: PointerButton) { + self.hover(); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers: Modifiers::default(), + }); + } + } + + pub fn click_modifiers(&self, modifiers: Modifiers) { + self.click_button_modifiers(PointerButton::Primary, modifiers); + } + + pub fn click_button_modifiers(&self, button: PointerButton, modifiers: Modifiers) { + self.hover(); + self.modifiers(modifiers); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers, + }); + } + self.modifiers(Modifiers::default()); + } + + /// Click the node via accesskit. + /// + /// This will trigger a [`accesskit::Action::Click`] action. + /// In contrast to `click()`, this can also click widgets that are not currently visible. + pub fn click_accesskit(&self) { + self.event(egui::Event::AccessKitActionRequest( + accesskit::ActionRequest { + target: self.accesskit_node.id(), + action: accesskit::Action::Click, + data: None, + }, + )); + } + + pub fn rect(&self) -> egui::Rect { + let rect = self + .accesskit_node + .bounding_box() + .expect("Every egui node should have a rect"); + egui::Rect { + min: Pos2::new(rect.x0 as f32, rect.y0 as f32), + max: Pos2::new(rect.x1 as f32, rect.y1 as f32), + } + } + + pub fn focus(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::Focus, + target: self.accesskit_node.id(), + data: None, + })); + } + + #[deprecated = "Use `Harness::key_down` instead."] + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: true, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + #[deprecated = "Use `Harness::key_up` instead."] + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: false, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + pub fn type_text(&self, text: &str) { + self.event(egui::Event::Text(text.to_owned())); + } + + pub fn value(&self) -> Option { + self.accesskit_node.value() + } + + pub fn is_focused(&self) -> bool { + self.accesskit_node.is_focused() + } + + /// Scroll the node into view. + pub fn scroll_to_me(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollIntoView, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node down (100px). + pub fn scroll_down(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollDown, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node up (100px). + pub fn scroll_up(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollUp, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node left (100px). + pub fn scroll_left(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollLeft, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node right (100px). + pub fn scroll_right(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollRight, + target: self.accesskit_node.id(), + data: None, + })); + } +} diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index c85a09070..8a457ec52 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -13,16 +13,117 @@ pub struct SnapshotOptions { /// wgpu backends). pub threshold: f32, + /// The number of pixels that can differ before the snapshot is considered a failure. + /// Preferably, you should use `threshold` to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + /// If `None`, the default is `0` (meaning no pixels can differ). + /// If `Some`, the value can be set per OS + pub failed_pixel_count_threshold: usize, + /// The path where the snapshots will be saved. /// The default is `tests/snapshots`. pub output_path: PathBuf, } +/// Helper struct to define the number of pixels that can differ before the snapshot is considered a failure. +/// This is useful if you want to set different thresholds for different operating systems. +/// +/// The default values are 0 / 0.0 +/// +/// Example usage: +/// ```no_run +/// use egui_kittest::{OsThreshold, SnapshotOptions}; +/// let mut harness = egui_kittest::Harness::new_ui(|ui| { +/// ui.label("Hi!"); +/// }); +/// harness.snapshot_options( +/// "os_threshold_example", +/// &SnapshotOptions::new() +/// .threshold(OsThreshold::new(0.0).windows(10.0)) +/// .failed_pixel_count_threshold(OsThreshold::new(0).windows(10).macos(53) +/// )) +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct OsThreshold { + pub windows: T, + pub macos: T, + pub linux: T, + pub fallback: T, +} + +impl From for OsThreshold { + fn from(value: usize) -> Self { + Self::new(value) + } +} + +impl OsThreshold +where + T: Copy, +{ + /// Use the same value for all + pub fn new(same: T) -> Self { + Self { + windows: same, + macos: same, + linux: same, + fallback: same, + } + } + + /// Set the threshold for Windows. + #[inline] + pub fn windows(mut self, threshold: T) -> Self { + self.windows = threshold; + self + } + + /// Set the threshold for macOS. + #[inline] + pub fn macos(mut self, threshold: T) -> Self { + self.macos = threshold; + self + } + + /// Set the threshold for Linux. + #[inline] + pub fn linux(mut self, threshold: T) -> Self { + self.linux = threshold; + self + } + + /// Get the threshold for the current operating system. + pub fn threshold(&self) -> T { + if cfg!(target_os = "windows") { + self.windows + } else if cfg!(target_os = "macos") { + self.macos + } else if cfg!(target_os = "linux") { + self.linux + } else { + self.fallback + } + } +} + +impl From> for usize { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + +impl From> for f32 { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + impl Default for SnapshotOptions { fn default() -> Self { Self { threshold: 0.6, output_path: PathBuf::from("tests/snapshots"), + failed_pixel_count_threshold: 0, // Default is 0, meaning no pixels can differ } } } @@ -37,8 +138,8 @@ impl SnapshotOptions { /// The default is `0.6` (which is enough for most egui tests to pass across different /// wgpu backends). #[inline] - pub fn threshold(mut self, threshold: f32) -> Self { - self.threshold = threshold; + pub fn threshold(mut self, threshold: impl Into) -> Self { + self.threshold = threshold.into(); self } @@ -49,6 +150,20 @@ impl SnapshotOptions { self.output_path = output_path.into(); self } + + /// Change the number of pixels that can differ before the snapshot is considered a failure. + /// + /// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + #[inline] + pub fn failed_pixel_count_threshold( + mut self, + failed_pixel_count_threshold: impl Into>, + ) -> Self { + let failed_pixel_count_threshold = failed_pixel_count_threshold.into().threshold(); + self.failed_pixel_count_threshold = failed_pixel_count_threshold; + self + } } #[derive(Debug)] @@ -58,7 +173,7 @@ pub enum SnapshotError { /// Name of the test name: String, - /// Count of pixels that were different + /// Count of pixels that were different (above the per-pixel threshold). diff: i32, /// Path where the diff image was saved @@ -128,11 +243,17 @@ impl Display for SnapshotError { write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") } err => { - write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") + write!( + f, + "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}" + ) } }, err => { - write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/main/CONTRIBUTING.md#making-a-pr") + write!( + f, + "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/main/CONTRIBUTING.md#making-a-pr" + ) } } } @@ -189,15 +310,24 @@ fn should_update_snapshots() -> bool { /// reading or writing the snapshot. pub fn try_image_snapshot_options( new: &image::RgbaImage, - name: &str, + name: impl Into, + options: &SnapshotOptions, +) -> SnapshotResult { + try_image_snapshot_options_impl(new, name.into(), options) +} + +fn try_image_snapshot_options_impl( + new: &image::RgbaImage, + name: String, options: &SnapshotOptions, ) -> SnapshotResult { let SnapshotOptions { threshold, output_path, + failed_pixel_count_threshold, } = options; - let parent_path = if let Some(parent) = PathBuf::from(name).parent() { + let parent_path = if let Some(parent) = PathBuf::from(&name).parent() { output_path.join(parent) } else { output_path.clone() @@ -263,7 +393,7 @@ pub fn try_image_snapshot_options( return update_snapshot(); } else { return Err(SnapshotError::SizeMismatch { - name: name.to_owned(), + name, expected: previous.dimensions(), actual: new.dimensions(), }); @@ -274,19 +404,24 @@ pub fn try_image_snapshot_options( let result = dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None); - if let Some((diff, result_image)) = result { + if let Some((num_wrong_pixels, result_image)) = result { result_image .save(diff_path.clone()) .map_err(|err| SnapshotError::WriteSnapshot { path: diff_path.clone(), err, })?; + if should_update_snapshots() { update_snapshot() } else { + if num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64 { + return Ok(()); + } + Err(SnapshotError::Diff { - name: name.to_owned(), - diff, + name, + diff: num_wrong_pixels, diff_path, }) } @@ -308,7 +443,7 @@ pub fn try_image_snapshot_options( /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error /// reading or writing the snapshot. -pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotResult { +pub fn try_image_snapshot(current: &image::RgbaImage, name: impl Into) -> SnapshotResult { try_image_snapshot_options(current, name, &SnapshotOptions::default()) } @@ -329,7 +464,11 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotRes /// Panics if the image does not match the snapshot or if there was an error reading or writing the /// snapshot. #[track_caller] -pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &SnapshotOptions) { +pub fn image_snapshot_options( + current: &image::RgbaImage, + name: impl Into, + options: &SnapshotOptions, +) { match try_image_snapshot_options(current, name, options) { Ok(_) => {} Err(err) => { @@ -348,7 +487,7 @@ pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: & /// Panics if the image does not match the snapshot or if there was an error reading or writing the /// snapshot. #[track_caller] -pub fn image_snapshot(current: &image::RgbaImage, name: &str) { +pub fn image_snapshot(current: &image::RgbaImage, name: impl Into) { match try_image_snapshot(current, name) { Ok(_) => {} Err(err) => { @@ -379,13 +518,13 @@ impl Harness<'_, State> { /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. pub fn try_snapshot_options( &mut self, - name: &str, + name: impl Into, options: &SnapshotOptions, ) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; - try_image_snapshot_options(&image, name, options) + try_image_snapshot_options(&image, name.into(), options) } /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot. @@ -396,7 +535,7 @@ impl Harness<'_, State> { /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. - pub fn try_snapshot(&mut self, name: &str) -> SnapshotResult { + pub fn try_snapshot(&mut self, name: impl Into) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; @@ -422,7 +561,7 @@ impl Harness<'_, State> { /// Panics if the image does not match the snapshot, if there was an error reading or writing the /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + pub fn snapshot_options(&mut self, name: impl Into, options: &SnapshotOptions) { match self.try_snapshot_options(name, options) { Ok(_) => {} Err(err) => { @@ -440,7 +579,7 @@ impl Harness<'_, State> { /// Panics if the image does not match the snapshot, if there was an error reading or writing the /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn snapshot(&mut self, name: &str) { + pub fn snapshot(&mut self, name: impl Into) { match self.try_snapshot(name) { Ok(_) => {} Err(err) => { @@ -461,7 +600,7 @@ impl Harness<'_, State> { )] pub fn try_wgpu_snapshot_options( &mut self, - name: &str, + name: impl Into, options: &SnapshotOptions, ) -> SnapshotResult { self.try_snapshot_options(name, options) @@ -471,7 +610,7 @@ impl Harness<'_, State> { since = "0.31.0", note = "Use `try_snapshot` instead. This function will be removed in 0.32" )] - pub fn try_wgpu_snapshot(&mut self, name: &str) -> SnapshotResult { + pub fn try_wgpu_snapshot(&mut self, name: impl Into) -> SnapshotResult { self.try_snapshot(name) } @@ -479,7 +618,7 @@ impl Harness<'_, State> { since = "0.31.0", note = "Use `snapshot_options` instead. This function will be removed in 0.32" )] - pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + pub fn wgpu_snapshot_options(&mut self, name: impl Into, options: &SnapshotOptions) { self.snapshot_options(name, options); } @@ -555,11 +694,7 @@ impl SnapshotResults { /// Convert this into a `Result<(), Self>`. #[expect(clippy::missing_errors_doc)] pub fn into_result(self) -> Result<(), Self> { - if self.has_errors() { - Err(self) - } else { - Ok(()) - } + if self.has_errors() { Err(self) } else { Ok(()) } } pub fn into_inner(mut self) -> Vec { diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index efd954245..e0fef2901 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -2,7 +2,7 @@ use std::iter::once; use std::sync::Arc; use egui::TexturesDelta; -use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; +use egui_wgpu::{RenderState, ScreenDescriptor, WgpuSetup, wgpu}; use image::RgbaImage; use crate::texture_to_image::texture_to_image; diff --git a/crates/egui_kittest/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs index 02afddefc..3f1f33ba9 100644 --- a/crates/egui_kittest/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -1,8 +1,8 @@ //! Tests the accesskit accessibility output of egui. use egui::{ - accesskit::{NodeId, Role, TreeUpdate}, CentralPanel, Context, RawInput, Window, + accesskit::{NodeId, Role, TreeUpdate}, }; /// Baseline test that asserts there are no spurious nodes in the diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index fc19e8040..b7d001308 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -1,5 +1,5 @@ -use egui::containers::menu::{Bar, MenuConfig, SubMenuButton}; -use egui::{include_image, PopupCloseBehavior, Ui}; +use egui::containers::menu::{MenuBar, MenuConfig, SubMenuButton}; +use egui::{PopupCloseBehavior, Ui, include_image}; use egui_kittest::{Harness, SnapshotResults}; use kittest::Queryable as _; @@ -18,7 +18,7 @@ impl TestMenu { fn ui(&mut self, ui: &mut Ui) { ui.vertical(|ui| { - Bar::new().config(self.config.clone()).ui(ui, |ui| { + MenuBar::new().config(self.config.clone()).ui(ui, |ui| { egui::Sides::new().show( ui, |ui| { @@ -95,7 +95,7 @@ fn menu_close_on_click_outside() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); harness @@ -106,9 +106,7 @@ fn menu_close_on_click_outside() { // We should be able to check the checkbox without closing the menu // Click a couple of times, just in case for expect_checked in [true, false, true, false] { - harness - .get_by_label("Checkbox in Submenu C") - .simulate_click(); + harness.get_by_label("Checkbox in Submenu C").click(); harness.run(); assert_eq!(expect_checked, harness.state().checked); } @@ -119,7 +117,7 @@ fn menu_close_on_click_outside() { assert!(harness.query_by_label("Checkbox in Submenu C").is_some()); // Clicking outside should close the menu - harness.get_by_label("Some other label").simulate_click(); + harness.get_by_label("Some other label").click(); harness.run(); assert!(harness.query_by_label("Checkbox in Submenu C").is_none()); } @@ -130,14 +128,14 @@ fn menu_close_on_click() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); harness.get_by_label_contains("Submenu B with icon").hover(); harness.run(); // Clicking the button should close the menu (even if ui.close() is not called by the button) - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -145,21 +143,19 @@ fn menu_close_on_click() { #[test] fn clicking_submenu_button_should_never_close_menu() { // We test for this since otherwise the menu wouldn't work on touch devices - // The other tests use .hover to open submenus, but this test explicitly uses .simulate_click + // The other tests use .hover to open submenus, but this test explicitly uses .click let mut harness = TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); // Clicking the submenu button should not close the menu - harness - .get_by_label_contains("Submenu B with icon") - .simulate_click(); + harness.get_by_label_contains("Submenu B with icon").click(); harness.run(); - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -174,7 +170,7 @@ fn menu_snapshots() { harness.run(); results.add(harness.try_snapshot("menu/closed_hovered")); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); results.add(harness.try_snapshot("menu/opened")); diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs index 368e8de7e..a8d3bcc9d 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -23,7 +23,7 @@ fn test_interactive_tooltip() { harness.run(); harness.get_by_label("link").hover(); harness.run(); - harness.get_by_label("link").simulate_click(); + harness.get_by_label("link").click(); harness.run(); diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 50ed1095a..0cae152bf 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,6 +1,8 @@ -use egui::accesskit::Role; +use egui::accesskit::{self, Role}; use egui::{Button, ComboBox, Image, Vec2, Widget as _}; -use egui_kittest::{kittest::Queryable as _, Harness, SnapshotResults}; +#[cfg(all(feature = "wgpu", feature = "snapshot"))] +use egui_kittest::SnapshotResults; +use egui_kittest::{Harness, kittest::Queryable as _}; #[test] pub fn focus_should_skip_over_disabled_buttons() { @@ -10,19 +12,19 @@ pub fn focus_should_skip_over_disabled_buttons() { ui.add(Button::new("Button 3")); }); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); assert!(button_1.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_3 = harness.get_by_label("Button 3"); assert!(button_3.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); @@ -41,13 +43,13 @@ pub fn focus_should_skip_over_disabled_drag_values() { ui.add(egui::DragValue::new(&mut value_3)); }); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let drag_value_1 = harness.get_by(|node| node.numeric_value() == Some(1.0)); assert!(drag_value_1.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let drag_value_3 = harness.get_by(|node| node.numeric_value() == Some(3.0)); @@ -89,6 +91,7 @@ fn test_combobox() { harness.run(); + #[cfg(all(feature = "wgpu", feature = "snapshot"))] let mut results = SnapshotResults::new(); #[cfg(all(feature = "wgpu", feature = "snapshot"))] @@ -103,8 +106,7 @@ fn test_combobox() { results.add(harness.try_snapshot("combobox_opened")); let item_2 = harness.get_by_role_and_label(Role::Button, "Item 2"); - // Node::click doesn't close the popup, so we use simulate_click - item_2.simulate_click(); + item_2.click(); harness.run(); @@ -113,3 +115,45 @@ fn test_combobox() { // Popup should be closed now assert!(harness.query_by_label("Item 2").is_none()); } + +/// `https://github.com/emilk/egui/issues/7065` +#[test] +pub fn slider_should_move_with_fixed_decimals() { + let mut value: f32 = 1.0; + + let mut harness = Harness::new_ui(|ui| { + // Movement on arrow-key is relative to slider width; make the slider wide so the movement becomes small. + ui.spacing_mut().slider_width = 2000.0; + ui.add(egui::Slider::new(&mut value, 0.1..=10.0).fixed_decimals(2)); + }); + + harness.key_press(egui::Key::Tab); + harness.run(); + + let actual_slider = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(actual_slider.value(), Some("1.00".to_owned())); + + harness.key_press(egui::Key::ArrowRight); + harness.run(); + + let actual_slider = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(actual_slider.value(), Some("1.01".to_owned())); + + harness.key_press(egui::Key::ArrowRight); + harness.run(); + + let actual_slider = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(actual_slider.value(), Some("1.02".to_owned())); + + harness.key_press(egui::Key::ArrowLeft); + harness.run(); + + let actual_slider = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(actual_slider.value(), Some("1.01".to_owned())); + + harness.key_press(egui::Key::ArrowLeft); + harness.run(); + + let actual_slider = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(actual_slider.value(), Some("1.00".to_owned())); +} diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 4b8288326..a3d4ca79a 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1d172484712e3e12038f8ff427db8c0073aba124aa1b6be17edcc7dccb12f74 -size 1656 +oid sha256:341658df1dfe665e79180d4540965a986a21de09c9cbc1a8744bdcff1a7c2086 +size 1892 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png new file mode 100644 index 000000000..32969d743 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d76e55327de17163bc9c7e128c28153f95db3229dec919352a024eb80544f1 +size 7399 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png new file mode 100644 index 000000000..361925d04 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b7b3145401b7cf9815a652a0914b230892ffda3b5e23fea530dafee9c0c3d3 +size 8110 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 2b223f457..6d66c5f5a 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,6 +1,6 @@ -use egui::{include_image, Modifiers, Vec2}; -use egui_kittest::Harness; -use kittest::{Key, Queryable as _}; +use egui::{Modifiers, ScrollArea, Vec2, include_image}; +use egui_kittest::{Harness, SnapshotResults}; +use kittest::Queryable as _; #[test] fn test_shrink() { @@ -39,17 +39,15 @@ fn test_modifiers() { State::default(), ); - harness.get_by_label("Click me").key_down(Key::Command); - // This run isn't necessary, but allows us to test whether modifiers are remembered between frames - harness.run(); - harness.get_by_label("Click me").click(); - harness.get_by_label("Click me").key_up(Key::Command); + harness + .get_by_label("Click me") + .click_modifiers(Modifiers::COMMAND); harness.run(); - harness.press_key_modifiers(Modifiers::COMMAND, egui::Key::Z); + harness.key_press_modifiers(Modifiers::COMMAND, egui::Key::Z); harness.run(); - harness.node().key_combination(&[Key::Command, Key::Y]); + harness.key_combination_modifiers(Modifiers::COMMAND, &[egui::Key::Y]); harness.run(); let state = harness.state(); @@ -83,3 +81,60 @@ fn should_wait_for_images() { harness.snapshot("should_wait_for_images"); } + +fn test_scroll_harness() -> Harness<'static, bool> { + Harness::builder() + .with_size(Vec2::new(100.0, 200.0)) + .build_ui_state( + |ui, state| { + ScrollArea::vertical().show(ui, |ui| { + for i in 0..20 { + ui.label(format!("Item {i}")); + } + if ui.button("Hidden Button").clicked() { + *state = true; + }; + }); + }, + false, + ) +} + +#[test] +fn test_scroll_to_me() { + let mut harness = test_scroll_harness(); + let mut results = SnapshotResults::new(); + + results.add(harness.try_snapshot("test_scroll_initial")); + + harness.get_by_label("Hidden Button").scroll_to_me(); + + harness.run(); + results.add(harness.try_snapshot("test_scroll_scrolled")); + + harness.get_by_label("Hidden Button").click(); + harness.run(); + + assert!( + harness.state(), + "The button was not clicked after scrolling." + ); +} + +#[test] +fn test_scroll_down() { + let mut harness = test_scroll_harness(); + + let button = harness.get_by_label("Hidden Button"); + button.scroll_down(); + button.scroll_down(); + harness.run(); + + harness.get_by_label("Hidden Button").click(); + harness.run(); + + assert!( + harness.state(), + "The button was not clicked after scrolling down. (Probably not scrolled enough / at all)" + ); +} diff --git a/crates/emath/src/align.rs b/crates/emath/src/align.rs index b1b56755e..a672c456e 100644 --- a/crates/emath/src/align.rs +++ b/crates/emath/src/align.rs @@ -1,6 +1,6 @@ //! One- and two-dimensional alignment ([`Align::Center`], [`Align2::LEFT_TOP`] etc). -use crate::{pos2, vec2, Pos2, Rangef, Rect, Vec2}; +use crate::{Pos2, Rangef, Rect, Vec2, pos2, vec2}; /// left/center/right or top/center/bottom alignment for e.g. anchors and layouts. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -146,7 +146,7 @@ impl Align { // ---------------------------------------------------------------------------- /// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Align2(pub [Align; 2]); @@ -298,3 +298,9 @@ impl std::ops::IndexMut for Align2 { pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect { Align2::CENTER_CENTER.align_size_within_rect(size, frame) } + +impl std::fmt::Debug for Align2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Align2({:?}, {:?})", self.x(), self.y()) + } +} diff --git a/crates/emath/src/easing.rs b/crates/emath/src/easing.rs index 352c451c2..95fc7250d 100644 --- a/crates/emath/src/easing.rs +++ b/crates/emath/src/easing.rs @@ -137,11 +137,7 @@ pub fn exponential_in(t: f32) -> f32 { /// There is a small discontinuity at 1. #[inline] pub fn exponential_out(t: f32) -> f32 { - if t == 1. { - t - } else { - 1. - powf(2.0, -10. * t) - } + if t == 1. { t } else { 1. - powf(2.0, -10. * t) } } /// diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 337fa2045..2d9dfb2f1 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -44,7 +44,7 @@ mod vec2b; pub use self::{ align::{Align, Align2}, - gui_rounding::{GuiRounding, GUI_ROUNDING}, + gui_rounding::{GUI_ROUNDING, GuiRounding}, history::History, numeric::*, ordered_float::*, @@ -182,11 +182,7 @@ where ); let t = (x - *from.start()) / (*from.end() - *from.start()); // Ensure no numerical inaccuracies sneak in: - if T::ONE <= t { - *to.end() - } else { - lerp(to, t) - } + if T::ONE <= t { *to.end() } else { lerp(to, t) } } } diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index 62590b10f..fc26686b3 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -3,7 +3,7 @@ use std::{ ops::{Add, AddAssign, MulAssign, Sub, SubAssign}, }; -use crate::{lerp, Div, Mul, Vec2}; +use crate::{Div, Mul, Vec2, lerp}; /// A position on screen. /// diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 00bed04f0..089e81671 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,6 +1,7 @@ use std::fmt; -use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2}; +use crate::{Div, Mul, NumExt as _, Pos2, Rangef, Rot2, Vec2, lerp, pos2, vec2}; +use std::ops::{BitOr, BitOrAssign}; /// A rectangular region of space. /// @@ -341,11 +342,13 @@ impl Rect { self.max - self.min } + /// Note: this can be negative. #[inline(always)] pub fn width(&self) -> f32 { self.max.x - self.min.x } + /// Note: this can be negative. #[inline(always)] pub fn height(&self) -> f32 { self.max.y - self.min.y @@ -373,9 +376,10 @@ impl Rect { } } + /// This is never negative, and instead returns zero for negative rectangles. #[inline(always)] pub fn area(&self) -> f32 { - self.width() * self.height() + self.width().at_least(0.0) * self.height().at_least(0.0) } /// The distance from the rect to the position. @@ -651,7 +655,7 @@ impl Rect { pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { debug_assert!( d.is_normalized(), - "expected normalized direction, but `d` has length {}", + "Debug assert: expected normalized direction, but `d` has length {}", d.length() ); @@ -773,6 +777,22 @@ impl Div for Rect { } } +impl BitOr for Rect { + type Output = Self; + + #[inline] + fn bitor(self, other: Self) -> Self { + self.union(other) + } +} + +impl BitOrAssign for Rect { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = self.union(other); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/emath/src/rect_align.rs b/crates/emath/src/rect_align.rs index 5a8102ad1..31580405f 100644 --- a/crates/emath/src/rect_align.rs +++ b/crates/emath/src/rect_align.rs @@ -7,9 +7,9 @@ use crate::{Align2, Pos2, Rect, Vec2}; /// /// There are helper constants for the 12 common menu positions: /// ```text -/// ┌───────────┐ ┌────────┐ ┌─────────┐ -/// │ TOP_START │ │ TOP │ │ TOP_END │ -/// └───────────┘ └────────┘ └─────────┘ +/// ┌───────────┐ ┌────────┐ ┌─────────┐ +/// │ TOP_START │ │ TOP │ │ TOP_END │ +/// └───────────┘ └────────┘ └─────────┘ /// ┌──────────┐ ┌────────────────────────────────────┐ ┌───────────┐ /// │LEFT_START│ │ │ │RIGHT_START│ /// └──────────┘ │ │ └───────────┘ @@ -19,9 +19,9 @@ use crate::{Align2, Pos2, Rect, Vec2}; /// ┌──────────┐ │ │ ┌───────────┐ /// │ LEFT_END │ │ │ │ RIGHT_END │ /// └──────────┘ └────────────────────────────────────┘ └───────────┘ -/// ┌────────────┐ ┌──────┐ ┌──────────┐ -/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│ -/// └────────────┘ └──────┘ └──────────┘ +/// ┌────────────┐ ┌──────┐ ┌──────────┐ +/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│ +/// └────────────┘ └──────┘ └──────────┘ /// ``` // There is no `new` function on purpose, since writing out `parent` and `child` is more // reasonable. @@ -235,45 +235,34 @@ impl RectAlign { [self.flip_x(), self.flip_y(), self.flip()] } - /// Look for the [`RectAlign`] that fits best in the available space. + /// Look for the first alternative [`RectAlign`] that allows the child rect to fit + /// inside the `screen_rect`. + /// + /// If no alternative fits, the first is returned. + /// If no alternatives are given, `None` is returned. /// /// See also: /// - [`RectAlign::symmetries`] to calculate alternatives /// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions pub fn find_best_align( - mut values_to_try: impl Iterator, - available_space: Rect, + values_to_try: impl Iterator, + screen_rect: Rect, parent_rect: Rect, gap: f32, - size: Vec2, - ) -> Self { - let area = size.x * size.y; - - let blocked_area = |pos: Self| { - let rect = pos.align_rect(&parent_rect, size, gap); - area - available_space.intersect(rect).area() - }; - - let first = values_to_try.next().unwrap_or_default(); - - if blocked_area(first) == 0.0 { - return first; - } - - let mut best_area = blocked_area(first); - let mut best = first; + expected_size: Vec2, + ) -> Option { + let mut first_choice = None; for align in values_to_try { - let blocked = blocked_area(align); - if blocked == 0.0 { - return align; - } - if blocked < best_area { - best = align; - best_area = blocked; + first_choice = first_choice.or(Some(align)); // Remember the first alternative + + let suggested_popup_rect = align.align_rect(&parent_rect, expected_size, gap); + + if screen_rect.contains_rect(suggested_popup_rect) { + return Some(align); } } - best + first_choice } } diff --git a/crates/emath/src/rect_transform.rs b/crates/emath/src/rect_transform.rs index da9382790..3539efe75 100644 --- a/crates/emath/src/rect_transform.rs +++ b/crates/emath/src/rect_transform.rs @@ -1,4 +1,4 @@ -use crate::{pos2, remap, remap_clamp, Pos2, Rect, Vec2}; +use crate::{Pos2, Rect, Vec2, pos2, remap, remap_clamp}; /// Linearly transforms positions from one [`Rect`] to another. /// diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 4771343e8..f79359df9 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -170,11 +170,7 @@ impl Vec2 { #[inline(always)] pub fn normalized(self) -> Self { let len = self.length(); - if len <= 0.0 { - self - } else { - self / len - } + if len <= 0.0 { self } else { self / len } } /// Checks if `self` has length `1.0` up to a precision of `1e-6`. diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 2c2cfcc1c..bff5336a4 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,32 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.32.0 - 2025-07-10 +### ⭐ Added +* Impl AsRef<[u8]> for FontData [#5757](https://github.com/emilk/egui/pull/5757) by [@StratusFearMe21](https://github.com/StratusFearMe21) +* Add `expand_bg` to customize size of text background [#5365](https://github.com/emilk/egui/pull/5365) by [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) +* Add anchored text rotation method, and clarify related docs [#7130](https://github.com/emilk/egui/pull/7130) by [@pmarks](https://github.com/pmarks) +* Add `Galley::intrinsic_size` [#7146](https://github.com/emilk/egui/pull/7146) by [@lucasmerlin](https://github.com/lucasmerlin) + +### 🔧 Changed +* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk) +* 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) +* Make text underline and strikethrough pixel perfect crisp [#5857](https://github.com/emilk/egui/pull/5857) by [@emilk](https://github.com/emilk) +* Update `emoji-icon-font` with fix for fullwidth latin characters [#7067](https://github.com/emilk/egui/pull/7067) by [@emilk](https://github.com/emilk) +* Add assert messages and print bad argument values in asserts [#5216](https://github.com/emilk/egui/pull/5216) by [@bircni](https://github.com/bircni) + +### 🔥 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) + +### 🐛 Fixed +* Fix: transform `TextShape` underline width [#5865](https://github.com/emilk/egui/pull/5865) by [@emilk](https://github.com/emilk) +* Fix `visual_bounding_rect` for rotated text [#7050](https://github.com/emilk/egui/pull/7050) by [@pmarks](https://github.com/pmarks) + +### 🚀 Performance +* Optimize editing long text by caching each paragraph [#5411](https://github.com/emilk/egui/pull/5411) by [@afishhh](https://github.com/afishhh) + + ## 0.31.1 - 2025-03-05 * Fix panic when rendering thin textured rectangles [#5692](https://github.com/emilk/egui/pull/5692) by [@PPakalns](https://github.com/PPakalns) diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 14f4d2fa7..d4b10a216 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -1,8 +1,8 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use epaint::{ - pos2, tessellator::Path, ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, - TessellationOptions, Tessellator, TextureAtlas, Vec2, + AlphaFromCoverage, ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, + TessellationOptions, Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path, }; #[global_allocator] @@ -66,7 +66,7 @@ fn tessellate_circles(c: &mut Criterion) { let pixels_per_point = 2.0; let options = TessellationOptions::default(); - let atlas = TextureAtlas::new([4096, 256]); + let atlas = TextureAtlas::new([4096, 256], AlphaFromCoverage::default()); let font_tex_size = atlas.size(); let prepared_discs = atlas.prepared_discs(); diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index b6183e7a1..e14ea869e 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -1,30 +1,26 @@ use emath::Vec2; -use crate::{textures::TextureOptions, Color32}; +use crate::{Color32, textures::TextureOptions}; use std::sync::Arc; /// An image stored in RAM. /// /// To load an image file, see [`ColorImage::from_rgba_unmultiplied`]. /// -/// In order to paint the image on screen, you first need to convert it to +/// This is currently an enum with only one variant, but more image types may be added in the future. /// -/// See also: [`ColorImage`], [`FontImage`]. -#[derive(Clone, PartialEq)] +/// See also: [`ColorImage`]. +#[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageData { /// RGBA image. Color(Arc), - - /// Used for the font texture. - Font(FontImage), } impl ImageData { pub fn size(&self) -> [usize; 2] { match self { Self::Color(image) => image.size, - Self::Font(image) => image.size, } } @@ -38,7 +34,7 @@ impl ImageData { pub fn bytes_per_pixel(&self) -> usize { match self { - Self::Color(_) | Self::Font(_) => 4, + Self::Color(_) => 4, } } } @@ -271,6 +267,37 @@ impl ColorImage { } Self::new([width, height], output) } + + /// Clone a sub-region as a new image. + pub fn region_by_pixels(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self { + assert!( + x + w <= self.width(), + "x + w should be <= self.width(), but x: {}, w: {}, width: {}", + x, + w, + self.width() + ); + assert!( + y + h <= self.height(), + "y + h should be <= self.height(), but y: {}, h: {}, height: {}", + y, + h, + self.height() + ); + + let mut pixels = Vec::with_capacity(w * h); + for y in y..y + h { + let offset = y * self.width() + x; + pixels.extend(&self.pixels[offset..(offset + w)]); + } + assert_eq!( + pixels.len(), + w * h, + "pixels.len should be w * h, but got {}", + pixels.len() + ); + Self::new([w, h], pixels) + } } impl std::ops::Index<(usize, usize)> for ColorImage { @@ -318,127 +345,56 @@ impl std::fmt::Debug for ColorImage { // ---------------------------------------------------------------------------- -/// A single-channel image designed for the font texture. -/// -/// Each value represents "coverage", i.e. how much a texel is covered by a character. -/// -/// This is roughly interpreted as the opacity of a white image. -#[derive(Clone, Default, PartialEq)] +/// How to convert font coverage values into alpha and color values. +// +// This whole thing is less than rigorous. +// Ideally we should do this in a shader instead, and use different computations +// for different text colors. +// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis. +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct FontImage { - /// width, height - pub size: [usize; 2], - - /// The coverage value. +pub enum AlphaFromCoverage { + /// `alpha = coverage`. /// - /// Often you want to use [`Self::srgba_pixels`] instead. - pub pixels: Vec, -} - -impl FontImage { - pub fn new(size: [usize; 2]) -> Self { - Self { - size, - pixels: vec![0.0; size[0] * size[1]], - } - } - - #[inline] - pub fn width(&self) -> usize { - self.size[0] - } - - #[inline] - pub fn height(&self) -> usize { - self.size[1] - } - - /// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom. + /// Looks good for black-on-white text, i.e. light mode. /// - /// `gamma` should normally be set to `None`. + /// Same as [`Self::Gamma`]`(1.0)`, but more efficient. + Linear, + + /// `alpha = coverage^gamma`. + Gamma(f32), + + /// `alpha = 2 * coverage - coverage^2` /// - /// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`. - #[inline] - pub fn srgba_pixels(&self, gamma: Option) -> impl ExactSizeIterator + '_ { - // This whole function is less than rigorous. - // Ideally we should do this in a shader instead, and use different computations - // for different text colors. - // See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis. - self.pixels.iter().map(move |coverage| { - let alpha = if let Some(gamma) = gamma { - coverage.powf(gamma) - } else { - // alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending) - - // The following is recommended by the article for BLACK text (using linear blending). - // Very similar to a gamma of 0.5, but produces sharper text. - // In practice it works well for all text colors (better than a gamma of 0.5, for instance). - // See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison. - 2.0 * coverage - coverage * coverage - }; - Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha)) - }) - } - - /// Clone a sub-region as a new image. - pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self { - assert!( - x + w <= self.width(), - "x + w should be <= self.width(), but x: {}, w: {}, width: {}", - x, - w, - self.width() - ); - assert!( - y + h <= self.height(), - "y + h should be <= self.height(), but y: {}, h: {}, height: {}", - y, - h, - self.height() - ); - - let mut pixels = Vec::with_capacity(w * h); - for y in y..y + h { - let offset = y * self.width() + x; - pixels.extend(&self.pixels[offset..(offset + w)]); - } - assert_eq!( - pixels.len(), - w * h, - "pixels.len should be w * h, but got {}", - pixels.len() - ); - Self { - size: [w, h], - pixels, - } - } + /// This looks good for white-on-black text, i.e. dark mode. + /// + /// Very similar to a gamma of 0.5, but produces sharper text. + /// See for a comparison to gamma=0.5. + #[default] + TwoCoverageMinusCoverageSq, } -impl std::ops::Index<(usize, usize)> for FontImage { - type Output = f32; +impl AlphaFromCoverage { + /// A good-looking default for light mode (black-on-white text). + pub const LIGHT_MODE_DEFAULT: Self = Self::Linear; - #[inline] - fn index(&self, (x, y): (usize, usize)) -> &f32 { - let [w, h] = self.size; - assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); - &self.pixels[y * w + x] - } -} + /// A good-looking default for dark mode (white-on-black text). + pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq; -impl std::ops::IndexMut<(usize, usize)> for FontImage { - #[inline] - fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 { - let [w, h] = self.size; - assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); - &mut self.pixels[y * w + x] - } -} - -impl From for ImageData { + /// Convert coverage to alpha. #[inline(always)] - fn from(image: FontImage) -> Self { - Self::Font(image) + pub fn alpha_from_coverage(&self, coverage: f32) -> f32 { + match self { + Self::Linear => coverage, + Self::Gamma(gamma) => coverage.powf(*gamma), + Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage, + } + } + + #[inline(always)] + pub fn color_from_coverage(&self, coverage: f32) -> Color32 { + let alpha = self.alpha_from_coverage(coverage); + Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha)) } } @@ -447,7 +403,7 @@ impl From for ImageData { /// A change to an image. /// /// Either a whole new image, or an update to a rectangular region of it. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "The painter must take care of this"] pub struct ImageDelta { diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 633aa6689..f02889d97 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -50,7 +50,7 @@ pub use self::{ color::ColorMode, corner_radius::CornerRadius, corner_radius_f32::CornerRadiusF32, - image::{ColorImage, FontImage, ImageData, ImageDelta}, + image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta}, margin::Margin, margin_f32::*, mesh::{Mesh, Mesh16, Vertex}, @@ -73,7 +73,7 @@ pub use self::{ pub type Rounding = CornerRadius; pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; -pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; +pub use emath::{Pos2, Rect, Vec2, pos2, vec2}; #[deprecated = "Use the ahash crate directly."] pub use ahash; diff --git a/crates/epaint/src/margin.rs b/crates/epaint/src/margin.rs index 417cc5568..e6f6d2287 100644 --- a/crates/epaint/src/margin.rs +++ b/crates/epaint/src/margin.rs @@ -1,4 +1,4 @@ -use emath::{vec2, Rect, Vec2}; +use emath::{Rect, Vec2, vec2}; /// A value for all four sides of a rectangle, /// often used to express padding or spacing. diff --git a/crates/epaint/src/margin_f32.rs b/crates/epaint/src/margin_f32.rs index fd88611d0..22bc8b3d4 100644 --- a/crates/epaint/src/margin_f32.rs +++ b/crates/epaint/src/margin_f32.rs @@ -1,4 +1,4 @@ -use emath::{vec2, Rect, Vec2}; +use emath::{Rect, Vec2, vec2}; use crate::Margin; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 60be2935c..fbff16ba2 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -1,4 +1,4 @@ -use crate::{emath, Color32, TextureId, WHITE_UV}; +use crate::{Color32, TextureId, WHITE_UV, emath}; use emath::{Pos2, Rect, Rot2, TSTransform, Vec2}; /// The 2D vertex type. diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index ee010ee99..ace5ab90a 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -29,7 +29,8 @@ pub struct Shadow { #[test] fn shadow_size() { assert_eq!( - std::mem::size_of::(), 8, + std::mem::size_of::(), + 8, "Shadow changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); } diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 57de14969..4b68d5964 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - color, CircleShape, Color32, ColorMode, CubicBezierShape, EllipseShape, Mesh, PathShape, - QuadraticBezierShape, RectShape, Shape, TextShape, + CircleShape, Color32, ColorMode, CubicBezierShape, EllipseShape, Mesh, PathShape, + QuadraticBezierShape, RectShape, Shape, TextShape, color, }; /// Remember to handle [`Color32::PLACEHOLDER`] specially! diff --git a/crates/epaint/src/shapes/bezier_shape.rs b/crates/epaint/src/shapes/bezier_shape.rs index 7d291c7fd..5823c3796 100644 --- a/crates/epaint/src/shapes/bezier_shape.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -253,8 +253,8 @@ impl CubicBezierShape { if p > 0.0 { return None; } - let r = (-1.0 * (p / 3.0).powi(3)).sqrt(); - let theta = (-1.0 * q / (2.0 * r)).acos() / 3.0; + let r = (-(p / 3.0).powi(3)).sqrt(); + let theta = (-q / (2.0 * r)).acos() / 3.0; let t1 = 2.0 * r.cbrt() * theta.cos() + h; let t2 = 2.0 * r.cbrt() * (theta + 120.0 * std::f32::consts::PI / 180.0).cos() + h; diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index ead5b7af2..2e855d369 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -59,7 +59,8 @@ pub struct RectShape { #[test] fn rect_shape_size() { assert_eq!( - std::mem::size_of::(), 48, + std::mem::size_of::(), + 48, "RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index bc16e43d5..3cdabeadc 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -2,12 +2,12 @@ use std::sync::Arc; -use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2}; +use emath::{Align2, Pos2, Rangef, Rect, TSTransform, Vec2, pos2}; use crate::{ + Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId, stroke::PathStroke, text::{FontId, Fonts, Galley}, - Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId, }; use super::{ @@ -73,7 +73,8 @@ pub enum Shape { #[test] fn shape_size() { assert_eq!( - std::mem::size_of::(), 64, + std::mem::size_of::(), + 64, "Shape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( @@ -362,7 +363,7 @@ impl Shape { Self::Vec(shapes) => { let mut rect = Rect::NOTHING; for shape in shapes { - rect = rect.union(shape.visual_bounding_rect()); + rect |= shape.visual_bounding_rect(); } rect } diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index bf9db964b..9505dc49b 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -130,10 +130,12 @@ impl TextShape { num_vertices: _, num_indices: _, pixels_per_point: _, + intrinsic_size, } = Arc::make_mut(galley); *rect = transform.scaling * *rect; *mesh_bounds = transform.scaling * *mesh_bounds; + *intrinsic_size = transform.scaling * *intrinsic_size; for text::PlacedRow { pos, row } in rows { *pos *= transform.scaling; @@ -179,7 +181,12 @@ mod tests { #[test] fn text_bounding_box_under_rotation() { - let fonts = Fonts::new(1.0, 1024, FontDefinitions::default()); + let fonts = Fonts::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let font = FontId::monospace(12.0); let mut t = crate::Shape::text( diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 50f4f678b..473376f40 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -4,7 +4,7 @@ use std::{fmt::Debug, sync::Arc}; use emath::GuiRounding as _; -use super::{emath, Color32, ColorMode, Pos2, Rect}; +use super::{Color32, ColorMode, Pos2, Rect, emath}; /// Describes the width and color of a line. /// diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b083ea452..4b1b23ba0 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -5,13 +5,13 @@ #![allow(clippy::identity_op)] -use emath::{pos2, remap, vec2, GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2}; +use emath::{GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2, pos2, remap, vec2}; use crate::{ - color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, - ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, EllipseShape, Mesh, - PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, StrokeKind, TextShape, - TextureId, Vertex, WHITE_UV, + CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, + EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, + StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, color::ColorMode, emath, + stroke::PathStroke, texture_atlas::PreparedDisc, }; // ---------------------------------------------------------------------------- @@ -28,7 +28,7 @@ mod precomputed_vertices { // println!("];") // } - use emath::{vec2, Vec2}; + use emath::{Vec2, vec2}; pub const CIRCLE_8: [Vec2; 9] = [ vec2(1.000000, 0.000000), @@ -340,7 +340,7 @@ impl Path { } pub fn add_circle(&mut self, center: Pos2, radius: f32) { - use precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; + use precomputed_vertices::{CIRCLE_8, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_128}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? // same cutoffs as in add_circle_quadrant @@ -535,7 +535,7 @@ impl Path { pub mod path { //! Helpers for constructing paths use crate::CornerRadiusF32; - use emath::{pos2, Pos2, Rect}; + use emath::{Pos2, Rect, pos2}; /// overwrites existing points pub fn rounded_rectangle(path: &mut Vec, rect: Rect, cr: CornerRadiusF32) { @@ -602,7 +602,7 @@ pub mod path { // - quadrant 3: right top // * angle 4 * TAU / 4 = right pub fn add_circle_quadrant(path: &mut Vec, center: Pos2, radius: f32, quadrant: f32) { - use super::precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; + use super::precomputed_vertices::{CIRCLE_8, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_128}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? // same cutoffs as in add_circle diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index b79bea2f2..dd095c443 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -1,12 +1,12 @@ use std::collections::BTreeMap; use std::sync::Arc; -use emath::{vec2, GuiRounding as _, Vec2}; +use emath::{GuiRounding as _, Vec2, vec2}; use crate::{ + TextureAtlas, mutex::{Mutex, RwLock}, text::FontTweak, - TextureAtlas, }; // ---------------------------------------------------------------------------- @@ -279,12 +279,13 @@ impl FontImpl { } else { let glyph_pos = { let atlas = &mut self.atlas.lock(); + let text_alpha_from_coverage = atlas.text_alpha_from_coverage; let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); glyph.draw(|x, y, v| { if 0.0 < v { let px = glyph_pos.0 + x as usize; let py = glyph_pos.1 + y as usize; - image[(px, py)] = v; + image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v); } }); glyph_pos diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index e65514215..e59f9bc26 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1,12 +1,12 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{ + AlphaFromCoverage, TextureAtlas, mutex::{Mutex, MutexGuard}, text::{ - font::{Font, FontImpl}, Galley, LayoutJob, LayoutSection, + font::{Font, FontImpl}, }, - TextureAtlas, }; use emath::{NumExt as _, OrderedFloat}; @@ -430,36 +430,56 @@ impl Fonts { pub fn new( pixels_per_point: f32, max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { let fonts_and_cache = FontsAndCache { - fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + fonts: FontsImpl::new( + pixels_per_point, + max_texture_side, + text_alpha_from_coverage, + definitions, + ), galley_cache: Default::default(), }; Self(Arc::new(Mutex::new(fonts_and_cache))) } /// Call at the start of each frame with the latest known - /// `pixels_per_point` and `max_texture_side`. + /// `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`. /// /// Call after painting the previous frame, but before using [`Fonts`] for the new frame. /// - /// This function will react to changes in `pixels_per_point` and `max_texture_side`, + /// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`, /// as well as notice when the font atlas is getting full, and handle that. - pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) { + pub fn begin_pass( + &self, + pixels_per_point: f32, + max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, + ) { let mut fonts_and_cache = self.0.lock(); let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point; let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side; + let text_alpha_from_coverage_changed = + fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage; let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8; - let needs_recreate = - pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full; + let needs_recreate = pixels_per_point_changed + || max_texture_side_changed + || text_alpha_from_coverage_changed + || font_atlas_almost_full; if needs_recreate { let definitions = fonts_and_cache.fonts.definitions.clone(); *fonts_and_cache = FontsAndCache { - fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + fonts: FontsImpl::new( + pixels_per_point, + max_texture_side, + text_alpha_from_coverage, + definitions, + ), galley_cache: Default::default(), }; } @@ -497,7 +517,7 @@ impl Fonts { /// The full font atlas image. #[inline] - pub fn image(&self) -> crate::FontImage { + pub fn image(&self) -> crate::ColorImage { self.lock().fonts.atlas.lock().image().clone() } @@ -642,6 +662,7 @@ impl FontsImpl { pub fn new( pixels_per_point: f32, max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { assert!( @@ -651,7 +672,7 @@ impl FontsImpl { let texture_width = max_texture_side.at_most(16 * 1024); let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways. - let atlas = TextureAtlas::new([texture_width, initial_height]); + let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage); let atlas = Arc::new(Mutex::new(atlas)); @@ -680,7 +701,8 @@ impl FontsImpl { /// Get the right font implementation from size and [`FontFamily`]. pub fn font(&mut self, font_id: &FontId) -> &mut Font { - let FontId { mut size, family } = font_id; + let FontId { size, family } = font_id; + let mut size = *size; size = size.at_least(0.1).at_most(2048.0); self.sized_family @@ -803,7 +825,7 @@ impl GalleyCache { let job = Arc::new(job); if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { let (child_galleys, child_hashes) = - self.layout_each_paragraph_individuallly(fonts, &job); + self.layout_each_paragraph_individually(fonts, &job); debug_assert_eq!( child_hashes.len(), child_galleys.len(), @@ -847,7 +869,7 @@ impl GalleyCache { } /// Split on `\n` and lay out (and cache) each paragraph individually. - fn layout_each_paragraph_individuallly( + fn layout_each_paragraph_individually( &mut self, fonts: &mut FontsImpl, job: &LayoutJob, @@ -862,9 +884,14 @@ impl GalleyCache { while start < job.text.len() { let is_first_paragraph = start == 0; - let end = job.text[start..] + // `end` will not include the `\n` since we don't want to create an empty row in our + // split galley + let mut end = job.text[start..] .find('\n') - .map_or(job.text.len(), |i| start + i + 1); + .map_or(job.text.len(), |i| start + i); + if end == job.text.len() - 1 && job.text.ends_with('\n') { + end += 1; // If the text ends with a newline, we include it in the last paragraph. + } let mut paragraph_job = LayoutJob { text: job.text[start..end].to_owned(), @@ -898,7 +925,7 @@ impl GalleyCache { if section_range.end <= start { // The section is behind us current_section += 1; - } else if end <= section_range.start { + } else if end < section_range.start { break; // Haven't reached this one yet. } else { // Section range overlaps with paragraph range @@ -931,10 +958,6 @@ impl GalleyCache { // This will prevent us from invalidating cache entries unnecessarily: if max_rows_remaining != usize::MAX { max_rows_remaining -= galley.rows.len(); - // Ignore extra trailing row, see merging `Galley::concat` for more details. - if end < job.text.len() && !galley.elided { - max_rows_remaining += 1; - } } let elided = galley.elided; @@ -943,7 +966,7 @@ impl GalleyCache { break; } - start = end; + start = end + 1; } (child_galleys, child_hashes) @@ -1050,7 +1073,8 @@ mod tests { use core::f32; use super::*; - use crate::{text::TextFormat, Stroke}; + use crate::text::{TextWrapping, layout}; + use crate::{Stroke, text::TextFormat}; use ecolor::Color32; use emath::Align; @@ -1062,12 +1086,41 @@ mod tests { Color32::WHITE, f32::INFINITY, ), + LayoutJob::simple( + "ends with newlines\n\n".to_owned(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + f32::INFINITY, + ), LayoutJob::simple( "Simple test.".to_owned(), FontId::new(14.0, FontFamily::Monospace), Color32::WHITE, f32::INFINITY, ), + { + let mut job = LayoutJob::simple( + "hi".to_owned(), + FontId::default(), + Color32::WHITE, + f32::INFINITY, + ); + job.append("\n", 0.0, TextFormat::default()); + job.append("\n", 0.0, TextFormat::default()); + job.append("world", 0.0, TextFormat::default()); + job.wrap.max_rows = 2; + job + }, + { + let mut job = LayoutJob::simple( + "Test text with a lot of words\n and a newline.".to_owned(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + 40.0, + ); + job.first_row_min_height = 30.0; + job + }, LayoutJob::simple( "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(), FontId::new(14.0, FontFamily::Proportional), @@ -1119,6 +1172,7 @@ mod tests { let mut fonts = FontsImpl::new( pixels_per_point, max_texture_side, + AlphaFromCoverage::default(), FontDefinitions::default(), ); @@ -1160,4 +1214,60 @@ mod tests { } } } + + #[test] + fn test_intrinsic_size() { + let pixels_per_point = [1.0, 1.3, 2.0, 0.867]; + let max_widths = [40.0, 80.0, 133.0, 200.0]; + let rounded_output_to_gui = [false, true]; + + for pixels_per_point in pixels_per_point { + let mut fonts = FontsImpl::new( + pixels_per_point, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); + + for &max_width in &max_widths { + for round_output_to_gui in rounded_output_to_gui { + for mut job in jobs() { + job.wrap = TextWrapping::wrap_at_width(max_width); + + job.round_output_to_gui = round_output_to_gui; + + let galley_wrapped = layout(&mut fonts, job.clone().into()); + + job.wrap = TextWrapping::no_max_width(); + + let text = job.text.clone(); + let galley_unwrapped = layout(&mut fonts, job.into()); + + let intrinsic_size = galley_wrapped.intrinsic_size(); + let unwrapped_size = galley_unwrapped.size(); + + let difference = (intrinsic_size - unwrapped_size).length().abs(); + similar_asserts::assert_eq!( + format!("{intrinsic_size:.4?}"), + format!("{unwrapped_size:.4?}"), + "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?} + Difference: {difference:.8?} + wrapped rows: {}, unwrapped rows: {} + pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}", + galley_wrapped.rows.len(), + galley_unwrapped.rows.len() + ); + similar_asserts::assert_eq!( + format!("{intrinsic_size:.4?}"), + format!("{unwrapped_size:.4?}"), + "Unwrapped galley intrinsic size should exactly match its size. \ + {:.8?} vs {:8?}", + galley_unwrapped.intrinsic_size(), + galley_unwrapped.size(), + ); + } + } + } + } + } } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 63dfc3893..8d4a90fb7 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use emath::{pos2, vec2, Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2}; +use emath::{Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2, pos2, vec2}; -use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; +use crate::{Color32, Mesh, Stroke, Vertex, stroke::PathStroke, text::font::Font}; use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals}; @@ -82,6 +82,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { num_indices: 0, pixels_per_point: fonts.pixels_per_point(), elided: true, + intrinsic_size: Vec2::ZERO, }; } @@ -94,6 +95,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let point_scale = PointScale::new(fonts.pixels_per_point()); + let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs); + let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { @@ -124,7 +127,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { } // Calculate the Y positions and tessellate the text: - galley_from_rows(point_scale, job, rows, elided) + galley_from_rows(point_scale, job, rows, elided, intrinsic_size) } // Ignores the Y coordinate. @@ -190,6 +193,38 @@ fn layout_section( } } +/// Calculate the intrinsic size of the text. +/// +/// The result is eventually passed to `Response::intrinsic_size`. +/// This works by calculating the size of each `Paragraph` (instead of each `Row`). +fn calculate_intrinsic_size( + point_scale: PointScale, + job: &LayoutJob, + paragraphs: &[Paragraph], +) -> Vec2 { + let mut intrinsic_size = Vec2::ZERO; + for (idx, paragraph) in paragraphs.iter().enumerate() { + let width = paragraph + .glyphs + .last() + .map(|l| l.max_x()) + .unwrap_or_default(); + intrinsic_size.x = f32::max(intrinsic_size.x, width); + + let mut height = paragraph + .glyphs + .iter() + .map(|g| g.line_height) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(paragraph.empty_paragraph_height); + if idx == 0 { + height = f32::max(height, job.first_row_min_height); + } + intrinsic_size.y += point_scale.round_to_pixel(height); + } + intrinsic_size +} + // Ignores the Y coordinate. fn rows_from_paragraphs( paragraphs: Vec, @@ -210,7 +245,7 @@ fn rows_from_paragraphs( if paragraph.glyphs.is_empty() { rows.push(PlacedRow { - pos: Pos2::ZERO, + pos: pos2(0.0, f32::NAN), row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], @@ -610,17 +645,18 @@ fn galley_from_rows( job: Arc, mut rows: Vec, elided: bool, + intrinsic_size: Vec2, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; for placed_row in &mut rows { - let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); + let mut max_row_height = first_row_min_height.at_least(placed_row.height()); let row = Arc::make_mut(&mut placed_row.row); first_row_min_height = 0.0; for glyph in &row.glyphs { - max_row_height = max_row_height.max(glyph.line_height); + max_row_height = max_row_height.at_least(glyph.line_height); } max_row_height = point_scale.round_to_pixel(max_row_height); @@ -655,13 +691,12 @@ fn galley_from_rows( let mut num_indices = 0; for placed_row in &mut rows { - rect = rect.union(placed_row.rect()); + rect |= placed_row.rect(); let row = Arc::make_mut(&mut placed_row.row); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); - mesh_bounds = - mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); + mesh_bounds |= row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2()); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); @@ -680,6 +715,7 @@ fn galley_from_rows( num_vertices, num_indices, pixels_per_point: point_scale.pixels_per_point, + intrinsic_size, }; if galley.job.round_output_to_gui { @@ -1034,11 +1070,18 @@ fn is_cjk_break_allowed(c: char) -> bool { #[cfg(test)] mod tests { + use crate::AlphaFromCoverage; + use super::{super::*, *}; #[test] fn test_zero_max_width() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default()); layout_job.wrap.max_width = 0.0; let galley = layout(&mut fonts, layout_job.into()); @@ -1049,7 +1092,12 @@ mod tests { fn test_truncate_with_newline() { // No matter where we wrap, we should be appending the newline character. - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let text_format = TextFormat { font_id: FontId::monospace(12.0), ..Default::default() @@ -1094,7 +1142,12 @@ mod tests { #[test] fn test_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section( "日本語とEnglishの混在した文章".into(), TextFormat::default(), @@ -1109,7 +1162,12 @@ mod tests { #[test] fn test_pre_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section( "日本語とEnglishの混在した文章".into(), TextFormat::default(), @@ -1124,7 +1182,12 @@ mod tests { #[test] fn test_truncate_width() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default()); layout_job.wrap.max_width = f32::INFINITY; @@ -1140,4 +1203,72 @@ mod tests { assert_eq!(row.pos, Pos2::ZERO); assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x()); } + + #[test] + fn test_empty_row() { + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); + + let font_id = FontId::default(); + let font_height = fonts.font(&font_id).row_height(); + + let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY); + + let galley = layout(&mut fonts, job.into()); + + assert_eq!(galley.rows.len(), 1, "Expected one row"); + assert_eq!( + galley.rows[0].row.glyphs.len(), + 0, + "Expected no glyphs in the empty row" + ); + assert_eq!( + galley.size(), + Vec2::new(0.0, font_height.round()), + "Unexpected galley size" + ); + assert_eq!( + galley.intrinsic_size(), + Vec2::new(0.0, font_height.round()), + "Unexpected intrinsic size" + ); + } + + #[test] + fn test_end_with_newline() { + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); + + let font_id = FontId::default(); + let font_height = fonts.font(&font_id).row_height(); + + let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY); + + let galley = layout(&mut fonts, job.into()); + + assert_eq!(galley.rows.len(), 2, "Expected two rows"); + assert_eq!( + galley.rows[1].row.glyphs.len(), + 0, + "Expected no glyphs in the empty row" + ); + assert_eq!( + galley.size().round(), + Vec2::new(17.0, font_height.round() * 2.0), + "Unexpected galley size" + ); + assert_eq!( + galley.intrinsic_size().round(), + Vec2::new(17.0, font_height.round() * 2.0), + "Unexpected intrinsic size" + ); + } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index f6a182dcb..7635dcede 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -9,7 +9,7 @@ use super::{ font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; -use emath::{pos2, vec2, Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2}; +use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2}; /// Describes the task of laying out text. /// @@ -560,6 +560,8 @@ pub struct Galley { /// so that we can warn if this has changed once we get to /// tessellation. pub pixels_per_point: f32, + + pub(crate) intrinsic_size: Vec2, } #[derive(Clone, Debug, PartialEq)] @@ -795,29 +797,19 @@ impl Galley { self.rect.size() } - // TODO: Instead return Option? - pub fn desired_size(&self) -> Vec2 { - let mut current_width: f32 = 0.0; - let mut widest_width: f32 = 0.0; - let mut height = self.rows.first().map_or(0.0, |row| row.height()); - for row in &self.rows { - if current_width != 0.0 { - let space = row.glyphs.last(); - if let Some(space) = space { - if space.chr.is_whitespace() { - // TODO: Needed or not? Doesn't seem like it's needed - // current_width += space.advance_width; - } - } - } - current_width += row.rect().width(); - widest_width = widest_width.max(current_width); - if row.ends_with_newline { - height += row.height(); - current_width = 0.0; - } + /// This is the size that a non-wrapped, non-truncated, non-justified version of the text + /// would have. + /// + /// Useful for advanced layouting. + #[inline] + pub fn intrinsic_size(&self) -> Vec2 { + // We do the rounding here instead of in `round_output_to_gui` so that rounding + // errors don't accumulate when concatenating multiple galleys. + if self.job.round_output_to_gui { + self.intrinsic_size.round_ui() + } else { + self.intrinsic_size } - vec2(widest_width, height) } pub(crate) fn round_output_to_gui(&mut self) { @@ -861,42 +853,39 @@ impl Galley { num_vertices: 0, num_indices: 0, pixels_per_point, + intrinsic_size: Vec2::ZERO, }; for (i, galley) in galleys.iter().enumerate() { let current_y_offset = merged_galley.rect.height(); + let is_last_galley = i + 1 == galleys.len(); - let mut rows = galley.rows.iter(); - // As documented in `Row::ends_with_newline`, a '\n' will always create a - // new `Row` immediately below the current one. Here it doesn't make sense - // for us to append this new row so we just ignore it. - let is_last_row = i + 1 == galleys.len(); - if !is_last_row && !galley.elided { - let popped = rows.next_back(); - debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in Galley::concat"); - } + merged_galley + .rows + .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| { + let new_pos = placed_row.pos + current_y_offset * Vec2::Y; + let new_pos = new_pos.round_to_pixels(pixels_per_point); + merged_galley.mesh_bounds |= + placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2()); + merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size); - merged_galley.rows.extend(rows.map(|placed_row| { - let new_pos = placed_row.pos + current_y_offset * Vec2::Y; - let new_pos = new_pos.round_to_pixels(pixels_per_point); - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - merged_galley.rect = merged_galley - .rect - .union(Rect::from_min_size(new_pos, placed_row.size)); - - super::PlacedRow { - pos: new_pos, - row: placed_row.row.clone(), - } - })); + let mut row = placed_row.row.clone(); + let is_last_row_in_galley = row_idx + 1 == galley.rows.len(); + if !is_last_galley && is_last_row_in_galley { + // Since we remove the `\n` when splitting rows, we need to add it back here + Arc::make_mut(&mut row).ends_with_newline = true; + } + super::PlacedRow { pos: new_pos, row } + })); merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; // Note that if `galley.elided` is true this will be the last `Galley` in // the vector and the loop will end. merged_galley.elided |= galley.elided; + merged_galley.intrinsic_size.x = + f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x); + merged_galley.intrinsic_size.y += galley.intrinsic_size.y; } if merged_galley.job.round_output_to_gui { diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index 790540224..36dd1b48e 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -1,6 +1,7 @@ -use emath::{remap_clamp, Rect}; +use ecolor::Color32; +use emath::{Rect, remap_clamp}; -use crate::{FontImage, ImageDelta}; +use crate::{AlphaFromCoverage, ColorImage, ImageDelta}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Rectu { @@ -57,7 +58,7 @@ pub struct PreparedDisc { /// More characters can be added, possibly expanding the texture. #[derive(Clone)] pub struct TextureAtlas { - image: FontImage, + image: ColorImage, /// What part of the image that is dirty dirty: Rectu, @@ -72,18 +73,22 @@ pub struct TextureAtlas { /// pre-rasterized discs of radii `2^i`, where `i` is the index. discs: Vec, + + /// Controls how to convert glyph coverage to alpha. + pub(crate) text_alpha_from_coverage: AlphaFromCoverage, } impl TextureAtlas { - pub fn new(size: [usize; 2]) -> Self { + pub fn new(size: [usize; 2], text_alpha_from_coverage: AlphaFromCoverage) -> Self { assert!(size[0] >= 1024, "Tiny texture atlas"); let mut atlas = Self { - image: FontImage::new(size), + image: ColorImage::filled(size, Color32::TRANSPARENT), dirty: Rectu::EVERYTHING, cursor: (0, 0), row_height: 0, overflowed: false, discs: vec![], // will be filled in below + text_alpha_from_coverage, }; // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: @@ -93,7 +98,7 @@ impl TextureAtlas { (0, 0), "Expected the first allocation to be at (0, 0), but was at {pos:?}" ); - image[pos] = 1.0; + image[pos] = Color32::WHITE; // Allocate a series of anti-aliased discs used to render small filled circles: // TODO(emilk): these circles can be packed A LOT better. @@ -116,7 +121,7 @@ impl TextureAtlas { let coverage = remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0); image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = - coverage; + text_alpha_from_coverage.color_from_coverage(coverage); } } atlas.discs.push(PrerasterizedDisc { @@ -184,7 +189,7 @@ impl TextureAtlas { /// The full font atlas image. #[inline] - pub fn image(&self) -> &FontImage { + pub fn image(&self) -> &ColorImage { &self.image } @@ -200,14 +205,14 @@ impl TextureAtlas { } else { let pos = [dirty.min_x, dirty.min_y]; let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y]; - let region = self.image.region(pos, size); + let region = self.image.region_by_pixels(pos, size); Some(ImageDelta::partial(pos, region, texture_options)) } } /// Returns the coordinates of where the rect ended up, /// and invalidates the region. - pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) { + pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut ColorImage) { /// On some low-precision GPUs (my old iPad) characters get muddled up /// if we don't add some empty pixels between the characters. /// On modern high-precision GPUs this is not needed. @@ -254,13 +259,15 @@ impl TextureAtlas { } } -fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool { +fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool { while required_height >= image.height() { image.size[1] *= 2; // double the height } if image.width() * image.height() > image.pixels.len() { - image.pixels.resize(image.width() * image.height(), 0.0); + image + .pixels + .resize(image.width() * image.height(), Color32::TRANSPARENT); true } else { false diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index 315437750..919b7405f 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - emath::NumExt as _, mutex::RwLock, textures::TextureOptions, ImageData, ImageDelta, TextureId, - TextureManager, + ImageData, ImageDelta, TextureId, TextureManager, emath::NumExt as _, mutex::RwLock, + textures::TextureOptions, }; /// Used to paint images. diff --git a/crates/epaint/src/textures.rs b/crates/epaint/src/textures.rs index cc191a75f..0944a9052 100644 --- a/crates/epaint/src/textures.rs +++ b/crates/epaint/src/textures.rs @@ -271,7 +271,7 @@ pub enum TextureWrapMode { /// What has been allocated and freed during the last period. /// /// These are commands given to the integration painter. -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "The painter must take care of this"] pub struct TexturesDelta { diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 709733613..f61b14cda 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.32.0 - 2025-07-10 +Nothing new + + ## 0.31.1 - 2025-03-05 Nothing new diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index f20af6faf..d7de7866c 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -3,8 +3,8 @@ name = "confirm_exit" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/confirm_exit/screenshot.png b/examples/confirm_exit/screenshot.png index 854955cf7..33debffe2 100644 --- a/examples/confirm_exit/screenshot.png +++ b/examples/confirm_exit/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3122591a76a063f1db0b88b54ecc4afe012ee27a1404c0948d0b9d639aeeece6 -size 3124 +oid sha256:0175461bbd86fffaad3538ea8dcec5001c7511aa201aa37b7736e7d2010b1522 +size 3483 diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index 98824d3a4..1d78565ec 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -3,8 +3,8 @@ name = "custom_3d_glow" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_3d_glow/screenshot.png b/examples/custom_3d_glow/screenshot.png index c3907a5ec..19bf33d64 100644 --- a/examples/custom_3d_glow/screenshot.png +++ b/examples/custom_3d_glow/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd5ffcf5a530f6fbe959fd74e2f5b4aeaee335baf79ad1cde8e42c34d84156c -size 24590 +oid sha256:b5f36e1df27007b19cf56771145a667b4410dc9f4697afe629a2b42518640d62 +size 26531 diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index da29460c7..86ce17bfb 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -3,8 +3,8 @@ name = "custom_font" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_font/screenshot.png b/examples/custom_font/screenshot.png index 7e6edc3dd..e7a20348d 100644 --- a/examples/custom_font/screenshot.png +++ b/examples/custom_font/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0ab12a55c0d87f044ef7675f5b94b39977f2c5d3a2ea74eb843dfc85d5b9f31 -size 5645 +oid sha256:a5bdb725a17bb6f8871ee95ae8cf46e55055083b0be14716cccd17615c863c81 +size 6179 diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index d7db7450a..b3cd82d2a 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -3,8 +3,8 @@ name = "custom_font_style" version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_font_style/screenshot.png b/examples/custom_font_style/screenshot.png index bb7727a1d..09f76bd13 100644 --- a/examples/custom_font_style/screenshot.png +++ b/examples/custom_font_style/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d443ef30cf12d2c4367135fd663270ed46f4864af2f3aae424610be86c73197 -size 66193 +oid sha256:044417e2259f58b8b71c44c49a60fe928e7faac1616d76eba7e156764c1bc2b2 +size 88583 diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index a866ae4d9..de66772da 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -3,8 +3,8 @@ name = "custom_keypad" version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs index 1934ff602..ddc675461 100644 --- a/examples/custom_keypad/src/keypad.rs +++ b/examples/custom_keypad/src/keypad.rs @@ -1,4 +1,4 @@ -use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2}; +use eframe::egui::{self, Button, Ui, Vec2, pos2, vec2}; #[derive(Clone, Copy, Debug, Default, PartialEq)] enum Transition { diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index e6571fa04..baae7bfc1 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -2,8 +2,8 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_style/src/main.rs b/examples/custom_style/src/main.rs index 1e78bea0f..58f7e1d3a 100644 --- a/examples/custom_style/src/main.rs +++ b/examples/custom_style/src/main.rs @@ -2,7 +2,7 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui::{ - self, global_theme_preference_buttons, style::Selection, Color32, Stroke, Style, Theme, + self, Color32, Stroke, Style, Theme, global_theme_preference_buttons, style::Selection, }; use egui_demo_lib::{View as _, WidgetGallery}; diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index b98ea27d2..74f07237e 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -3,8 +3,8 @@ name = "custom_window_frame" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/custom_window_frame/screenshot.png b/examples/custom_window_frame/screenshot.png index 92e11a695..bb327544a 100644 --- a/examples/custom_window_frame/screenshot.png +++ b/examples/custom_window_frame/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfdf77d119ae7926f52ac781455c4b1574eea53276069614738ba8b13b9921ea -size 6024 +oid sha256:1eba63346816dfbcf90c6b87e8df8b2de989d33359fda91041a813c474f85cc1 +size 17981 diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index bf347848f..bc7ddcb82 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -75,7 +75,7 @@ fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOn } fn title_bar_ui(ui: &mut egui::Ui, title_bar_rect: eframe::epaint::Rect, title: &str) { - use egui::{vec2, Align2, FontId, Id, PointerButton, Sense, UiBuilder}; + use egui::{Align2, FontId, Id, PointerButton, Sense, UiBuilder, vec2}; let painter = ui.painter(); diff --git a/examples/external_eventloop/Cargo.toml b/examples/external_eventloop/Cargo.toml index 301f30251..e14852930 100644 --- a/examples/external_eventloop/Cargo.toml +++ b/examples/external_eventloop/Cargo.toml @@ -3,8 +3,8 @@ name = "external_eventloop" version = "0.1.0" authors = ["Will Brown "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/external_eventloop/screenshot.png b/examples/external_eventloop/screenshot.png new file mode 100644 index 000000000..1236f4f84 --- /dev/null +++ b/examples/external_eventloop/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f2630b0cfe5b044698e9f9533752e23a130e1984dfa0123645bc040d412dba5 +size 8636 diff --git a/examples/external_eventloop/src/main.rs b/examples/external_eventloop/src/main.rs index 178c2865f..d72f6914a 100644 --- a/examples/external_eventloop/src/main.rs +++ b/examples/external_eventloop/src/main.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![allow(rustdoc::missing_crate_level_docs)] // it's an example -use eframe::{egui, UserEvent}; +use eframe::{UserEvent, egui}; use std::{cell::Cell, rc::Rc}; use winit::event_loop::{ControlFlow, EventLoop}; diff --git a/examples/external_eventloop_async/Cargo.toml b/examples/external_eventloop_async/Cargo.toml index 399ff7c39..5305bdbb3 100644 --- a/examples/external_eventloop_async/Cargo.toml +++ b/examples/external_eventloop_async/Cargo.toml @@ -3,8 +3,8 @@ name = "external_eventloop_async" version = "0.1.0" authors = ["Will Brown "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/external_eventloop_async/src/app.rs b/examples/external_eventloop_async/src/app.rs index de3326b19..a7b3e0efe 100644 --- a/examples/external_eventloop_async/src/app.rs +++ b/examples/external_eventloop_async/src/app.rs @@ -1,4 +1,4 @@ -use eframe::{egui, EframePumpStatus, UserEvent}; +use eframe::{EframePumpStatus, UserEvent, egui}; use std::{cell::Cell, io, os::fd::AsRawFd as _, rc::Rc, time::Duration}; use tokio::task::LocalSet; use winit::event_loop::{ControlFlow, EventLoop}; diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index 816686979..bf437a396 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -3,8 +3,8 @@ name = "file_dialog" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index b893f35e9..6e077c74f 100644 --- a/examples/hello_android/Cargo.toml +++ b/examples/hello_android/Cargo.toml @@ -3,8 +3,8 @@ name = "hello_android" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false # `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs index c91c0e754..c138b97e1 100644 --- a/examples/hello_android/src/lib.rs +++ b/examples/hello_android/src/lib.rs @@ -1,6 +1,6 @@ #![doc = include_str!("../README.md")] -use eframe::{egui, CreationContext}; +use eframe::{CreationContext, egui}; #[cfg(target_os = "android")] #[no_mangle] diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 0ad5ff844..4a5779b8c 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -3,8 +3,8 @@ name = "hello_world" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index dd980888b..ee3cb7c84 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -3,8 +3,8 @@ name = "hello_world_par" version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 57f0269e9..d5ce91625 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -3,8 +3,8 @@ name = "hello_world_simple" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/hello_world_simple/screenshot.png b/examples/hello_world_simple/screenshot.png index 8e5d56570..546366aea 100644 --- a/examples/hello_world_simple/screenshot.png +++ b/examples/hello_world_simple/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bb13d0cb819d30ffbcd12d9327589a1e3ca226943cb381fa17eccfb8f7c06fb -size 3229 +oid sha256:7be893df63405f3e86d1eb280083914a7ac63bb21fb8dce47e0476fb48ec9bd8 +size 8293 diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index d605e25e0..01b63625c 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -3,8 +3,8 @@ name = "images" version = "0.1.0" authors = ["Jan Procházka "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/images/screenshot.png b/examples/images/screenshot.png index 833b6565b..8dd58f2bd 100644 --- a/examples/images/screenshot.png +++ b/examples/images/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a836741d52e1972b2047cefaabf59f601637d430d4b41bf6407ebda4f7931dac -size 273450 +oid sha256:329972caa792f9e3a7caf207f41c35e1e26f0d09067e9282bf6538d560f13f7c +size 79617 diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index 00d1d591f..2ae7b0731 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -3,8 +3,8 @@ name = "keyboard_events" version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/keyboard_events/screenshot.png b/examples/keyboard_events/screenshot.png index ed6ba6837..3048a2c5c 100644 --- a/examples/keyboard_events/screenshot.png +++ b/examples/keyboard_events/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cf53e0da26c4295bafe9cbb9ea0ad8c50933e29eb67467c3a11783697ec5494 -size 7525 +oid sha256:45fce5a660dbca5a2ecca2fa93fa99b67dce7b03ad583fb5d44d2642d052a80c +size 8603 diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 995baa431..a672ea0db 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -3,8 +3,8 @@ name = "multiple_viewports" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/multiple_viewports/src/main.rs b/examples/multiple_viewports/src/main.rs index a8adab2c7..919c24a3a 100644 --- a/examples/multiple_viewports/src/main.rs +++ b/examples/multiple_viewports/src/main.rs @@ -2,8 +2,8 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::{ - atomic::{AtomicBool, Ordering}, Arc, + atomic::{AtomicBool, Ordering}, }; use eframe::egui; diff --git a/examples/popups/screenshot.png b/examples/popups/screenshot.png new file mode 100644 index 000000000..54b1df8c7 --- /dev/null +++ b/examples/popups/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e082670ac9daaee83e593c5dab0e3fccc6f6a4b824071f4983f7f51da144cad9 +size 17893 diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index df39fd456..536f6ae72 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -3,8 +3,8 @@ name = "puffin_profiler" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [package.metadata.cargo-machete] diff --git a/examples/puffin_profiler/screenshot.png b/examples/puffin_profiler/screenshot.png index 319a34730..fba55018a 100644 --- a/examples/puffin_profiler/screenshot.png +++ b/examples/puffin_profiler/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:610b003b4c7715751d2d3753d304e2c5a0af17c9259fa24f21a97b33b9d0c3c7 -size 8549 +oid sha256:0787ac28dc8a0ca979f9be5f20c3fb1e2e1c5733add61d201bfe965590afe062 +size 25999 diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs index b75a20e00..1386e4884 100644 --- a/examples/puffin_profiler/src/main.rs +++ b/examples/puffin_profiler/src/main.rs @@ -2,15 +2,21 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::{ - atomic::{AtomicBool, Ordering}, Arc, + atomic::{AtomicBool, Ordering}, }; use eframe::egui; fn main() -> eframe::Result { let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); - std::env::set_var("RUST_LOG", rust_log); + + // SAFETY: we call this from the main thread without any other threads running. + #[expect(unsafe_code)] + unsafe { + std::env::set_var("RUST_LOG", rust_log); + }; + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). start_puffin_server(); // NOTE: you may only want to call this if the users specifies some flag or clicks a button! diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 0288328fa..d633159cc 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -6,8 +6,8 @@ authors = [ "Andreas Faber "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index ff1a7538b..24d362e8c 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -3,8 +3,8 @@ name = "user_attention" version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/examples/user_attention/screenshot.png b/examples/user_attention/screenshot.png index 015bdf7ec..eaaeda498 100644 --- a/examples/user_attention/screenshot.png +++ b/examples/user_attention/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85a508fa7d16d9bc51f38d3531b30f024d8888f7f74bf2f351453fd13d13e0aa -size 5928 +oid sha256:1d723a89d05be6e254846b8999041c54d5142baefcb8ad8c1effa5a50bae7791 +size 6578 diff --git a/examples/user_attention/src/main.rs b/examples/user_attention/src/main.rs index 3d171c279..6851b736e 100644 --- a/examples/user_attention/src/main.rs +++ b/examples/user_attention/src/main.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![allow(rustdoc::missing_crate_level_docs)] // it's an example -use eframe::{egui, CreationContext, NativeOptions}; +use eframe::{CreationContext, NativeOptions, egui}; use egui::{Button, CentralPanel, Context, UserAttentionType}; use std::time::{Duration, SystemTime}; diff --git a/rust-toolchain b/rust-toolchain index 6c3ca5854..71db6497f 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.84.0" +channel = "1.85.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index f232dfbb6..0207df1cc 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -# cargo +1.84.0 install --quiet typos-cli +# cargo +1.85.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index 17d2a8cd6..e91b18abc 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.84" +msrv = "1.85" allow-unwrap-in-tests = true diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 7e3ae4284..61986ee80 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -192,7 +192,9 @@ def remove_prefix(text, prefix): def print_section(heading: str, content: str) -> None: if content != "": print(f"## {heading}") - print(content) + print(content.strip()) + print() + print() print() @@ -345,11 +347,8 @@ def main() -> None: print() for crate in crate_names: if crate in crate_sections: - prs = crate_sections[crate] - print_section(crate, changelog_from_prs(prs, crate)) - print() + print_section(crate, changelog_from_prs(crate_sections[crate], crate)) print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs])) - print() print_section( "Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits]) ) diff --git a/scripts/generate_example_screenshots.sh b/scripts/generate_example_screenshots.sh index 91e338d31..e2ed2361e 100755 --- a/scripts/generate_example_screenshots.sh +++ b/scripts/generate_example_screenshots.sh @@ -7,10 +7,12 @@ cd "$script_path/.." cd examples for EXAMPLE_NAME in $(ls -1d */ | sed 's/\/$//'); do - if [ ${EXAMPLE_NAME} != "hello_world_par" ] && # screenshot not implemented for wgpu backend + if [ ${EXAMPLE_NAME} != "external_eventloop_async" ] && + [ ${EXAMPLE_NAME} != "hello_android" ] && + [ ${EXAMPLE_NAME} != "hello_world_par" ] && # screenshot not implemented for wgpu backend [ ${EXAMPLE_NAME} != "multiple_viewports" ] && - [ ${EXAMPLE_NAME} != "screenshot" ] && [ ${EXAMPLE_NAME} != "puffin_viewer" ] && + [ ${EXAMPLE_NAME} != "screenshot" ] && [ ${EXAMPLE_NAME} != "serial_windows" ]; then echo "" diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh new file mode 100755 index 000000000..c2f2fc24e --- /dev/null +++ b/scripts/publish_crates.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +(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_glow && cargo publish --quiet) && echo "✅ egui_glow" +(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" diff --git a/scripts/update_snapshots_from_ci.sh b/scripts/update_snapshots_from_ci.sh index c42ffb0fd..c15360365 100755 --- a/scripts/update_snapshots_from_ci.sh +++ b/scripts/update_snapshots_from_ci.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# This script searches for the last CI run with your branch name, downloads the test_results artefact +# This script searches for the last CI run with your branch name, downloads the test_results artifact # and replaces your existing snapshots with the new ones. # Make sure you have the gh cli installed and authenticated before running this script. # If prompted to select a default repo, choose the emilk/egui one diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d67759427..9a33c9359 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,6 +1,6 @@ -use egui::{include_image, Image}; -use egui_kittest::kittest::Queryable as _; +use egui::{Image, include_image}; use egui_kittest::Harness; +use egui_kittest::kittest::Queryable as _; #[test] fn image_button_should_have_alt_text() { diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index 7dbda11d9..f15fb0ce6 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad14068e60fa678ee749925dd3713ee2b12a83ec1bca9c413bdeb9bc27d8ac20 -size 407795 +oid sha256:d59882afca42e766dddc36450a3331ca247a130e3796f99d0335ac370a7c3610 +size 425517 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long.png b/tests/egui_tests/tests/snapshots/sides/default_long.png new file mode 100644 index 000000000..2d66f3665 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7ceaa95512c67dcbf1c8ba5a8f33bf4833c2e863d09903fb71b5aa2822cc086 +size 7889 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short.png b/tests/egui_tests/tests/snapshots/sides/default_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png new file mode 100644 index 000000000..39e1bab98 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88e1557dffa7295e7e7e37ed175fcec40aab939f9b67137a1ce33811e8ae4722 +size 7148 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png new file mode 100644 index 000000000..3326d9527 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508209ca303751ef323301b25bb3878410742ea79339b75363d2681b98d2712b +size 7068 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png new file mode 100644 index 000000000..36929a413 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c9e39c18fc5bb1fc02a86dbf02e3ffca5537dbe8986d5c5b50cb4984c97466 +size 9085 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png new file mode 100644 index 000000000..47398293f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e9ba0acb573853ef5b3dedb1156d99cdf80338ccb160093960e8aaa41bd5df +size 9048 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png index 8c8e9630c..364f7771f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button.png +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f64e581b97df6694cb7c85ee7728a955e3c1a851ab660e8b6091eee1885bbe -size 9719 +oid sha256:a573976aacbb629c88c285089fca28ba7998a0c28ecee9f783920d67929a1e2d +size 9735 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index c71c2aeb0..c38571a6e 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d39ec25b91f5f5d68305d2cb7cc0285d715fe30ccbd66369efbe7327d1899b52 -size 10753 +oid sha256:9fbb9aca2006aeca555c138f1ebdb89409026f1bed48da74cd0fa03dcd8facbe +size 10746 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 42f8ff02a..4be868a30 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46d86987ba895ead9b28efcc37e1b4374f34eedebac83d1db9eaa8e5a3202ee3 -size 13203 +oid sha256:8ff776897760d300a4f26c10578be0d9afed7b4ae9f95f941914e641c2a10cb8 +size 13798 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 114baa35d..ffabcae40 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9151f1c9d8a769ac2143a684cabf5d9ed1e453141fff555da245092003f1df1 -size 13563 +oid sha256:9cd6a7f38c876cc345eae1a5e01f7668d4642b70181198fe0f09570815e47da8 +size 13489 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png index 40852f3c2..113839a2f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e03cf99a3d28f73d4a72c0e616dc54198663b94bf5cffda694cf4eb4dee01be8 -size 13445 +oid sha256:ec75c3fccec8d6a72b808aba593f8c289618b6f95db08eb3cdb20a255b9d986e +size 13450 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index dbe3c13b6..56b5bb0e3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e86a37c7b259a6bad61897545d927d75e8307916dc78d256e4d33c410fcd6876 -size 7306 +oid sha256:c7e66a490236b306ce03c504d29490cdadc3708a79e21e3b46d11df8eb22a26b +size 7309 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png index a42ad5012..8e89197e1 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a172cfadc91467529e5546e686673be73ba0071a55d55abc7a41fb1d07214d -size 11700 +oid sha256:895914fa37608ff68c5ae7fdd22d0363da26907c78d4980f6bf1ed19f7e5f388 +size 11697 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png index 81f995515..67d80fed3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ef91dfedc74cae59099bce32b2e42cb04649e84442e8010282a9c1ff2a7f2c8 -size 12469 +oid sha256:0e0c4277eebadb0c350b5110d5ea7ff9292ab2b0231d6b36e9ada3aeefc7c198 +size 12510 diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png index 6c8348559..7e868c0e7 100644 --- a/tests/egui_tests/tests/snapshots/visuals/slider.png +++ b/tests/egui_tests/tests/snapshots/visuals/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1892358a4552af3f529141d314cd18e4cf55a629d870798278a5470e3e0a8a94 -size 11030 +oid sha256:ec09e0e3432668c0d08bbba0aa8608c4eefba33d57f2335fdf105d144791406d +size 11036 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 5f2a64b8d..1e1e4d394 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7300a0b88d4fdb6c1e543bfaf50e8964b2f84aaaf8197267b671d0cf3c8da30a -size 7033 +oid sha256:9353e6d39d309e7a6e6c0a17be819809c2dbea8979e9e73b3c73b67b07124a36 +size 7031 diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index abc9f2d05..cf2abbe1a 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -69,3 +69,42 @@ fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult { harness.try_snapshot(name) } + +#[test] +fn test_intrinsic_size() { + let widgets = [Ui::button, Ui::label]; + + for widget in widgets { + let mut intrinsic_size = None; + for wrapping in [ + TextWrapMode::Extend, + TextWrapMode::Wrap, + TextWrapMode::Truncate, + ] { + _ = HarnessBuilder::default() + .with_size(Vec2::new(100.0, 100.0)) + .build_ui(|ui| { + ui.style_mut().wrap_mode = Some(wrapping); + let response = widget( + ui, + "Hello world this is a long text that should be wrapped.", + ); + if let Some(current_intrinsic_size) = intrinsic_size { + assert_eq!( + Some(current_intrinsic_size), + response.intrinsic_size, + "For wrapping: {wrapping:?}" + ); + } + assert!( + response.intrinsic_size.is_some(), + "intrinsic_size should be set for `Button`" + ); + intrinsic_size = response.intrinsic_size; + if wrapping == TextWrapMode::Extend { + assert_eq!(Some(response.rect.size()), response.intrinsic_size); + } + }); + } + } +} diff --git a/tests/egui_tests/tests/test_sides.rs b/tests/egui_tests/tests/test_sides.rs new file mode 100644 index 000000000..52d35db4c --- /dev/null +++ b/tests/egui_tests/tests/test_sides.rs @@ -0,0 +1,76 @@ +use egui::{TextWrapMode, Vec2, containers::Sides}; +use egui_kittest::{Harness, SnapshotResults}; + +#[test] +fn sides_container_tests() { + let mut results = SnapshotResults::new(); + + test_variants("default", |sides| sides, &mut results); + + test_variants( + "shrink_left", + |sides| sides.shrink_left().truncate(), + &mut results, + ); + + test_variants( + "shrink_right", + |sides| sides.shrink_right().truncate(), + &mut results, + ); + + test_variants( + "wrap_left", + |sides| sides.shrink_left().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); + + test_variants( + "wrap_right", + |sides| sides.shrink_right().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); +} + +fn test_variants( + name: &str, + mut create_sides: impl FnMut(Sides) -> Sides, + results: &mut SnapshotResults, +) { + for (variant_name, left_text, right_text, fit_contents) in [ + ("short", "Left", "Right", false), + ( + "long", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + false, + ), + ("short_fit_contents", "Left", "Right", true), + ( + "long_fit_contents", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + true, + ), + ] { + let mut harness = Harness::builder() + .with_size(Vec2::new(400.0, 50.0)) + .build_ui(|ui| { + create_sides(Sides::new()).show( + ui, + |left| { + left.label(left_text); + }, + |right| { + right.label(right_text); + }, + ); + }); + + if fit_contents { + harness.fit_contents(); + } + + results.add(harness.try_snapshot(format!("sides/{name}_{variant_name}"))); + } +} diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 110eff810..a6050e95b 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,11 +1,11 @@ use egui::load::SizedTexture; use egui::{ - include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, - StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, + Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event, + Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, + TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image, }; -use egui_kittest::kittest::{by, Node, Queryable as _}; -use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; +use egui_kittest::kittest::{Queryable as _, by}; +use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; #[test] fn widget_tests() { @@ -244,7 +244,7 @@ fn test_widget_layout(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> Sna }); harness.fit_contents(); - harness.try_snapshot(&format!("layout/{name}")) + harness.try_snapshot(format!("layout/{name}")) } /// Utility to create a snapshot test of the different states of a egui widget. @@ -278,14 +278,10 @@ impl<'a> VisualTests<'a> { }); self.add("pressed", |harness| { harness.get_next().hover(); - let rect = harness.get_next().bounding_box().unwrap(); - let pos = Pos2::new( - ((rect.x0 + rect.x1) / 2.0) as f32, - ((rect.y0 + rect.y1) / 2.0) as f32, - ); + let rect = harness.get_next().rect(); harness.input_mut().events.push(Event::PointerButton { button: PointerButton::Primary, - pos, + pos: rect.center(), pressed: true, modifiers: Default::default(), }); @@ -374,7 +370,7 @@ impl<'a> VisualTests<'a> { harness.fit_contents(); - harness.try_snapshot(&format!("visuals/{}", self.name)) + harness.try_snapshot(format!("visuals/{}", self.name)) } } diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index 0f5e8f50a..6058a1865 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -2,8 +2,8 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 7187c599a..5171700ea 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -3,8 +3,8 @@ name = "test_inline_glow_paint" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index 280a40c1b..6dbdfe1c0 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -3,8 +3,8 @@ name = "test_size_pass" version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index e4dd35f4d..a826324a0 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -3,8 +3,8 @@ name = "test_ui_stack" version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index e8c8bc754..9a64b485d 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -3,8 +3,8 @@ name = "test_viewports" version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.84" +edition = "2024" +rust-version = "1.85" publish = false [lints] diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 3f9964c94..9b7876323 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use eframe::egui; -use egui::{mutex::RwLock, Id, InnerResponse, UiBuilder, ViewportBuilder, ViewportId}; +use egui::{Id, InnerResponse, UiBuilder, ViewportBuilder, ViewportId, mutex::RwLock}; // Drag-and-drop between windows is not yet implemented, but if you wanna work on it, enable this: pub const DRAG_AND_DROP_TEST: bool = false;