Compare commits

...

127 commits

Author SHA1 Message Date
Emil Ernerfeldt
7215fdfb7c Release eframe 0.21.3 - fix web input of the the letter P 2023-02-15 08:26:45 +01:00
Emil Ernerfeldt
e2778d9d6a
eframe: Fix inputting of the letter P on web (#2740)
* eframe: Fix inputting of the letter P on web

* Update changelog

* silence clippy
2023-02-15 08:24:52 +01:00
Emil Ernerfeldt
38849fe381 Release eframe 0.21.2 - support --no-default-features 2023-02-12 19:34:37 +01:00
Emil Ernerfeldt
df7e5bd57a
Allow compiling eframe with --no-default-features (#2728)
* Check that we can compile eframe with --no-default-features

* Allow compiling eframe with `--no-default-features`

This is useful for libraries that depend on `eframe::Frame`
but don't care what renderer eframe is using.
2023-02-12 19:29:42 +01:00
Emil Ernerfeldt
e3e781ced8 fix puffin_profiler example 2023-02-12 19:27:10 +01:00
Emil Ernerfeldt
97756bc246 Add badges to all crates' README.md 2023-02-12 18:08:13 +01:00
Emil Ernerfeldt
f71d79a0ff Release egui-winit, eframe 0.21.1 - fix crash on monitor size/dpi change
https://github.com/emilk/egui/pull/2722

The crash could happen if the monitor size or DPI changes between runs
2023-02-12 15:29:47 +01:00
Aevyrie
95247daa17
Fix window position assertion caused by negative window size (#2722) 2023-02-12 15:14:38 +01:00
Emil Ernerfeldt
530e9f667c format: add some blank lines where it was needed 2023-02-10 18:03:46 +01:00
Emil Ernerfeldt
409fb968d3 add cargo config file that sets --cfg=web_sys_unstable_apis on wasm32 2023-02-10 17:56:10 +01:00
Emil Ernerfeldt
1581f0229e examples/README.md: explain that the examples are for master
and add a link to the latest release
2023-02-08 20:34:43 +01:00
Emil Ernerfeldt
ae722ab0cf Release 0.21.0 - Deadlock fix and winit update 2023-02-08 20:11:21 +01:00
Emil Ernerfeldt
1384fa3287 Publish new web demo 2023-02-08 20:10:12 +01:00
Emil Ernerfeldt
83b5b81227 Update changelogs with recent additions 2023-02-08 19:36:33 +01:00
Emil Ernerfeldt
63fa3aec10 Update example screenshots 2023-02-08 19:04:04 +01:00
Emil Ernerfeldt
ebeb788b1f
We no longer use tts, so remove speech-related dependencies (#2698) 2023-02-08 18:45:44 +01:00
Emil Ernerfeldt
0fc25c2680 Fix: make sure always_on_top is respected on glow again 2023-02-08 18:12:52 +01:00
Emil Ernerfeldt
449dd1c23c
cargo update (#2697)
* cargo update

    Updating anyhow v1.0.68 -> v1.0.69
    Updating bindgen v0.63.0 -> v0.64.0
    Removing cocoa v0.24.1
    Updating glutin v0.30.3 -> v0.30.4
    Updating glutin_egl_sys v0.3.1 -> v0.4.0
    Updating glutin_glx_sys v0.3.0 -> v0.4.0
    Updating glutin_wgl_sys v0.3.0 -> v0.4.0
    Updating proc-macro2 v1.0.50 -> v1.0.51
    Updating rgb v0.8.34 -> v0.8.35
    Updating serde_json v1.0.91 -> v1.0.92
    Updating tiny-skia v0.8.2 -> v0.8.3
    Updating tiny-skia-path v0.8.2 -> v0.8.3
    Removing windows-sys v0.36.1
    Removing windows_aarch64_msvc v0.36.1
    Removing windows_i686_gnu v0.36.1
    Removing windows_i686_msvc v0.36.1
    Removing windows_x86_64_gnu v0.36.1
    Removing windows_x86_64_msvc v0.36.1
    Updating zbus v3.8.0 -> v3.9.0
    Updating zbus_macros v3.8.0 -> v3.9.0

* Update wasm-bindgen to 0.2.84

* cargo update

    Updating js-sys v0.3.60 -> v0.3.61
    Updating wasm-bindgen-futures v0.4.33 -> v0.4.34
    Updating web-sys v0.3.60 -> v0.3.61

* Update pollster to 0.3

* Update rfd to 0.11

* Make sure we never depend on cmake
2023-02-08 18:11:34 +01:00
Emil Ernerfeldt
636a39cbe1
Update glow to 0.12 (#2695)
* Remove three-d example due to glow incompatibility

* Update to glow 0.12

* Remove three-d from deny.toml

* Add line to changelog
2023-02-08 17:16:44 +01:00
Red Artist
be9b5a3641
polish glutin upgrade with glutin-winit crate (#2526)
* use glutin-winit for glow context creation

* added some tracing for easier debugging of glutin problems

* fmt

* add more debug logs

* more tracing

* fallback egl instead of prefer egl

* update pure glow example to use glutin_winit

* add more logging. ignore vsync option if not supported

* cranky lint

* add some logging for easier debugging

* drop window after glutin surface

* small changes based on pr review

* build fix

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-08 14:28:42 +01:00
Emil Ernerfeldt
e8b9e706ca
Fix Window::pivot causing windows to move around (#2694)
* Fix Window::pivot causing windows to move around

* Add line to changelog
2023-02-08 12:41:36 +01:00
Emil Ernerfeldt
a8d5a82a7f Lowe multisampling in examples from 8 to 4
Closes https://github.com/emilk/egui/issues/2658
2023-02-08 10:14:43 +01:00
Emil Ernerfeldt
c2d37571f7 constrain popups to the screen 2023-02-08 10:02:50 +01:00
Emil Ernerfeldt
90cd178117 Document GlyphInfo::id 2023-02-08 10:01:47 +01:00
Emil Ernerfeldt
7397be3401 Fix item spacing in CollapsingHeader 2023-02-08 10:01:37 +01:00
Emil Ernerfeldt
1edd333864 Update to winit 0.28.1 2023-02-08 10:00:03 +01:00
Harrison Gieraltowski
b40dba18c6
DragValue: when keyboard editing, only update the value on focus lost (#2688)
* test

* moved some accesskit stuff

* reverted accesskit change

* Add explanatory comment

* fmt

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-08 09:35:44 +01:00
Emil Ernerfeldt
4721a0a680
Check --all-targets in CI (#1395) 2023-02-08 09:35:01 +01:00
Emil Ernerfeldt
8d6c2580f4 Add Checkbox::without_text
Closes https://github.com/emilk/egui/pull/2508
2023-02-08 08:42:00 +01:00
lictex_
8bc88c9bf4
make dragvalue textedit style consistent with button (#2649)
* make dragvalue textedit style consistent with button

* fix comments &  fix wrong interactive cursor pos

* * apply button_padding to textedit
* support vertical align
* add same min size as button to avoid unintented height shrink
2023-02-07 10:52:48 +01:00
Andreas Reich
b52cd2052f
Support for transparent backbuffer in wgpu winit binding (#2684)
* Support for transparent backbuffer in wgpu winit binding
Choose best fitting composite alpha mode on the fly.

* Compilation fix

* Add line to eframe CHANGELOG

* Attempt to mollify CI: try different way to install apt packages

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-06 14:16:17 +01:00
Emil Ernerfeldt
b1e214bbdf Add Order::TOP 2023-02-06 08:48:12 +01:00
Emil Ernerfeldt
cef0c0b6d8 Fix typo 2023-02-05 21:58:15 +01:00
Emil Ernerfeldt
d5dcc87ace Improve custom_window_frame 2023-02-05 21:58:15 +01:00
Matt Campbell
853d492724
Update for AccessKit refactor that drastically reduces memory usage (#2678)
* Update for AccessKit refactor that drastically reduces memory usage

* changelog entry

* satisfy clippy
2023-02-05 19:10:40 +01:00
Emil Ernerfeldt
d15ce22e2e winit: Fix bug where the cursor could get stuck using the wrong icon 2023-02-05 08:48:40 +01:00
hinto-janaiyo
628c84cbee
Add trailing_fill() toggle to Slider (#2660)
* slider: add trailing_color toggle

* slider/visuals: add global option in visuals with override toggle

* slider: add to demos

* use `.unwrap_or_else()` instead of match
2023-02-05 08:17:58 +01:00
Luc (Echow) Varoqui
212656f3fc
Fix set_plot_bounds (#2653) 2023-02-04 16:17:15 +01:00
Emil Ernerfeldt
660566c499
eframe: ask if the window is minimized or maximized (#2672)
* eframe: ask if the window is minimized or maximized

* Improve note
2023-02-04 16:05:23 +01:00
Hoping White
430cbe541c
New feature to support unity vertex layout requirement (#2493)
* Update mesh.rs

adjust Vertex layout

* add unity feature

* add unity feature

* document the `unity` feature flag

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-04 15:33:41 +01:00
Sheldon M
53f8e4049f
Position persistence and sane clamping to still-available monitors for Windows (#2583)
* Attempt to fix monitor clamping on Windows so window positions can be restored between sessions.

* Missed a change.

* Renamed variables, reorganized some lines of code, and added some more comments.

* Cargo fmt run

* Updated CHANGELOG.md to briefly describe my change

* Updated CHANGELOG.md to briefly describe my change

* Applied suggested fixes from emilk
Discovered an issue where putting the monitor off a non-primary monitor to the left causes the position to be off the monitor x and y range, clamping to the primary instead of the non-primary.

* Fix for matching negative restored window positions. Should clamp if any part of the window had been visible on a remaining monitor.

* Apparently compiler attributes on statements have been marked unstable.
Rather than just wrap in blocks, I kind of prefer the more explicit if cfg! call for line 114.

CHANGELOG.md - correct a missing paren I noticed

* I was being silly, I don't need to clone inner_size_points on line 112

* Cargo fmt run

* Update crates/egui-winit/CHANGELOG.md

emilk suggested changelog formatting

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Update window_settings.rs

Satisfy CI Error

* clippy

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-04 15:33:32 +01:00
SunDoge
f0718a61d3
eframe: add set_minimized and set_maximized (#2292)
* add actions for window controls

* add maximized to WindowInfo
update button text
fix clippy

* add overlap icon when maximized

* remove argument `app`

* remove WindowInfo { maximized }

* Update minimum window size

* Double-click titlebar to toggle maximized state

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-04 14:42:42 +01:00
tlhenvironment
fb5cb3052d
Fix typo in docstring (#2643)
Tiny typo fix <3
2023-02-04 13:53:20 +01:00
itfanr
9c270448a6
Add link to tauri-egui in README.md
add a 3rd party integration: tauri-egui for tauri
2023-02-04 13:52:47 +01:00
lictex_
d15f4ef992
select all text when dragvalue gets focus (#2650) 2023-02-04 13:46:16 +01:00
Emil Ernerfeldt
21a59143a4
cargo update (#2671)
* cargo update

    Updating crates.io index
    Updating ab_glyph v0.2.18 -> v0.2.20
    Updating ab_glyph_rasterizer v0.1.7 -> v0.1.8
    Updating ahash v0.8.2 -> v0.8.3
    Updating anyhow v1.0.66 -> v1.0.68
    Updating async-broadcast v0.4.1 -> v0.5.0
    Updating async-recursion v1.0.0 -> v1.0.2
    Updating async-trait v0.1.59 -> v0.1.64
    Updating atomic_refcell v0.1.8 -> v0.1.9
    Updating bumpalo v3.11.1 -> v3.12.0
    Updating bytemuck v1.12.3 -> v1.13.0
    Updating bytemuck_derive v1.3.0 -> v1.4.0
    Updating bytes v1.3.0 -> v1.4.0
    Updating calloop v0.10.3 -> v0.10.5
    Updating cc v1.0.77 -> v1.0.79
    Removing chunked_transfer v1.4.0
    Updating clipboard-win v4.4.2 -> v4.5.0
    Updating concurrent-queue v2.0.0 -> v2.1.0
    Updating cxx v1.0.83 -> v1.0.89
    Updating cxx-build v1.0.83 -> v1.0.89
    Updating cxxbridge-flags v1.0.83 -> v1.0.89
    Updating cxxbridge-macro v1.0.83 -> v1.0.89
    Updating document-features v0.2.6 -> v0.2.7
    Updating dyn-clone v1.0.9 -> v1.0.10
    Updating either v1.8.0 -> v1.8.1
    Updating enum-map v2.4.1 -> v2.4.2
    Updating enum-map-derive v0.10.0 -> v0.11.0
    Updating futures-core v0.3.25 -> v0.3.26
    Updating futures-io v0.3.25 -> v0.3.26
    Updating futures-sink v0.3.25 -> v0.3.26
    Updating futures-task v0.3.25 -> v0.3.26
    Updating futures-util v0.3.25 -> v0.3.26
    Updating glob v0.3.0 -> v0.3.1
    Updating heck v0.4.0 -> v0.4.1
    Updating image v0.24.4 -> v0.24.5
    Updating itoa v1.0.4 -> v1.0.5
    Updating jpeg-decoder v0.2.6 -> v0.3.0
    Updating js-sys v0.3.60 -> v0.3.61
    Updating libc v0.2.138 -> v0.2.139
    Updating link-cplusplus v1.0.7 -> v1.0.8
    Updating nom v7.1.1 -> v7.1.3
      Adding nom8 v0.2.0
    Updating num_enum v0.5.7 -> v0.5.9
    Updating num_enum_derive v0.5.7 -> v0.5.9
    Updating once_cell v1.16.0 -> v1.17.0
    Updating ordered-stream v0.1.2 -> v0.1.4
    Updating owned_ttf_parser v0.17.1 -> v0.18.1
    Updating parking_lot_core v0.9.5 -> v0.9.7
    Updating paste v1.0.9 -> v1.0.11
    Updating plist v1.3.1 -> v1.4.0
    Updating polling v2.5.1 -> v2.5.2
    Updating proc-macro-crate v1.2.1 -> v1.3.0
    Updating proc-macro2 v1.0.47 -> v1.0.50
    Updating puffin v0.14.0 -> v0.14.2
    Updating puffin_http v0.11.0 -> v0.11.1
      Adding quick-xml v0.26.0
    Updating quote v1.0.21 -> v1.0.23
    Updating regex v1.7.0 -> v1.7.1
    Updating rustls v0.20.7 -> v0.20.8
    Updating ryu v1.0.11 -> v1.0.12
    Updating scratch v1.0.2 -> v1.0.3
    Updating serde v1.0.149 -> v1.0.152
    Updating serde_derive v1.0.149 -> v1.0.152
    Updating serde_json v1.0.89 -> v1.0.91
    Updating serde_repr v0.1.9 -> v0.1.10
    Updating syn v1.0.105 -> v1.0.107
    Updating termcolor v1.1.3 -> v1.2.0
    Updating thiserror v1.0.37 -> v1.0.38
    Updating thiserror-impl v1.0.37 -> v1.0.38
    Updating tinyvec_macros v0.1.0 -> v0.1.1
    Updating toml v0.5.9 -> v0.5.11
      Adding toml_datetime v0.5.1
      Adding toml_edit v0.18.1
    Updating ttf-parser v0.17.1 -> v0.18.1
    Updating typenum v1.15.0 -> v1.16.0
    Updating unicode-bidi v0.3.8 -> v0.3.10
    Updating unicode-ident v1.0.5 -> v1.0.6
    Updating ureq v2.5.0 -> v2.6.2
    Updating wasm-bindgen v0.2.83 -> v0.2.84
    Updating wasm-bindgen-backend v0.2.83 -> v0.2.84
    Updating wasm-bindgen-futures v0.4.33 -> v0.4.34
    Updating wasm-bindgen-macro v0.2.83 -> v0.2.84
    Updating wasm-bindgen-macro-support v0.2.83 -> v0.2.84
    Updating wasm-bindgen-shared v0.2.83 -> v0.2.84
    Updating wayland-sys v0.30.0 -> v0.30.1
    Updating web-sys v0.3.60 -> v0.3.61
    Updating webbrowser v0.8.6 -> v0.8.7
    Updating webpki-roots v0.22.5 -> v0.22.6
    Updating which v4.3.0 -> v4.4.0
    Updating x11-dl v2.20.1 -> v2.21.0
    Updating zbus v3.6.2 -> v3.8.0
    Updating zbus_macros v3.6.2 -> v3.8.0
    Updating zbus_names v2.4.0 -> v2.5.0
    Updating zstd-sys v2.0.4+zstd.1.5.2 -> v2.0.6+zstd.1.5.2
    Updating zvariant v3.9.0 -> v3.10.0
    Updating zvariant_derive v3.9.0 -> v3.10.0

* Remove unnecessary import of wasm_bindgen::JsCast (its now in prelude)

* egui_glow/README.md: add line on how to run the example

* revert wasm-bindgen update

* Revert "Remove unnecessary import of wasm_bindgen::JsCast (its now in prelude)"

This reverts commit 95c3076cce76577d9f0f35e48f99b4acd2dbe62e.
2023-02-04 13:41:34 +01:00
Emil Ernerfeldt
06d753c289
Update SVG crates (#2670)
* Update resvg, tiny-skia and usv to latest

* Remove default features from svg crates

Users can always opt-in to them themselves

* Update png 0.17.6 -> 0.17.7
2023-02-04 13:00:04 +01:00
Emil Ernerfeldt
8344e88f8a
Update to to winit 0.28 (#2654)
* Update to winit 0.28

Mac trackpads pinch gestures will now generate `egui::Event::Zoom`

* Update accesskit_winit

* Try to get Android CI green

* Fix wayland compilation

* Add comment about android-activity

* Update changelogs

* Fix call to register_xlib_error_hook
2023-02-04 12:43:43 +01:00
Emil Ernerfeldt
e4eaf99072
Remove native screen reader support (#2669)
* Remove native screen reader support

Use the "accesskit" feature flag to `eframe` instead.
[AccessKit](https://github.com/AccessKit/accesskit) is the future.

`tts` was a mess to compile on many platforms, so I no longer want
the native `tts` dependency.

* Update tts to 0.25

* Update changelogs

* Turn on all feature flags for package.metadata.docs.rs

* remove tts from deny.toml skip-tree

* Update web build scripts

* Update deny.toml
2023-02-04 11:47:36 +01:00
Emil Ernerfeldt
1353a5733f
Deprecate egui_glium - looking for new maintainer (#2668)
* Deprecate egui_glium - looking for new maintainer

egui_glium was the first backend of egui, and it served us well for
a long time, but we have long since moved on to glow and wgpu.

Not egui_glium is holding back an update to latest winit.

Since development on glium has long since been discontinued I will
therefore deprecate egui_glium with this PR.

The code is still there in the repository for a while longer,
but is no longer compiled.

If there is any interest in maintaining egui_glium, then fork it and
make a PR to remove the last egui_glium from this repository.
I will give you publish rights on crates.io.

* update glutin 0.30.2 -> 0.30.3

* cargo update -p backtrace

    Updating crates.io index
    Updating addr2line v0.17.0 -> v0.19.0
    Updating backtrace v0.3.66 -> v0.3.67
    Updating gimli v0.26.2 -> v0.27.1
    Removing miniz_oxide v0.5.4
    Updating object v0.29.0 -> v0.30.3

* cargo deny: allow duplicates of windows-sys, wayland-sys, and nix

* cargo-deny whitelist tiny-skia
2023-02-04 11:21:02 +01:00
Andreas Reich
8aa07e9d43
Clear color values are now explicitely sent to the rendering backend as-is. (#2666)
* Clear color values are not explicitely sent to the rendering backend as-is.
Previously, converting from Color32 to Rgba caused an srgb->linear conversion. This conversion is incorrect if the backbuffer doesn't perform automatic conversion from linear->srgb (lack of this conversion is generally what egui assumes!).

* fill in pr numbers in changelog

* Epi comment fix

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Color32 comment fix

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* move changelog line

* rename fix

* use backticks in doc

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-02-04 11:02:15 +01:00
Emil Ernerfeldt
a6b60d5d58
Control Separator widths, and less clipping in ScrollArea (#2665)
* Add Separator::grow and Separator::shrink

* Be more conservative with the clipping in ScrollArea:s

* Add test of the growing separator

* Improve test output

* Update changelog

* Add back a little bit more clipping

* Make the minimum scroll handle length a bit longer

* More clip rect tweaks
2023-02-03 13:19:25 +01:00
John Hughes
8c59888ebd
Add simple Windows CI workflow check (#2663)
* Add simple Windows CI workflow check

* Fix Windows build in CI

* Windows fix
2023-02-03 13:19:12 +01:00
NemuiSen
5725868b57
ggez-egui was renamed to ggegui (#2662) 2023-02-03 10:59:28 +01:00
Emil Ernerfeldt
8c3d8b3ba5 Make sure the panel resize lines are visible when hovered/resizing 2023-02-03 09:31:24 +01:00
Emil Ernerfeldt
312cab5355 Export egui::Margin 2023-02-03 09:31:04 +01:00
Emil Ernerfeldt
5444ab269a Add Response::with_new_rect 2023-02-02 21:12:06 +01:00
Emil Ernerfeldt
d01e4342f0
Update accesskit_winit and dark_light (#2655)
* Update accesskit_winit to 0.8.1

* Update dark_light to 1.0
2023-01-31 10:41:27 +01:00
Emil Ernerfeldt
cc20dcb9d0
eframe: Improve window centering (#2651)
* Create the winit::Window in one unified place

* Fix wrong unit of initial_window_pos

* Improve centering
2023-01-30 15:52:30 +01:00
Emil Ernerfeldt
f222ee044e Fix for highlighting SelectableLabel 2023-01-28 00:21:53 +01:00
Emil Ernerfeldt
c72bdb77b5
Add ability to highlight any widget (#2632)
* Add ability to highlight any widget

* Add line to changelog

* Demote the demo to a test
2023-01-27 23:36:14 +01:00
Emil Ernerfeldt
e7c0547e23
DragValue and Slider text is now proportional instead of monospace (#2638)
* DragValue and Slider text is now proportional instead of monospace

Control with `Style::drag_value_text_style`

* Update changelog
2023-01-27 23:30:20 +01:00
Emil Ernerfeldt
fe7ff66266
Make the line left of indented regions optional (#2636)
* Make the line left of indented regions optional

Controlled with Visuals::indent_has_left_vline

* Add line to changelog

* Fix doclink
2023-01-27 11:12:08 +01:00
Red Artist
ce62b61e15
wgpu upgraded to 0.15. demo working on linux (#2629) 2023-01-27 09:19:47 +01:00
Emil Ernerfeldt
4a0bafbeab
Update webbrowser (#2631)
* Update webbrowser

* Update webbrowser in Cargo.toml too

Make sure all users get updated too
2023-01-26 11:31:39 +01:00
Emil Ernerfeldt
5b1cad2b72 Constrain menus to the screen 2023-01-26 11:13:21 +01:00
Emil Ernerfeldt
8ce0e1c520
Avoid deadlocks by using lambdas for context lock (#2625)
ctx.input().key_pressed(Key::A) -> ctx.input(|i| i.key_pressed(Key::A))
2023-01-25 10:24:23 +01:00
lictex_
eee4cf6a82
add functions to check which button triggered a drag start & end (#2507)
* add button release events for drags

* add utility functions

* fix CHANGELOG.md

* fix CHANGELOG.md

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2023-01-24 10:11:32 +01:00
Emil Ernerfeldt
4bd4eca2e4
Add ability to hide button backgrounds (#2621)
* Add Spacing::combo_width

* Put ComboBox arrow closer to the text

* Tweak faint_bg_color

* Make it possible to have buttons without background

…while still having background for sliders, checkboxes, etc

* Rename mandatory_bg_fill -> bg_fill

* tweak grid stripe color (again)

* Make the animated part of the ProgressBar more visible

* Add line in changelog

* Add another line in changelog

* Menu fix: use the `open` widget style for open menus

* Adjust sizes on menu buttons and regular buttons to make sure they match

* Update comment

Co-authored-by: Andreas Reich <andreas@rerun.io>

* optional_bg_fill -> weak_bg_fill

Co-authored-by: Andreas Reich <andreas@rerun.io>
2023-01-24 10:11:05 +01:00
Emil Ernerfeldt
c75e72693c Fix rendering of (ellipsis)
Broken when we introduced thing space support
2023-01-23 20:24:38 +01:00
Pâris DOUADY
518b4f447e
Allow changing ProgressBar fill color (#2618) 2023-01-23 15:33:02 +01:00
Emil Ernerfeldt
d4f9f6984d Fix red doctest 2023-01-23 15:04:35 +01:00
Weasy
356ebe55da
Add rounding fn to Button, to enable rounded buttons (#2616) 2023-01-23 12:37:39 +01:00
LEAVING
5029575ed0
Fix typo: 'Viewport width' -> 'Viewport height' (#2615) 2023-01-23 12:37:26 +01:00
Timon
30e49f1da2
Expose area interactable and movable to Window api. (#2610)
* Expose area interactable to window.

* Add movable function

* update dockstring
2023-01-23 12:37:15 +01:00
lictex_
01bbda4544
check point count before tessellating bezier (#2506) 2023-01-23 12:20:05 +01:00
Ales Tsurko
f87c6cbd7c
Derive Hash for KeyboardShortcut and Modifiers (#2563) 2023-01-23 10:06:54 +01:00
RadonCoding
ce5472633d
Fix close button not working (#2533)
* Fix close button not working

By adding the close button after the title bar drag listener the close button will sense clicks.

* Update main.rs
2023-01-23 09:55:57 +01:00
Robert Walter
0ad8aea811
Fix: button_padding when using image+text buttons (#2510)
* feat(image-button-margin): implement image button margin

- add `image_margin` field on `Button` widget
- implement setter method called `image_margin` for `Button` widget
- use margin from `image_margin` field of `Button` widget in `Widget`
  trait functions

* feat(image-button-margin): update changelog

* feat(image-button-margin): implement `map_or` clippy fix

* feat(image-button-margin): remove margin field & fix button-padding instead

* feat(image-button-margin): fix CI errors

* feat(image-button-margin): update changelog to include fix

* feat(image-button-margin): re-add changes after creating screenshots for PR
2023-01-23 09:23:57 +01:00
Fangdun Tsai
53b1d0e5e9
Add menu with an image button (#2488) 2023-01-23 08:22:43 +01:00
apoorv569
0eabd894bd
Fix typo in cargo run command. (#2582)
I think someone by mistake wrote `cargo run -p hello_world` instead of `cargo run -p keyboard_events`.
2023-01-17 10:58:38 +01:00
Emil Ernerfeldt
cd0f66b9ae
eframe web: ctrl-P and cmd-P will not open the print dialog (#2598) 2023-01-16 14:40:19 +01:00
Andreas Reich
60b4f5e3fe
changelog & doc fix for #2539 (Window::default_open) 2023-01-07 20:51:01 +01:00
joffreybesos
f7ed135192
window starting collapsed state (#2539) 2023-01-07 20:48:49 +01:00
Kornel
e70204d726
Point to caller's location when using log_or_panic (#2552) 2023-01-07 20:27:19 +01:00
Emil Ernerfeldt
34f587d1e1 Add emath::inverse_lerp 2022-12-22 12:33:06 +01:00
Emil Ernerfeldt
fa0d7f7f7f Add Response::on_hover_and_drag_cursor 2022-12-21 17:47:52 +01:00
Emil Ernerfeldt
a68c891092 Improve choice of number of decimals to show when hovering in plot 2022-12-20 15:27:01 +01:00
Emil Ernerfeldt
0e2656b77c Add ScrollArea::drag_to_scroll 2022-12-20 11:09:53 +01:00
lucasmerlin
80ea12877e
Add egui_skia integration to readme (#2483)
* Add egui_skia integration to readme

* sort

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-12-19 12:35:42 +01:00
Emil Ernerfeldt
99af63fad2
Add Plot::clamp_grid (#2480)
* Add Plot::clamp_grid

* Update changelog
2022-12-19 11:39:22 +01:00
Emil Ernerfeldt
e4e1638fc0 Fix newly introduced rendering bug for thin rectangles 2022-12-19 11:31:27 +01:00
Sven Niederberger
7e9c7dac41
Improve plot grid appearance (#2412)
* improve plot grid appearance

* update changelog

* Update crates/egui/src/widgets/plot/mod.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* address review comments

* make lines a bit weaker

* move changelog entry

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-12-19 10:57:30 +01:00
Ivan
f9728b88db
Combobox .wrap(true) width usage (#2472)
* Combobox .wrap(true) width fix

.wrap(true) does note use all the available width
this fix does not change the original .wrap(false) behaviours

* Code comment convention

Co-authored-by: IVANMK-7 <68190772+IVANMK-7@users.noreply.github.com>
2022-12-19 10:55:39 +01:00
francesco-cattoglio
2c9b130423
Add Memory::any_popup_open() (#2464)
* Added the "any_popup_open()" function

* Updated CHANGELOG.md

* add PR link to changelog

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-12-19 10:55:27 +01:00
Emil Ernerfeldt
6554fbb151 epaint: Improve rendering of very thin rectangles 2022-12-19 10:33:03 +01:00
Emil Ernerfeldt
6ae4bc486b Add Slider::drag_value_speed 2022-12-15 17:05:43 +01:00
Emil Ernerfeldt
367087f84f Annotate at_least and at_most with #[must_use] 2022-12-15 15:38:31 +01:00
Emil Ernerfeldt
ea5c9483a2
Fix bug in Mesh::split_to_u16 (#2459)
Co-authored-by: Vladislav Zhukov <vlad.zhukov@elektron.se>
2022-12-15 11:13:01 +01:00
Emil Ernerfeldt
37fd141dd1 Rename eframe::EframeError to eframe::Error 2022-12-14 17:29:54 +01:00
Emil Ernerfeldt
4e8341d35c
Don't render the \r (Carriage Return) character, because it sucks (#2452)
* Don't render the \r (Carriage Return) character, because it sucks

* Update PR links

* Fix doclink
2022-12-14 15:15:29 +01:00
Emil Ernerfeldt
df9df39f10 eframe: set_window_size, set_fullscreen and set_window_pos updates info 2022-12-14 14:14:20 +01:00
Emil Ernerfeldt
a70b173333 Toggle fullscreen in egui_demo_app with F11 2022-12-14 14:13:52 +01:00
Matt Campbell
a925511032
Expose the TextEdit multiline flag to AccessKit (#2448)
* Expose the TextEdit multiline flag to AccessKit

* Add changelog entry
2022-12-14 08:37:51 +01:00
Emil Ernerfeldt
126be51ac3 Add Visuals::striped as global default for Grids and Tables 2022-12-13 21:02:39 +01:00
Emil Ernerfeldt
51081d69fc
Maybe fix WSLg (#2439) 2022-12-13 16:06:19 +01:00
Emil Ernerfeldt
0ba2d8ca1d Add Visuals::menu_rounding 2022-12-13 15:44:27 +01:00
Emil Ernerfeldt
4a72abc8ec
Clamp eframe window size to at most the size of the largest monitor (#2445)
This can hopefully fix some reported bugs where
2022-12-13 09:09:36 +01:00
Emil Ernerfeldt
32677a54e4 egui-winit: improve clipboard logging 2022-12-13 09:09:25 +01:00
Emil Ernerfeldt
26eb002270 Remove references to old, fixed Firefox WebGL bug 2022-12-12 21:45:57 +01:00
Emil Ernerfeldt
c58ac86935
egui-wgpu: only depend on epaint (not entire egui) (#2438)
* egui-wgpu: only depend on epaint (not entire egui)

* Update changelog link

* Fix doclinks
2022-12-12 17:25:00 +01:00
Emil Ernerfeldt
e0b5bb17e5
Improve the look of thin lines, making them look weaker (#2437)
* Revert "fix all clippy lints and remove them from allow list in cranky (#2419)"

This reverts commit 930ef2db38.

* Explain the cranky lints better

* Add Color32::gamma_multiply

* Remove unused pub use

* Remove non-existing crate category

* Improve color test with more lines

* Improve the look of thin lines, making them look weaker

Before they looked were too strong for the thickness.

* Use asserts for shader compilations

* Update changelogs
2022-12-12 16:18:05 +01:00
Emil Ernerfeldt
6c4fc50fdf
Make egui_wgpu::winit::Painter::set_window async (#2434)
* Make `egui_wgpu::winit::Painter::set_window` async

* Fix changelog link
2022-12-12 15:37:55 +01:00
Emil Ernerfeldt
7a658e3ddb
Add Event::Key::repeat that is set to true if the event is a repeat (#2435)
* Add `Event::Key::repeat` that is set to `true` if the event is a repeat

* Update changelog

* Improve docstring
2022-12-12 15:37:42 +01:00
Emil Ernerfeldt
cb77458f70
eframe error handling (#2433)
* eframe::run_native: return errors instead of crashing

* Detect and handle glutin errors

* egui_demo_app: silence wgpu log spam

* Add trace logs for why eframe is shutting down

* Fix: only save App state once on Mac

* Handle Winit failure

* Log where we load app state from

* Don't panic on zero-sized window

* Clamp loaded window size to not be too tiny to see

* Simplify code: more shared code in window_builder

* Improve code readability

* Fix wasm32 build

* fix android

* Update changelog
2022-12-12 15:16:32 +01:00
Emil Ernerfeldt
1437ec8903 Tell Rust Analyzer to run build to its own target folder 2022-12-12 10:37:41 +01:00
Ryan Hileman
c8dd5d1b2d
egui-wgpu: don't panic if we can't find a device (#2427) (#2428) 2022-12-12 10:36:48 +01:00
Emil Ernerfeldt
059e6f719d egui-winit: don't call set_cursor each frame on any platform
On some platforms (WSL?) the setting of the cursor can also fail,
leading to warnings being logged by winit each frame. Not good.
2022-12-12 10:26:14 +01:00
Red Artist
930ef2db38
fix all clippy lints and remove them from allow list in cranky (#2419)
* manual range contains clippy lint removed from allow list
* removed multiple redundant allowed clippy lints
2022-12-11 19:06:09 +01:00
Emil Ernerfeldt
228f30ed46 Release 0.20.1 of eframe egui-winit egui_glium egui_glow - docs fixes 2022-12-11 17:04:23 +01:00
Emil Ernerfeldt
d7189d69f6 Release 0.20.1 - Fix key-repeat 2022-12-11 16:59:54 +01:00
Emil Ernerfeldt
2713f60f5b Update web demo 2022-12-11 16:57:20 +01:00
Emil Ernerfeldt
4e3ae098a9 Improve eframe wasm32 docs 2022-12-11 16:52:37 +01:00
Emil Ernerfeldt
0a1b85f35a
Revert key-repeat behavior (#2429)
* Revert key-repeat behavior

This fixes key-repeats everywhere in egui where it was broken,including:

- Enter in TextEdit:s
- Arrow keys for sliders and dragvalues
- …

* Update changelog

* Remove old comment
2022-12-11 16:26:57 +01:00
bilabila
e7471f1191
Fix drag_value.edit_string unexpected reset (#2421)
* Fix drag_value.edit_string unexpected reset

  solve issue #2418 #2370

* Also reset drag_value.edit_string on click

* Fix for clippy check
2022-12-10 10:05:51 +01:00
Tomoya Matsuura
aca3807e43
Fixed docs comment in web::start (#2422)
* fixed typo in docs

* fixed example code in docs (wrong return type)
2022-12-09 15:09:54 +01:00
Emil Ernerfeldt
32606c2223
Fix docs.rs build issues for eframe, egui-winit, egui_glium, egui_glow (#2420)
* Fix docs.rs build issues for eframe, egui-winit, egui_glium, egui_glow

I hope we can get rid of the `tts` crate very soon,
now that AcessKit has landed. It is only used for web atm.
Should probably be removed from all native libraries.

* Update changelogs
2022-12-09 12:24:47 +01:00
201 changed files with 5768 additions and 4472 deletions

6
.cargo/config.toml Normal file
View file

@ -0,0 +1,6 @@
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]

View file

@ -16,66 +16,93 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: 1.65.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
#uses: awalsh128/cache-apt-pkgs-action@v1.2.2
#TODO(emilk) use upstream when https://github.com/awalsh128/cache-apt-pkgs-action/pull/90 is merged
uses: rerun-io/cache-apt-pkgs-action@59534850182063abf1b2c11bb3686722a12a8397
with:
packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
version: 1.0
execute_install_scripts: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Install cargo-cranky
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-cranky
- name: Check all features
- name: check --all-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --all-features
- name: Check default features
args: --locked --all-features --all-targets
- name: check default features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked
- name: Check no default features
args: --locked --all-targets
- name: check --no-default-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --no-default-features --lib
args: --locked --no-default-features --lib --all-targets
- name: check eframe --no-default-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --no-default-features --lib --all-targets -p eframe
- name: Test doc-tests
uses: actions-rs/cargo@v1
with:
command: test
args: --doc --all-features
- name: cargo doc --lib
uses: actions-rs/cargo@v1
with:
command: doc
args: --lib --no-deps --all-features
- name: cargo doc --document-private-items
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps --all-features
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features
- name: Cranky
uses: actions-rs/cargo@v1
with:
command: cranky
args: --all-targets --all-features -- -D warnings
# ---------------------------------------------------------------------------
check_wasm:
name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04
@ -119,7 +146,7 @@ jobs:
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.83"
version: "0.2.84"
- run: ./sh/wasm_bindgen_check.sh --skip-setup
@ -129,6 +156,8 @@ jobs:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
# ---------------------------------------------------------------------------
cargo-deny:
name: cargo deny
runs-on: ubuntu-22.04
@ -138,18 +167,45 @@ jobs:
with:
rust-version: "1.65.0"
# ---------------------------------------------------------------------------
android:
name: android
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
target: aarch64-linux-android
override: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- run: cargo check --features wgpu --target aarch64-linux-android
working-directory: crates/eframe
# ---------------------------------------------------------------------------
windows:
name: Check Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
override: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Check
uses: actions-rs/cargo@v1
with:
command: check
args: --all-targets --all-features

4
.gitignore vendored
View file

@ -1,5 +1,7 @@
.DS_Store
**/target
**/target_ra
**/target_wasm
/.*.json
/.vscode
/media/*
.DS_Store

29
.vscode/settings.json vendored
View file

@ -1,5 +1,32 @@
{
"files.insertFinalNewline": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true
"files.trimTrailingWhitespace": true,
"editor.semanticTokenColorCustomizations": {
"rules": {
"*.unsafe:rust": "#eb5046"
}
},
"files.exclude": {
"target/**": true,
"target_ra/**": true,
},
// Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run`
"rust-analyzer.checkOnSave.overrideCommand": [
"cargo",
"cranky",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets"
],
"rust-analyzer.cargo.buildScripts.overrideCommand": [
"cargo",
"check",
"--quiet",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets"
],
}

View file

@ -5,8 +5,57 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
### Added ⭐
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)).
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
* Add `Response::on_hover_and_drag_cursor`.
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
### Changed 🔧
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
* `DragValue` and `Slider` now use the proportional font ([#2638](https://github.com/emilk/egui/pull/2638)).
* `ScrollArea` is less aggressive about clipping its contents ([#2665](https://github.com/emilk/egui/pull/2665)).
* Updated to be compatible with a major breaking change in AccessKit that drastically reduces memory usage when accessibility is enabled ([#2678](https://github.com/emilk/egui/pull/2678)).
* Improve `DragValue` behavior ([#2649](https://github.com/emilk/egui/pull/2649), [#2650](https://github.com/emilk/egui/pull/2650), [#2688](https://github.com/emilk/egui/pull/2688), [#2638](https://github.com/emilk/egui/pull/2638)).
### Fixed 🐛
* Fix key-repeat for backspace and arrow keys in `TextEdit` ([#2416](https://github.com/emilk/egui/pull/2416)).
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
* Menus are now moved to fit on the screen.
* Fix `Window::pivot` causing windows to move around ([#2694](https://github.com/emilk/egui/pull/2694)).
## 0.20.1 - 2022-12-11 - Fix key-repeat
### Changed 🔧
* `InputState`: all press functions again include key repeats (like in egui 0.19) ([#2429](https://github.com/emilk/egui/pull/2429)).
* Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)).
### Fixed 🐛
* Fix key-repeats for `TextEdit`, `Slider`s, etc ([#2429](https://github.com/emilk/egui/pull/2429)).
## 0.20.0 - 2022-12-08 - AccessKit, prettier text, overlapping widgets

1862
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,6 @@ members = [
"crates/egui_demo_app",
"crates/egui_demo_lib",
"crates/egui_extras",
"crates/egui_glium",
"crates/egui_glow",
"crates/egui-wgpu",
"crates/egui-winit",
@ -27,7 +26,8 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
[profile.dev]
split-debuginfo = "unpacked" # faster debug builds on mac
# Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897
# split-debuginfo = "unpacked" # faster debug builds on mac
# opt-level = 1 # Make debug builds run faster
# Optimize all dependencies even in debug builds (does not affect workspace packages):

View file

@ -113,10 +113,11 @@ warn = [
]
allow = [
# TODO(emilk): enable more lints
"clippy::manual_range_contains", # This one is just annoying
# Some of these we should try to put in "warn":
"clippy::type_complexity",
"clippy::undocumented_unsafe_blocks",
"clippy::manual_range_contains",
"trivial_casts",
"unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
"unused_qualifications",

View file

@ -66,11 +66,11 @@ To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
@ -158,17 +158,17 @@ An integration needs to do the following each frame:
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Run the application code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs))
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
### Official integrations
These are the official egui integrations:
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui_glow` and `egui-winit`.
* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`.
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps.
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) (DEPRECATED - looking for new maintainer).
### 3rd party integrations
@ -186,12 +186,14 @@ These are the official egui integrations:
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash).
* [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs).
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) for the [ggez](https://ggez.rs/) game framework.
* [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework.
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
* [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc).
* [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan).
* [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13).
* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe).
* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/).
* [`tauri-egui`](https://github.com/tauri-apps/tauri-egui) for [tauri](https://github.com/tauri-apps/tauri).
Missing an integration for the thing you're working on? Create one, it's easy!
@ -370,6 +372,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>.
egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock.
### Inspiration
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy.
@ -395,6 +399,7 @@ Notable contributions by:
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).

View file

@ -5,5 +5,9 @@ All notable changes to the `ecolor` crate will be noted in this file.
## Unreleased
## 0.21.0 - 2023-02-08
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
## 0.20.0 - 2022-12-08
* Split out `ecolor` crate from `epaint`

View file

@ -1,6 +1,6 @@
[package]
name = "ecolor"
version = "0.20.0"
version = "0.21.0"
authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>",
@ -12,7 +12,7 @@ homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding", "images"]
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]

View file

@ -1,5 +1,11 @@
# ecolor - egui color library
[![Latest version](https://img.shields.io/crates/v/ecolor.svg)](https://crates.io/crates/ecolor)
[![Documentation](https://docs.rs/ecolor/badge.svg)](https://docs.rs/ecolor)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
A simple color storage and conversion library.
Made for [`egui`](https://github.com/emilk/egui/).

View file

@ -171,11 +171,46 @@ impl Color32 {
Rgba::from(*self).to_srgba_unmultiplied()
}
/// Multiply with 0.5 to make color half as opaque.
/// Multiply with 0.5 to make color half as opaque, perceptually.
///
/// Fast multiplication in gamma-space.
///
/// This is perceptually even, and faster that [`Self::linear_multiply`].
#[inline]
pub fn gamma_multiply(self, factor: f32) -> Color32 {
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
let Self([r, g, b, a]) = self;
Self([
(r as f32 * factor + 0.5) as u8,
(g as f32 * factor + 0.5) as u8,
(b as f32 * factor + 0.5) as u8,
(a as f32 * factor + 0.5) as u8,
])
}
/// Multiply with 0.5 to make color half as opaque in linear space.
///
/// This is using linear space, which is not perceptually even.
/// You may want to use [`Self::gamma_multiply`] instead.
pub fn linear_multiply(self, factor: f32) -> Color32 {
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
// As an unfortunate side-effect of using premultiplied alpha
// we need a somewhat expensive conversion to linear space and back.
Rgba::from(self).multiply(factor).into()
}
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
///
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
/// in order to obtain linear space color values.
#[inline]
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
let Self([r, g, b, a]) = self;
[
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
]
}
}

View file

@ -7,6 +7,38 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
## Unreleased
## 0.21.3 - 2023-02-15
* Fix typing the letter 'P' on web ([#2740](https://github.com/emilk/egui/pull/2740)).
## 0.21.2 - 2023-02-12
* Allow compiling `eframe` with `--no-default-features` ([#2728](https://github.com/emilk/egui/pull/2728)).
## 0.21.1 - 2023-02-12
* Fixed crash when native window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
## 0.21.0 - 2023-02-08 - Update to `winit` 0.28
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
#### Desktop/Native:
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Fix bug where the cursor could get stuck using the wrong icon.
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
#### Web:
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
* Allow empty textures with the glow renderer.

View file

@ -1,6 +1,6 @@
[package]
name = "eframe"
version = "0.20.0"
version = "0.21.3"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021"
@ -15,6 +15,7 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[lib]
@ -35,7 +36,7 @@ dark-light = ["dep:dark-light"]
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
glow = ["dep:glow", "dep:egui_glow", "dep:glutin"]
glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
## Enable saving app state to disk.
persistence = [
@ -52,8 +53,10 @@ persistence = [
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
screen_reader = ["egui-winit/screen_reader", "tts"]
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
##
## For other platforms, use the "accesskit" feature instead.
web_screen_reader = ["tts"]
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
## This is used to generate images for the examples.
@ -61,57 +64,53 @@ __screenshot = ["dep:image"]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## This overrides the `glow` feature.
wgpu = ["dep:wgpu", "dep:egui-wgpu"]
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
[dependencies]
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"bytemuck",
"tracing",
] }
thiserror = "1.0.37"
tracing = { version = "0.1", default-features = false, features = ["std"] }
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
egui_glow = { version = "0.20.0", path = "../egui_glow", optional = true, default-features = false }
glow = { version = "0.11", optional = true }
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
glow = { version = "0.12", optional = true }
ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] }
# -------------------------------------------
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.20.0", path = "../egui-winit", default-features = false, features = [
egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
"clipboard",
"links",
] }
raw-window-handle = { version = "0.5.0" }
winit = "0.27.2"
winit = "0.28.1"
# optional native:
dark-light = { version = "0.2.1", optional = true }
dark-light = { version = "1.0", optional = true }
directories-next = { version = "2", optional = true }
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true, features = [
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
"winit",
] } # if wgpu is used, use it with winit
pollster = { version = "0.3", optional = true } # needed for wgpu
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
# this can be done at the same time we expose x11/wayland features of winit crate.
glutin = { version = "0.30.0", optional = true, es = [
"egl",
"glx",
"x11",
"wayland",
"wgl",
] }
glutin = { version = "0.30", optional = true }
glutin-winit = { version = "0.3.0", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [
"png",
] }
puffin = { version = "0.14", optional = true }
wgpu = { version = "0.14", optional = true }
wgpu = { version = "0.15.0", optional = true }
# -------------------------------------------
# web:
@ -119,7 +118,7 @@ wgpu = { version = "0.14", optional = true }
bytemuck = "1.7"
js-sys = "0.3"
percent-encoding = "2.1"
wasm-bindgen = "0.2"
wasm-bindgen = "=0.2.84"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.58", features = [
"BinaryType",
@ -165,6 +164,6 @@ web-sys = { version = "0.3.58", features = [
] }
# optional web:
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
tts = { version = "0.24", optional = true }
wgpu = { version = "0.14", optional = true, features = ["webgl"] }
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
tts = { version = "0.25", optional = true, default-features = false }
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }

View file

@ -22,7 +22,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
To use on Linux, first run:
```
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
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.
@ -45,7 +45,6 @@ You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/win
* Mobile text editing is not as good as for a normal web app.
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
* No integration with browser settings for colors and fonts.
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).

View file

@ -10,9 +10,11 @@
use std::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use crate::native::run::UserEvent;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use winit::event_loop::EventLoopBuilder;
/// Hook into the building of an event loop before it is run
@ -20,6 +22,7 @@ pub use winit::event_loop::EventLoopBuilder;
/// You can configure any platform specific details required on top of the default configuration
/// done by `EFrame`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
/// This is how your app is created.
@ -74,7 +77,7 @@ pub trait App {
///
/// Can be used from web to interact or other external context.
///
/// You need to implement this if you want to be able to access the application from JS using [`AppRunner::app_mut`].
/// You need to implement this if you want to be able to access the application from JS using [`crate::web::backend::AppRunner`].
///
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
///
@ -145,20 +148,25 @@ pub trait App {
/// The size limit of the web app canvas.
///
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
///
/// A large canvas can lead to bad frame rates on some older browsers on some platforms
/// (see <https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0>).
fn max_size_points(&self) -> egui::Vec2 {
egui::Vec2::INFINITY
}
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
///
/// This is the background of your windows if you don't set a central panel.
fn clear_color(&self, _visuals: &egui::Visuals) -> egui::Rgba {
///
/// ATTENTION:
/// Since these float values go to the render as-is, any color space conversion as done
/// e.g. by converting from [`egui::Color32`] to [`egui::Rgba`] may cause incorrect results.
/// egui recommends that rendering backends use a normal "gamma-space" (non-sRGB-aware) blending,
/// which means the values you return here should also be in `sRGB` gamma-space in the 0-1 range.
/// You can use [`egui::Color32::to_normalized_gamma_f32`] for this.
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
// NOTE: a bright gray makes the shadows of the windows look weird.
// We use a bit of transparency so that if the user switches on the
// `transparent()` option they get immediate results.
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).to_normalized_gamma_f32()
// _visuals.window_fill() would also be a natural choice
}
@ -176,7 +184,7 @@ pub trait App {
}
/// If `true` a warm-up call to [`Self::update`] will be issued where
/// `ctx.memory().everything_is_visible()` will be set to `true`.
/// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`.
///
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
///
@ -258,10 +266,10 @@ pub struct NativeOptions {
/// The initial inner size of the native window in points (logical pixels).
pub initial_window_size: Option<egui::Vec2>,
/// The minimum inner window size
/// The minimum inner window size in points (logical pixels).
pub min_window_size: Option<egui::Vec2>,
/// The maximum inner window size
/// The maximum inner window size in points (logical pixels).
pub max_window_size: Option<egui::Vec2>,
/// Should the app window be resizable?
@ -312,6 +320,7 @@ pub struct NativeOptions {
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub renderer: Renderer,
/// Only used if the `dark-light` feature is enabled:
@ -350,6 +359,7 @@ pub struct NativeOptions {
/// event loop before it is run.
///
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub event_loop_builder: Option<EventLoopBuilderHook>,
#[cfg(feature = "glow")]
@ -376,9 +386,13 @@ impl Clone for NativeOptions {
fn clone(&self) -> Self {
Self {
icon_data: self.icon_data.clone(),
#[cfg(any(feature = "glow", feature = "wgpu"))]
event_loop_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "wgpu")]
wgpu_options: self.wgpu_options.clone(),
..*self
}
}
@ -392,8 +406,10 @@ impl Default for NativeOptions {
maximized: false,
decorated: true,
fullscreen: false,
#[cfg(target_os = "macos")]
fullsize_content: false,
drag_and_drop_support: true,
icon_data: None,
initial_window_pos: None,
@ -408,14 +424,22 @@ impl Default for NativeOptions {
depth_buffer: 0,
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu"))]
renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark,
run_and_return: true,
#[cfg(any(feature = "glow", feature = "wgpu"))]
event_loop_builder: None,
#[cfg(feature = "glow")]
shader_version: None,
centered: false,
#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
}
@ -432,6 +456,7 @@ impl NativeOptions {
match dark_light::detect() {
dark_light::Mode::Dark => Some(Theme::Dark),
dark_light::Mode::Light => Some(Theme::Light),
dark_light::Mode::Default => None,
}
} else {
None
@ -553,6 +578,7 @@ pub enum WebGlContextOption {
/// What rendering backend to use.
///
/// You need to enable the "glow" and "wgpu" features to have a choice.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@ -566,6 +592,7 @@ pub enum Renderer {
Wgpu,
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl Default for Renderer {
fn default() -> Self {
#[cfg(feature = "glow")]
@ -581,6 +608,7 @@ impl Default for Renderer {
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -593,6 +621,7 @@ impl std::fmt::Display for Renderer {
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::str::FromStr for Renderer {
type Err = String;
@ -708,9 +737,22 @@ impl Frame {
#[doc(alias = "exit")]
#[doc(alias = "quit")]
pub fn close(&mut self) {
tracing::debug!("eframe::Frame::close called");
self.output.close = true;
}
/// Minimize or unminimize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_minimized(&mut self, minimized: bool) {
self.output.minimized = Some(minimized);
}
/// Maximize or unmaximize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_maximized(&mut self, maximized: bool) {
self.output.maximized = Some(maximized);
}
/// Tell `eframe` to close the desktop window.
#[cfg(not(target_arch = "wasm32"))]
#[deprecated = "Renamed `close`"]
@ -722,6 +764,7 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))]
pub fn set_window_size(&mut self, size: egui::Vec2) {
self.output.window_size = Some(size);
self.info.window_info.size = size; // so that subsequent calls see the updated value
}
/// Set the desired title of the window.
@ -742,12 +785,14 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))]
pub fn set_fullscreen(&mut self, fullscreen: bool) {
self.output.fullscreen = Some(fullscreen);
self.info.window_info.fullscreen = fullscreen; // so that subsequent calls see the updated value
}
/// set the position of the outer window.
#[cfg(not(target_arch = "wasm32"))]
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
self.output.window_pos = Some(pos);
self.info.window_info.position = Some(pos); // so that subsequent calls see the updated value
}
/// When called, the native window will follow the
@ -789,6 +834,7 @@ impl Frame {
}
/// for integrations only: call once per frame
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
std::mem::take(&mut self.output)
}
@ -819,6 +865,12 @@ pub struct WindowInfo {
/// Are we in fullscreen mode?
pub fullscreen: bool,
/// Are we minimized?
pub minimized: bool,
/// Are we maximized?
pub maximized: bool,
/// Window inner size in egui points (logical pixels).
pub size: egui::Vec2,
@ -1001,5 +1053,13 @@ pub(crate) mod backend {
/// Set to some bool to tell the window always on top.
#[cfg(not(target_arch = "wasm32"))]
pub always_on_top: Option<bool>,
/// Set to some bool to minimize or unminimize window.
#[cfg(not(target_arch = "wasm32"))]
pub minimized: Option<bool>,
/// Set to some bool to maximize or unmaximize window.
#[cfg(not(target_arch = "wasm32"))]
pub maximized: Option<bool>,
}
}

View file

@ -103,9 +103,20 @@ pub use web_sys;
/// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<AppRunnerRef>, eframe::wasm_bindgen::JsValue> {
/// pub struct WebHandle {
/// handle: AppRunnerRef,
/// }
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
/// let web_options = eframe::WebOptions::default();
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
/// eframe::start_web(
/// canvas_id,
/// web_options,
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
/// )
/// .await
/// .map(|handle| WebHandle { handle })
/// }
/// ```
///
@ -116,7 +127,7 @@ pub async fn start_web(
canvas_id: &str,
web_options: WebOptions,
app_creator: AppCreator,
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator).await?;
Ok(handle)
@ -126,6 +137,7 @@ pub async fn start_web(
// When compiling natively
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod native;
/// This is how you start a native (desktop) app.
@ -163,9 +175,17 @@ mod native;
/// }
/// }
/// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)]
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub fn run_native(
app_name: &str,
native_options: NativeOptions,
app_creator: AppCreator,
) -> Result<()> {
let renderer = native_options.renderer;
#[cfg(not(feature = "__screenshot"))]
@ -178,37 +198,65 @@ pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: Ap
#[cfg(feature = "glow")]
Renderer::Glow => {
tracing::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator);
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator);
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
}
// ----------------------------------------------------------------------------
/// The different problems that can occur when trying to run `eframe`.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(not(target_arch = "wasm32"))]
#[error("winit error: {0}")]
Winit(#[from] winit::error::OsError),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("glutin error: {0}")]
Glutin(#[from] glutin::error::Error),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
#[cfg(feature = "wgpu")]
#[error("WGPU error: {0}")]
Wgpu(#[from] egui_wgpu::WgpuError),
}
pub type Result<T> = std::result::Result<T, Error>;
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
macro_rules! profile_function {
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_function;
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
macro_rules! profile_scope {
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_scope;
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) use profiling_scopes::*;

View file

@ -5,12 +5,21 @@ use winit::platform::macos::WindowBuilderExtMacOS as _;
#[cfg(feature = "accesskit")]
use egui::accesskit;
use egui::NumExt as _;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
use crate::{epi, Theme, WindowInfo};
#[derive(Default)]
pub struct WindowState {
// We cannot simply call `winit::Window::is_minimized/is_maximized`
// because that deadlocks on mac.
pub minimized: bool,
pub maximized: bool,
}
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize {
width: points.x as f64,
@ -18,7 +27,11 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
}
}
pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -> WindowInfo {
pub fn read_window_info(
window: &winit::window::Window,
pixels_per_point: f32,
window_state: &WindowState,
) -> WindowInfo {
let position = window
.outer_position()
.ok()
@ -37,9 +50,13 @@ pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -
.inner_size()
.to_logical::<f32>(pixels_per_point.into());
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
WindowInfo {
position,
fullscreen: window.fullscreen().is_some(),
minimized: window_state.minimized,
maximized: window_state.maximized,
size: egui::Vec2 {
x: size.width,
y: size.height,
@ -48,12 +65,13 @@ pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -
}
}
pub fn window_builder(
pub fn window_builder<E>(
event_loop: &EventLoopWindowTarget<E>,
title: &str,
native_options: &epi::NativeOptions,
window_settings: &Option<WindowSettings>,
window_settings: Option<WindowSettings>,
) -> winit::window::WindowBuilder {
let epi::NativeOptions {
always_on_top,
maximized,
decorated,
fullscreen,
@ -67,19 +85,23 @@ pub fn window_builder(
max_window_size,
resizable,
transparent,
centered,
..
} = native_options;
let window_icon = icon_data.clone().and_then(load_icon);
let mut window_builder = winit::window::WindowBuilder::new()
.with_always_on_top(*always_on_top)
.with_title(title)
.with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
.with_maximized(*maximized)
.with_resizable(*resizable)
.with_transparent(*transparent)
.with_window_icon(window_icon);
.with_window_icon(window_icon)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
#[cfg(target_os = "macos")]
if *fullsize_content {
@ -98,21 +120,73 @@ pub fn window_builder(
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
if let Some(window_settings) = window_settings {
let inner_size_points = if let Some(mut window_settings) = window_settings {
// Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)]
window_settings.clamp_window_to_sane_position(&event_loop);
window_builder = window_settings.initialize_window(window_builder);
window_settings.inner_size_points()
} else {
if let Some(pos) = *initial_window_pos {
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
if let Some(initial_window_size) = *initial_window_size {
let initial_window_size =
initial_window_size.at_most(largest_monitor_point_size(event_loop));
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
}
*initial_window_size
};
if *centered {
if let Some(monitor) = event_loop.available_monitors().next() {
let monitor_size = monitor.size();
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0 && monitor_size.height > 0 {
let x = (monitor_size.width - inner_size.x as u32) / 2;
let y = (monitor_size.height - inner_size.y as u32) / 2;
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
x: x as f64,
y: y as f64,
});
}
}
}
window_builder
}
pub fn apply_native_options_to_window(
window: &winit::window::Window,
native_options: &crate::NativeOptions,
) {
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
}
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
let mut max_size = egui::Vec2::ZERO;
for monitor in event_loop.available_monitors() {
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
let size = egui::vec2(size.width, size.height);
max_size = max_size.max(size);
}
window_builder
if max_size == egui::Vec2::ZERO {
egui::Vec2::splat(16000.0)
} else {
max_size
}
}
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
@ -141,6 +215,7 @@ pub fn handle_app_output(
window: &winit::window::Window,
current_pixels_per_point: f32,
app_output: epi::backend::AppOutput,
window_state: &mut WindowState,
) {
let epi::backend::AppOutput {
close: _,
@ -152,6 +227,8 @@ pub fn handle_app_output(
window_pos,
visible: _, // handled in post_present
always_on_top,
minimized,
maximized,
} = app_output;
if let Some(decorated) = decorated {
@ -188,7 +265,22 @@ pub fn handle_app_output(
}
if let Some(always_on_top) = always_on_top {
window.set_always_on_top(always_on_top);
use winit::window::WindowLevel;
window.set_window_level(if always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
}
if let Some(minimized) = minimized {
window.set_minimized(minimized);
window_state.minimized = minimized;
}
if let Some(maximized) = maximized {
window.set_maximized(maximized);
window_state.maximized = maximized;
}
}
@ -215,6 +307,7 @@ pub struct EpiIntegration {
/// When set, it is time to close the native window.
close: bool,
can_drag_window: bool,
window_state: WindowState,
}
impl EpiIntegration {
@ -229,16 +322,22 @@ impl EpiIntegration {
) -> Self {
let egui_ctx = egui::Context::default();
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
egui_ctx.memory_mut(|mem| *mem = memory);
let native_pixels_per_point = window.scale_factor() as f32;
let window_state = WindowState {
minimized: window.is_minimized().unwrap_or(false),
maximized: window.is_maximized(),
};
let frame = epi::Frame {
info: epi::IntegrationInfo {
system_theme,
cpu_usage: None,
native_pixels_per_point: Some(native_pixels_per_point),
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state),
},
output: epi::backend::AppOutput {
visible: Some(true),
@ -263,6 +362,7 @@ impl EpiIntegration {
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
window_state,
}
}
@ -281,17 +381,18 @@ impl EpiIntegration {
egui_ctx.enable_accesskit();
// Enqueue a repaint so we'll receive a full tree update soon.
egui_ctx.request_repaint();
egui::accesskit_placeholder_tree_update()
egui_ctx.accesskit_placeholder_tree_update()
});
}
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
self.egui_ctx
.memory_mut(|mem| mem.set_everything_is_visible(true));
let full_output = self.update(app, window);
self.pending_full_output.append(full_output); // Handle it next frame
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
@ -308,8 +409,15 @@ impl EpiIntegration {
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
WindowEvent::CloseRequested => self.close = app.on_close_event(),
WindowEvent::Destroyed => self.close = true,
WindowEvent::CloseRequested => {
tracing::debug!("Received WindowEvent::CloseRequested");
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
WindowEvent::Destroyed => {
tracing::debug!("Received WindowEvent::Destroyed");
self.close = true;
}
WindowEvent::MouseInput {
button: MouseButton::Left,
state: ElementState::Pressed,
@ -336,12 +444,16 @@ impl EpiIntegration {
) -> egui::FullOutput {
let frame_start = std::time::Instant::now();
self.frame.info.window_info = read_window_info(window, self.egui_ctx.pixels_per_point());
self.frame.info.window_info =
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
let raw_input = self.egui_winit.take_egui_input(window);
// Run user code:
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
crate::profile_scope!("App::update");
app.update(egui_ctx, &mut self.frame);
});
self.pending_full_output.append(full_output);
let full_output = std::mem::take(&mut self.pending_full_output);
@ -351,9 +463,15 @@ impl EpiIntegration {
self.can_drag_window = false;
if app_output.close {
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
self.frame.output.visible = app_output.visible; // this is handled by post_present
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
handle_app_output(
window,
self.egui_ctx.pixels_per_point(),
app_output,
&mut self.window_state,
);
}
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
@ -410,7 +528,8 @@ impl EpiIntegration {
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
crate::profile_scope!("App::save");

View file

@ -26,6 +26,7 @@ impl FileStorage {
/// Store the state in this .ron file.
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
let ron_filepath: PathBuf = ron_filepath.into();
tracing::debug!("Loading app state from {:?}…", ron_filepath);
Self {
kv: read_ron(&ron_filepath).unwrap_or_default(),
ron_filepath,

View file

@ -1,18 +1,21 @@
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
//! When making changes to one you often also want to apply it to the other.
use std::time::Duration;
use std::time::Instant;
use std::time::{Duration, Instant};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::winit;
use winit::event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::winit;
use crate::{epi, Result};
use super::epi_integration::{self, EpiIntegration};
use crate::epi;
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub enum UserEvent {
@ -35,6 +38,7 @@ pub use epi::NativeOptions;
#[derive(Debug)]
enum EventResult {
Wait,
/// Causes a synchronous repaint inside the event handler. This should only
/// be used in special situations if the window must be repainted while
/// handling a specific event. This occurs on Windows when handling resizes.
@ -42,25 +46,33 @@ enum EventResult {
/// `RepaintNow` creates a new frame synchronously, and should therefore
/// only be used for extremely urgent repaints.
RepaintNow,
/// Queues a repaint for once the event loop handles its next redraw. Exists
/// so that multiple input events can be handled in one frame. Does not
/// cause any delay like `RepaintNow`.
RepaintNext,
RepaintAt(Instant),
Exit,
}
trait WinitApp {
fn is_focused(&self) -> bool;
fn integration(&self) -> Option<&EpiIntegration>;
fn window(&self) -> Option<&winit::window::Window>;
fn save_and_destroy(&mut self);
fn paint(&mut self) -> EventResult;
fn on_event(
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult;
) -> Result<EventResult>;
}
fn create_event_loop_builder(
@ -79,10 +91,10 @@ fn create_event_loop_builder(
///
/// We reuse the event-loop so we can support closing and opening an eframe window
/// multiple times. This is just a limitation of winit.
fn with_event_loop(
fn with_event_loop<R>(
mut native_options: epi::NativeOptions,
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions),
) {
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions) -> R,
) -> R {
use std::cell::RefCell;
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<UserEvent>>> = RefCell::new(None));
@ -93,22 +105,31 @@ fn with_event_loop(
let mut event_loop = event_loop.borrow_mut();
let event_loop = event_loop
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
f(event_loop, native_options);
});
f(event_loop, native_options)
})
}
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl WinitApp) {
fn run_and_return(
event_loop: &mut EventLoop<UserEvent>,
mut winit_app: impl WinitApp,
) -> Result<()> {
use winit::platform::run_return::EventLoopExtRunReturn as _;
tracing::debug!("event_loop.run_return");
tracing::debug!("Entering the winit event loop (run_return)…");
let mut next_repaint_time = Instant::now();
let mut returned_result = Ok(());
event_loop.run_return(|event, event_loop, control_flow| {
let event_result = match &event {
winit::event::Event::LoopDestroyed => {
tracing::debug!("winit::event::Event::LoopDestroyed");
EventResult::Exit
// On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name),
// so we need to save state now:
tracing::debug!("Received Event::LoopDestroyed - saving app state…");
winit_app.save_and_destroy();
*control_flow = ControlFlow::Exit;
return;
}
// Platform-dependent event handlers to workaround a winit bug
@ -137,15 +158,28 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
EventResult::Wait
}
event => winit_app.on_event(event_loop, event),
event => match winit_app.on_event(event_loop, event) {
Ok(event_result) => event_result,
Err(err) => {
tracing::error!("Exiting because of error: {err:?} on event {event:?}");
returned_result = Err(err);
EventResult::Exit
}
},
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintNow => {
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint();
} else {
// Fix for https://github.com/emilk/egui/issues/2425
next_repaint_time = Instant::now();
}
}
EventResult::RepaintNext => {
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
@ -155,10 +189,7 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
// On Cmd-Q we get here and then `run_return` doesn't return,
// so we need to save state now:
tracing::debug!("Exiting event loop - saving app state…");
winit_app.save_and_destroy();
tracing::debug!("Asking to exit event loop…");
*control_flow = ControlFlow::Exit;
return;
}
@ -187,16 +218,21 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit();
});
returned_result
}
fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp + 'static) -> ! {
tracing::debug!("event_loop.run");
tracing::debug!("Entering the winit event loop (run)…");
let mut next_repaint_time = Instant::now();
event_loop.run(move |event, event_loop, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
winit::event::Event::LoopDestroyed => {
tracing::debug!("Received Event::LoopDestroyed");
EventResult::Exit
}
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
@ -215,14 +251,25 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
..
}) => EventResult::RepaintNext,
event => winit_app.on_event(event_loop, &event),
event => match winit_app.on_event(event_loop, &event) {
Ok(event_result) => event_result,
Err(err) => {
panic!("eframe encountered a fatal error: {err}");
}
},
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintNow => {
if cfg!(windows) {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint();
} else {
// Fix for https://github.com/emilk/egui/issues/2425
next_repaint_time = Instant::now();
}
}
EventResult::RepaintNext => {
next_repaint_time = Instant::now();
@ -231,7 +278,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
tracing::debug!("Quitting");
tracing::debug!("Quitting - saving app state");
winit_app.save_and_destroy();
#[allow(clippy::exit)]
std::process::exit(0);
@ -252,33 +299,20 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
})
}
fn center_window_pos(
monitor: Option<winit::monitor::MonitorHandle>,
native_options: &mut epi::NativeOptions,
) {
// Get the current_monitor.
if let Some(monitor) = monitor {
let monitor_size = monitor.size();
let inner_size = native_options
.initial_window_size
.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0 && monitor_size.height > 0 {
let x = (monitor_size.width - inner_size.x as u32) / 2;
let y = (monitor_size.height - inner_size.y as u32) / 2;
native_options.initial_window_pos = Some(egui::Pos2 {
x: x as _,
y: y as _,
});
}
}
}
// ----------------------------------------------------------------------------
/// Run an egui app
#[cfg(feature = "glow")]
mod glow_integration {
use std::sync::Arc;
use egui::NumExt as _;
use glutin::{
display::GetGlDisplay,
prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext},
surface::GlSurface,
};
use raw_window_handle::HasRawWindowHandle;
use super::*;
// Note: that the current Glutin API design tightly couples the GL context with
@ -302,148 +336,260 @@ mod glow_integration {
painter: egui_glow::Painter,
integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>,
// Conceptually this will be split out eventually so that the rest of the state
// can be persistent.
gl_window: GlutinWindowContext,
}
/// This struct will contain both persistent and temporary glutin state.
///
/// Platform Quirks:
/// * Microsoft Windows: requires that we create a window before opengl context.
/// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event.
///
/// winit guarantees that we will get a Resumed event on startup on all platforms.
/// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`.
/// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android.
/// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event.
///
/// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of
/// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended.
struct GlutinWindowContext {
window: winit::window::Window,
gl_context: glutin::context::PossiblyCurrentContext,
gl_display: glutin::display::Display,
gl_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
builder: winit::window::WindowBuilder,
swap_interval: glutin::surface::SwapInterval,
gl_config: glutin::config::Config,
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
window: Option<winit::window::Window>,
}
impl GlutinWindowContext {
// refactor this function to use `glutin-winit` crate eventually.
// preferably add android support at the same time.
/// There is a lot of complexity with opengl creation, so prefer extensivve logging to get all the help we can to debug issues.
///
#[allow(unsafe_code)]
unsafe fn new(
winit_window: winit::window::Window,
winit_window_builder: winit::window::WindowBuilder,
native_options: &epi::NativeOptions,
) -> Self {
event_loop: &EventLoopWindowTarget<UserEvent>,
) -> Result<Self> {
use glutin::prelude::*;
use raw_window_handle::*;
// convert native options to glutin options
let hardware_acceleration = match native_options.hardware_acceleration {
crate::HardwareAcceleration::Required => Some(true),
crate::HardwareAcceleration::Preferred => None,
crate::HardwareAcceleration::Off => Some(false),
};
let raw_display_handle = winit_window.raw_display_handle();
let raw_window_handle = winit_window.raw_window_handle();
// EGL is crossplatform and the official khronos way
// but sometimes platforms/drivers may not have it, so we use back up options where possible.
// TODO: check whether we can expose these options as "features", so that users can select the relevant backend they want.
// try egl and fallback to windows wgl. Windows is the only platform that *requires* window handle to create display.
#[cfg(target_os = "windows")]
let preference =
glutin::display::DisplayApiPreference::EglThenWgl(Some(raw_window_handle));
// try egl and fallback to x11 glx
#[cfg(target_os = "linux")]
let preference = glutin::display::DisplayApiPreference::EglThenGlx(Box::new(
winit::platform::unix::register_xlib_error_hook,
));
#[cfg(target_os = "macos")]
let preference = glutin::display::DisplayApiPreference::Cgl;
#[cfg(target_os = "android")]
let preference = glutin::display::DisplayApiPreference::Egl;
let gl_display = glutin::display::Display::new(raw_display_handle, preference)
.expect("failed to create glutin display");
let swap_interval = if native_options.vsync {
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
} else {
glutin::surface::SwapInterval::DontWait
};
let config_template = glutin::config::ConfigTemplateBuilder::new()
/* opengl setup flow goes like this:
1. we create a configuration for opengl "Display" / "Config" creation
2. choose between special extensions like glx or egl or wgl and use them to create config/display
3. opengl context configuration
4. opengl context creation
*/
// start building config for gl display
let config_template_builder = glutin::config::ConfigTemplateBuilder::new()
.prefer_hardware_accelerated(hardware_acceleration)
.with_depth_size(native_options.depth_buffer);
.with_depth_size(native_options.depth_buffer)
.with_stencil_size(native_options.stencil_buffer)
.with_transparency(native_options.transparent);
// we don't know if multi sampling option is set. so, check if its more than 0.
let config_template = if native_options.multisampling > 0 {
config_template.with_multisampling(
let config_template_builder = if native_options.multisampling > 0 {
config_template_builder.with_multisampling(
native_options
.multisampling
.try_into()
.expect("failed to fit multisamples into u8"),
.expect("failed to fit multisamples option of native_options into u8"),
)
} else {
config_template
config_template_builder
};
let config_template = config_template
.with_stencil_size(native_options.stencil_buffer)
.with_transparency(native_options.transparent)
.compatible_with_native_window(raw_window_handle)
.build();
// finds all valid configurations supported by this display that match the config_template
// this is where we will try to get a "fallback" config if we are okay with ignoring some native
// options required by user like multi sampling, srgb, transparency etc..
// TODO: need to figure out a good fallback config template
let config = gl_display
.find_configs(config_template)
.expect("failed to find even a single matching configuration")
.next()
.expect("failed to find a matching configuration for creating opengl context");
tracing::debug!(
"trying to create glutin Display with config: {:?}",
&config_template_builder
);
// create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android.
let (window, gl_config) = glutin_winit::DisplayBuilder::new()
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
.with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
.with_window_builder(Some(winit_window_builder.clone()))
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
"failed to find a matching configuration for creating glutin config",
);
tracing::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
let gl_display = gl_config.display();
tracing::debug!(
"successfully created GL Display with version: {} and supported features: {:?}",
gl_display.version_string(),
gl_display.supported_features()
);
let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle());
tracing::debug!(
"creating gl context using raw window handle: {:?}",
raw_window_handle
);
// create gl context. if core context cannot be created, try gl es context as fallback.
let context_attributes =
glutin::context::ContextAttributesBuilder::new().build(Some(raw_window_handle));
// for surface creation.
let (width, height): (u32, u32) = winit_window.inner_size().into();
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle);
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::Gles(None))
.build(raw_window_handle);
let gl_context = match gl_config
.display()
.create_context(&gl_config, &context_attributes)
{
Ok(it) => it,
Err(err) => {
tracing::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}");
tracing::debug!("retrying with fallback context attributes: {fallback_context_attributes:?}");
gl_config
.display()
.create_context(&gl_config, &fallback_context_attributes)?
}
};
let not_current_gl_context = Some(gl_context);
// the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but
// it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might
// help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc..
// https://github.com/emilk/egui/pull/2541#issuecomment-1370767582
Ok(GlutinWindowContext {
builder: winit_window_builder,
swap_interval,
gl_config,
current_gl_context: None,
window,
gl_surface: None,
not_current_gl_context,
})
}
/// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime.
/// roughly,
/// 1. check if window already exists. otherwise, create one now.
/// 2. create attributes for surface creation.
/// 3. create surface.
/// 4. make surface and context current.
///
/// we presently assume that we will
#[allow(unsafe_code)]
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
if self.gl_surface.is_some() {
tracing::warn!(
"on_resume called even thought we already have a surface. early return"
);
return Ok(());
}
tracing::debug!("running on_resume fn.");
// make sure we have a window or create one.
let window = self.window.take().unwrap_or_else(|| {
tracing::debug!("window doesn't exist yet. creating one now with finalize_window");
glutin_winit::finalize_window(event_loop, self.builder.clone(), &self.gl_config)
.expect("failed to finalize glutin window")
});
// surface attributes
let (width, height): (u32, u32) = window.inner_size().into();
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
let surface_attributes =
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(
raw_window_handle,
std::num::NonZeroU32::new(width).unwrap(),
std::num::NonZeroU32::new(height).unwrap(),
.build(window.raw_window_handle(), width, height);
tracing::debug!(
"creating surface with attributes: {:?}",
&surface_attributes
);
// start creating the gl objects
let gl_context = gl_display
.create_context(&config, &context_attributes)
.expect("failed to create opengl context");
// create surface
let gl_surface = unsafe {
self.gl_config
.display()
.create_window_surface(&self.gl_config, &surface_attributes)?
};
tracing::debug!("surface created successfully: {gl_surface:?}.making context current");
// make surface and context current.
let not_current_gl_context = self
.not_current_gl_context
.take()
.expect("failed to get not current context after resume event. impossible!");
let current_gl_context = not_current_gl_context.make_current(&gl_surface)?;
// try setting swap interval. but its not absolutely necessary, so don't panic on failure.
tracing::debug!("made context current. setting swap interval for surface");
if let Err(e) = gl_surface.set_swap_interval(&current_gl_context, self.swap_interval) {
tracing::error!("failed to set swap interval due to error: {e:?}");
}
// we will reach this point only once in most platforms except android.
// create window/surface/make context current once and just use them forever.
self.gl_surface = Some(gl_surface);
self.current_gl_context = Some(current_gl_context);
self.window = Some(window);
Ok(())
}
let gl_surface = gl_display
.create_window_surface(&config, &surface_attributes)
.expect("failed to create glutin window surface");
let gl_context = gl_context
.make_current(&gl_surface)
.expect("failed to make gl context current");
gl_surface
.set_swap_interval(&gl_context, swap_interval)
.expect("failed to set vsync swap interval");
GlutinWindowContext {
window: winit_window,
gl_context,
gl_display,
gl_surface,
}
}
fn window(&self) -> &winit::window::Window {
&self.window
}
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
use glutin::surface::GlSurface;
self.gl_surface.resize(
&self.gl_context,
physical_size
.width
.try_into()
.expect("physical size must not be zero"),
physical_size
.height
.try_into()
.expect("physical size must not be zero"),
/// only applies for android. but we basically drop surface + window and make context not current
fn on_suspend(&mut self) -> Result<()> {
tracing::debug!("received suspend event. dropping window and surface");
self.gl_surface.take();
self.window.take();
if let Some(current) = self.current_gl_context.take() {
tracing::debug!("context is current, so making it non-current");
self.not_current_gl_context = Some(current.make_not_current()?);
} else {
tracing::debug!(
"context is already not current??? could be duplicate suspend event"
);
}
fn swap_buffers(&self) -> glutin::error::Result<()> {
use glutin::surface::GlSurface;
self.gl_surface.swap_buffers(&self.gl_context)
Ok(())
}
fn window(&self) -> &winit::window::Window {
self.window.as_ref().expect("winit window doesn't exist")
}
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
self.gl_surface
.as_ref()
.expect("failed to get surface to resize")
.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width,
height,
);
}
fn swap_buffers(&self) -> glutin::error::Result<()> {
self.gl_surface
.as_ref()
.expect("failed to get surface to swap buffers")
.swap_buffers(
self.current_gl_context
.as_ref()
.expect("failed to get current context to swap buffers"),
)
}
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
use glutin::display::GlDisplay;
self.gl_display.get_proc_address(addr)
self.gl_config.display().get_proc_address(addr)
}
}
@ -484,25 +630,24 @@ mod glow_integration {
fn create_glutin_windowed_context(
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<&dyn epi::Storage>,
title: &String,
title: &str,
native_options: &NativeOptions,
) -> (GlutinWindowContext, glow::Context) {
) -> Result<(GlutinWindowContext, glow::Context)> {
crate::profile_function!();
let window_settings = epi_integration::load_window_settings(storage);
let window_builder = epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
.with_transparent(native_options.transparent)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
let winit_window = window_builder
.build(event_loop)
.expect("failed to create winit window");
// a lot of the code below has been lifted from glutin example in their repo.
let glutin_window_context =
unsafe { GlutinWindowContext::new(winit_window, native_options) };
let winit_window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let mut glutin_window_context = unsafe {
GlutinWindowContext::new(winit_window_builder, native_options, event_loop)?
};
glutin_window_context.on_resume(event_loop)?;
if let Some(window) = &glutin_window_context.window {
epi_integration::apply_native_options_to_window(window, native_options);
}
let gl = unsafe {
glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
@ -512,10 +657,10 @@ mod glow_integration {
})
};
(glutin_window_context, gl)
Ok((glutin_window_context, gl))
}
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) {
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
let storage = epi_integration::create_storage(&self.app_name);
let (gl_window, gl) = Self::create_glutin_windowed_context(
@ -523,7 +668,7 @@ mod glow_integration {
storage.as_deref(),
&self.app_name,
&self.native_options,
);
)?;
let gl = Arc::new(gl);
let painter =
@ -585,6 +730,8 @@ mod glow_integration {
integration,
app,
});
Ok(())
}
}
@ -726,29 +873,27 @@ mod glow_integration {
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult {
match event {
) -> Result<EventResult> {
Ok(match event {
winit::event::Event::Resumed => {
// first resume event.
// we can actually move this outside of event loop.
// and just run the on_resume fn of gl_window
if self.running.is_none() {
self.init_run_state(event_loop);
self.init_run_state(event_loop)?;
} else {
// not the first resume event. create whatever you need.
self.running
.as_mut()
.unwrap()
.gl_window
.on_resume(event_loop)?;
}
EventResult::RepaintNow
}
winit::event::Event::Suspended => {
#[cfg(target_os = "android")]
{
tracing::error!("Suspended app can't destroy Window surface state with current Egui Glow backend (undefined behaviour)");
// Instead of destroying everything which we _know_ we can't re-create
// we instead currently just try our luck with not destroying anything.
//
// When the application resumes then it will get a new `SurfaceView` but
// we have no practical way currently of creating a new EGL surface
// via the Glutin API while keeping the GL context and the rest of
// our app state. This will likely result in a black screen or
// frozen screen.
//
//self.running = None;
}
self.running.as_mut().unwrap().gl_window.on_suspend()?;
EventResult::Wait
}
@ -793,7 +938,8 @@ mod glow_integration {
winit::event::WindowEvent::CloseRequested
if running.integration.should_close() =>
{
return EventResult::Exit
tracing::debug!("Received WindowEvent::CloseRequested");
return Ok(EventResult::Exit);
}
_ => {}
}
@ -832,7 +978,7 @@ mod glow_integration {
}
}
_ => EventResult::Wait,
}
})
}
}
@ -840,24 +986,15 @@ mod glow_integration {
app_name: &str,
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) {
) -> Result<()> {
if native_options.run_and_return {
with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
with_event_loop(native_options, |event_loop, native_options| {
let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, glow_eframe);
});
run_and_return(event_loop, glow_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
@ -923,38 +1060,41 @@ mod wgpu_integration {
fn create_window(
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<&dyn epi::Storage>,
title: &String,
title: &str,
native_options: &NativeOptions,
) -> winit::window::Window {
) -> std::result::Result<winit::window::Window, winit::error::OsError> {
let window_settings = epi_integration::load_window_settings(storage);
epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false)
.build(event_loop)
.unwrap()
let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = window_builder.build(event_loop)?;
epi_integration::apply_native_options_to_window(&window, native_options);
Ok(window)
}
#[allow(unsafe_code)]
fn set_window(&mut self, window: winit::window::Window) {
fn set_window(
&mut self,
window: winit::window::Window,
) -> std::result::Result<(), egui_wgpu::WgpuError> {
self.window = Some(window);
if let Some(running) = &mut self.running {
unsafe {
running.painter.set_window(self.window.as_ref());
pollster::block_on(running.painter.set_window(self.window.as_ref()))?;
}
}
Ok(())
}
#[allow(unsafe_code)]
#[cfg(target_os = "android")]
fn drop_window(&mut self) {
fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> {
self.window = None;
if let Some(running) = &mut self.running {
unsafe {
running.painter.set_window(None);
pollster::block_on(running.painter.set_window(None))?;
}
}
Ok(())
}
fn init_run_state(
@ -962,15 +1102,16 @@ mod wgpu_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<Box<dyn epi::Storage>>,
window: winit::window::Window,
) {
) -> std::result::Result<(), egui_wgpu::WgpuError> {
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new(
self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _,
self.native_options.depth_buffer,
self.native_options.transparent,
);
painter.set_window(Some(&window));
pollster::block_on(painter.set_window(Some(&window)))?;
painter
};
@ -1028,6 +1169,8 @@ mod wgpu_integration {
app,
});
self.window = Some(window);
Ok(())
}
}
@ -1135,8 +1278,8 @@ mod wgpu_integration {
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult {
match event {
) -> Result<EventResult> {
Ok(match event {
winit::event::Event::Resumed => {
if let Some(running) = &self.running {
if self.window.is_none() {
@ -1145,8 +1288,8 @@ mod wgpu_integration {
running.integration.frame.storage(),
&self.app_name,
&self.native_options,
);
self.set_window(window);
)?;
self.set_window(window)?;
}
} else {
let storage = epi_integration::create_storage(&self.app_name);
@ -1155,14 +1298,14 @@ mod wgpu_integration {
storage.as_deref(),
&self.app_name,
&self.native_options,
);
self.init_run_state(event_loop, storage, window);
)?;
self.init_run_state(event_loop, storage, window)?;
}
EventResult::RepaintNow
}
winit::event::Event::Suspended => {
#[cfg(target_os = "android")]
self.drop_window();
self.drop_window()?;
EventResult::Wait
}
@ -1212,7 +1355,8 @@ mod wgpu_integration {
winit::event::WindowEvent::CloseRequested
if running.integration.should_close() =>
{
return EventResult::Exit
tracing::debug!("Received WindowEvent::CloseRequested");
return Ok(EventResult::Exit);
}
_ => {}
};
@ -1250,7 +1394,7 @@ mod wgpu_integration {
}
}
_ => EventResult::Wait,
}
})
}
}
@ -1258,24 +1402,15 @@ mod wgpu_integration {
app_name: &str,
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) {
) -> Result<()> {
if native_options.run_and_return {
with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
with_event_loop(native_options, |event_loop, native_options| {
let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, wgpu_eframe);
});
run_and_return(event_loop, wgpu_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}

View file

@ -1,11 +1,11 @@
use super::{web_painter::WebPainter, *};
use crate::epi;
use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
pub use egui::{pos2, Color32};
use crate::{epi, App};
use super::{web_painter::WebPainter, *};
// ----------------------------------------------------------------------------
@ -284,7 +284,7 @@ impl AppRunner {
/// Get mutable access to the concrete [`App`] we enclose.
///
/// This will panic if your app does not implement [`App::as_any_mut`].
pub fn app_mut<ConreteApp: 'static + crate::App>(&mut self) -> &mut ConreteApp {
pub fn app_mut<ConreteApp: 'static + App>(&mut self) -> &mut ConreteApp {
self.app
.as_any_mut()
.expect("Your app must implement `as_any_mut`, but it doesn't")
@ -313,10 +313,11 @@ impl AppRunner {
pub fn warm_up(&mut self) -> Result<(), JsValue> {
if self.app.warm_up_enabled() {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
self.egui_ctx
.memory_mut(|m| m.set_everything_is_visible(true));
self.logic()?;
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
Ok(())
@ -388,7 +389,7 @@ impl AppRunner {
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options().screen_reader {
if self.egui_ctx.options(|o| o.screen_reader) {
self.screen_reader
.speak(&platform_output.events_description());
}
@ -449,8 +450,6 @@ pub enum EventToUnsubscribe {
impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
match self {
EventToUnsubscribe::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback(
@ -467,6 +466,7 @@ impl EventToUnsubscribe {
}
}
}
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
@ -485,8 +485,6 @@ impl AppRunnerContainer {
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics

View file

@ -1,6 +1,9 @@
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
struct IsDestroyed(pub bool);
pub fn paint_and_schedule(
@ -28,7 +31,6 @@ pub fn paint_and_schedule(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
@ -64,11 +66,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.modifiers = modifiers;
let key = event.key();
let egui_key = translate_key(&key);
if let Some(key) = translate_key(&key) {
if let Some(key) = egui_key {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
repeat: false, // egui will fill this in for us!
modifiers,
});
}
@ -84,10 +88,18 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let prevent_default = if matches!(event.key().as_str(), "Tab") {
#[allow(clippy::if_same_then_else)]
let prevent_default = if egui_key == Some(Key::Tab) {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} else if egui_key == Some(Key::P) {
#[allow(clippy::needless_bool)]
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
} else {
false // let normal P:s through
}
} else if egui_wants_keyboard {
matches!(
event.key().as_str(),
@ -111,6 +123,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
if prevent_default {
event.prevent_default();
// event.stop_propagation();
}
},
)?;
@ -125,6 +138,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: false,
repeat: false,
modifiers,
});
}
@ -196,15 +210,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{
let prevent_default_events = [
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
"contextmenu",
// Allow users to use ctrl-p for e.g. a command palette
"afterprint",
];
for event_name in prevent_default_events {
let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
event.prevent_default();
// event.stop_propagation();
// tracing::debug!("Preventing event {:?}", event_name);
};
runner_container.add_event_listener(&canvas, event_name, closure)?;
@ -388,7 +408,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
}
web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL

View file

@ -83,7 +83,6 @@ pub fn system_theme() -> Option<Theme> {
}
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
use wasm_bindgen::JsCast;
let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()

View file

@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
match ron::from_str(&memory_string) {
Ok(memory) => {
*ctx.memory() = memory;
ctx.memory_mut(|m| *m = memory);
}
Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err);
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) {
match ron::to_string(&*ctx.memory()) {
match ctx.memory(|mem| ron::to_string(mem)) {
Ok(ron) => {
local_storage_set("egui_memory_ron", &ron);
}

View file

@ -10,7 +10,6 @@ use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.document()
@ -23,7 +22,6 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
/// Text event handler,
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
@ -129,7 +127,6 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;

View file

@ -1,4 +1,3 @@
use egui::Rgba;
use wasm_bindgen::JsValue;
/// Renderer for a browser canvas.
@ -19,7 +18,7 @@ pub(crate) trait WebPainter {
/// Update all internal textures and paint gui.
fn paint_and_update_textures(
&mut self,
clear_color: Rgba,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,

View file

@ -2,7 +2,6 @@ use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui::Rgba;
use egui_glow::glow;
use crate::{WebGlContextOption, WebOptions};
@ -49,7 +48,7 @@ impl WebPainter for WebPainterGlow {
fn paint_and_update_textures(
&mut self,
clear_color: Rgba,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui::{mutex::RwLock, Rgba};
use egui::mutex::RwLock;
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
use crate::WebOptions;
@ -49,6 +49,7 @@ impl WebPainterWgpu {
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
})
@ -60,8 +61,13 @@ impl WebPainterWgpu {
let canvas = super::canvas_element_or_die(canvas_id);
let instance = wgpu::Instance::new(options.wgpu_options.backends);
let surface = instance.create_surface_from_canvas(&canvas);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: options.wgpu_options.backends,
dx12_shader_compiler: Default::default(),
});
let surface = instance
.create_surface_from_canvas(&canvas)
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
@ -81,7 +87,7 @@ impl WebPainterWgpu {
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
let target_format =
egui_wgpu::preferred_framebuffer_format(&surface.get_supported_formats(&adapter));
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
let depth_format = options.wgpu_options.depth_format;
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
@ -99,6 +105,7 @@ impl WebPainterWgpu {
height: 0,
present_mode: options.wgpu_options.present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![target_format],
};
tracing::debug!("wgpu painter initialized.");
@ -128,7 +135,7 @@ impl WebPainter for WebPainterWgpu {
fn paint_and_update_textures(
&mut self,
clear_color: Rgba,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
@ -221,10 +228,10 @@ impl WebPainter for WebPainterWgpu {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color.r() as f64,
g: clear_color.g() as f64,
b: clear_color.b() as f64,
a: clear_color.a() as f64,
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: true,
},

View file

@ -5,6 +5,14 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased
## 0.21.0 - 2023-02-08
* Update to `wgpu` 0.15 ([#2629](https://github.com/emilk/egui/pull/2629))
* Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)).
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
* `winit::Painter` now supports transparent backbuffer ([#2684](https://github.com/emilk/egui/pull/2684)).
## 0.20.0 - 2022-12-08 - web support
* Renamed `RenderPass` to `Renderer`.
* Renamed `RenderPass::execute` to `RenderPass::render`.

View file

@ -1,6 +1,6 @@
[package]
name = "egui-wgpu"
version = "0.20.0"
version = "0.21.0"
description = "Bindings for using egui natively using the wgpu library"
authors = [
"Nils Hasenbanck <nils@hasenbanck.de>",
@ -32,25 +32,24 @@ all-features = true
puffin = ["dep:puffin"]
## Enable [`winit`](https://docs.rs/winit) integration.
winit = ["dep:pollster", "dep:winit"]
winit = ["dep:winit"]
[dependencies]
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
"bytemuck",
] }
bytemuck = "1.7"
tracing = { version = "0.1", default-features = false, features = ["std"] }
type-map = "0.5.0"
wgpu = "0.14"
wgpu = "0.15.0"
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
pollster = { version = "0.2", optional = true }
winit = { version = "0.27.2", optional = true }
winit = { version = "0.28", optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View file

@ -8,18 +8,19 @@
pub use wgpu;
/// Low-level painting of [`egui`] on [`wgpu`].
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
pub mod renderer;
pub use renderer::CallbackFn;
pub use renderer::Renderer;
/// Module for painting [`egui`] with [`wgpu`] on [`winit`].
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
use egui::mutex::RwLock;
use std::sync::Arc;
use epaint::mutex::RwLock;
/// Access to the render state for egui.
#[derive(Clone)]
pub struct RenderState {
@ -98,7 +99,39 @@ pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::Te
}
formats[0] // take the first
}
// maybe use this-error?
#[derive(Debug)]
pub enum WgpuError {
DeviceError(wgpu::RequestDeviceError),
SurfaceError(wgpu::CreateSurfaceError),
}
impl std::fmt::Display for WgpuError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl std::error::Error for WgpuError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WgpuError::DeviceError(e) => e.source(),
WgpuError::SurfaceError(e) => e.source(),
}
}
}
impl From<wgpu::RequestDeviceError> for WgpuError {
fn from(e: wgpu::RequestDeviceError) -> Self {
Self::DeviceError(e)
}
}
impl From<wgpu::CreateSurfaceError> for WgpuError {
fn from(e: wgpu::CreateSurfaceError) -> Self {
Self::SurfaceError(e)
}
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"

View file

@ -4,14 +4,13 @@ use std::num::NonZeroU64;
use std::ops::Range;
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
use egui::epaint::Vertex;
use egui::NumExt;
use egui::{epaint::Primitive, PaintCallbackInfo};
use type_map::concurrent::TypeMap;
use wgpu;
use wgpu::util::DeviceExt as _;
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU
/// rendering.
///
/// The callback is composed of two functions: `prepare` and `paint`:
@ -154,11 +153,11 @@ pub struct Renderer {
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
/// sampler.
textures: HashMap<egui::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
next_user_texture_id: u64,
samplers: HashMap<egui::TextureOptions, wgpu::Sampler>,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
/// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
/// pipelines that must have the lifetime of the renderpass.
pub paint_callback_resources: TypeMap,
}
@ -346,7 +345,7 @@ impl Renderer {
pub fn render<'rp>(
&'rp self,
render_pass: &mut wgpu::RenderPass<'rp>,
paint_jobs: &[egui::epaint::ClippedPrimitive],
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) {
crate::profile_function!();
@ -361,7 +360,7 @@ impl Renderer {
let mut index_buffer_slices = self.index_buffer.slices.iter();
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
for egui::ClippedPrimitive {
for epaint::ClippedPrimitive {
clip_rect,
primitive,
} in paint_jobs
@ -475,8 +474,8 @@ impl Renderer {
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: egui::TextureId,
image_delta: &egui::epaint::ImageDelta,
id: epaint::TextureId,
image_delta: &epaint::ImageDelta,
) {
crate::profile_function!();
@ -490,7 +489,7 @@ impl Renderer {
};
let data_color32 = match &image_delta.image {
egui::ImageData::Color(image) => {
epaint::ImageData::Color(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
@ -498,7 +497,7 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
egui::ImageData::Font(image) => {
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
@ -555,6 +554,7 @@ impl Renderer {
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
});
let sampler = self
.samplers
@ -582,7 +582,7 @@ impl Renderer {
};
}
pub fn free_texture(&mut self, id: &egui::TextureId) {
pub fn free_texture(&mut self, id: &epaint::TextureId) {
self.textures.remove(id);
}
@ -590,15 +590,15 @@ impl Renderer {
///
/// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`egui::Context::load_texture`].
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
pub fn texture(
&self,
id: &egui::TextureId,
id: &epaint::TextureId,
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
self.textures.get(id)
}
/// Registers a `wgpu::Texture` with a `egui::TextureId`.
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
///
/// 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
@ -609,7 +609,7 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
) -> egui::TextureId {
) -> epaint::TextureId {
self.register_native_texture_with_sampler_options(
device,
texture,
@ -622,7 +622,7 @@ impl Renderer {
)
}
/// Registers a `wgpu::Texture` with an existing `egui::TextureId`.
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
///
/// This enables applications to reuse `TextureId`s.
pub fn update_egui_texture_from_wgpu_texture(
@ -630,7 +630,7 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
id: egui::TextureId,
id: epaint::TextureId,
) {
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
device,
@ -645,7 +645,7 @@ impl Renderer {
);
}
/// Registers a `wgpu::Texture` with a `egui::TextureId` while also accepting custom
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to specify individual minification/magnification filters as well as
@ -660,7 +660,7 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
) -> egui::TextureId {
) -> epaint::TextureId {
crate::profile_function!();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@ -683,14 +683,14 @@ impl Renderer {
],
});
let id = egui::TextureId::User(self.next_user_texture_id);
let id = epaint::TextureId::User(self.next_user_texture_id);
self.textures.insert(id, (None, bind_group));
self.next_user_texture_id += 1;
id
}
/// Registers a `wgpu::Texture` with an existing `egui::TextureId` while also accepting custom
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to reuse `TextureId`s created with custom sampler options.
@ -700,7 +700,7 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
id: egui::TextureId,
id: epaint::TextureId,
) {
crate::profile_function!();
@ -741,7 +741,7 @@ impl Renderer {
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
paint_jobs: &[egui::epaint::ClippedPrimitive],
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) -> Vec<wgpu::CommandBuffer> {
crate::profile_function!();
@ -801,7 +801,7 @@ impl Renderer {
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
crate::profile_scope!("primitives");
for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
match primitive {
Primitive::Mesh(mesh) => {
{
@ -844,14 +844,17 @@ impl Renderer {
}
}
fn create_sampler(options: egui::TextureOptions, device: &wgpu::Device) -> wgpu::Sampler {
fn create_sampler(
options: epaint::textures::TextureOptions,
device: &wgpu::Device,
) -> wgpu::Sampler {
let mag_filter = match options.magnification {
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
let min_filter = match options.minification {
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!(
@ -893,7 +896,7 @@ struct ScissorRect {
}
impl ScissorRect {
fn new(clip_rect: &egui::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
// Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y;

View file

@ -1,13 +1,14 @@
use std::sync::Arc;
use egui::mutex::RwLock;
use epaint::mutex::RwLock;
use tracing::error;
use wgpu::{Adapter, Instance, Surface};
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
struct SurfaceState {
surface: Surface,
surface: wgpu::Surface,
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
}
@ -18,11 +19,12 @@ struct SurfaceState {
pub struct Painter {
configuration: WgpuConfiguration,
msaa_samples: u32,
support_transparent_backbuffer: bool,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
instance: Instance,
adapter: Option<Adapter>,
instance: wgpu::Instance,
adapter: Option<wgpu::Adapter>,
render_state: Option<RenderState>,
surface_state: Option<SurfaceState>,
}
@ -40,12 +42,21 @@ impl Painter {
/// [`set_window()`](Self::set_window) once you have
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
/// associated.
pub fn new(configuration: WgpuConfiguration, msaa_samples: u32, depth_bits: u8) -> Self {
let instance = wgpu::Instance::new(configuration.backends);
pub fn new(
configuration: WgpuConfiguration,
msaa_samples: u32,
depth_bits: u8,
support_transparent_backbuffer: bool,
) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: configuration.backends,
dx12_shader_compiler: Default::default(), //
});
Self {
configuration,
msaa_samples,
support_transparent_backbuffer,
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
depth_texture_view: None,
@ -60,26 +71,27 @@ impl Painter {
///
/// Will return [`None`] if the render state has not been initialized yet.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.as_ref().cloned()
self.render_state.clone()
}
async fn init_render_state(
&self,
adapter: &Adapter,
adapter: &wgpu::Adapter,
target_format: wgpu::TextureFormat,
) -> RenderState {
let (device, queue) =
pollster::block_on(adapter.request_device(&self.configuration.device_descriptor, None))
.unwrap();
let renderer = Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
) -> Result<RenderState, wgpu::RequestDeviceError> {
adapter
.request_device(&self.configuration.device_descriptor, None)
.await
.map(|(device, queue)| {
let renderer =
Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
RenderState {
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
renderer: Arc::new(RwLock::new(renderer)),
}
})
}
// We want to defer the initialization of our render state until we have a surface
@ -87,54 +99,52 @@ impl Painter {
//
// After we've initialized our render state once though we expect all future surfaces
// will have the same format and so this render state will remain valid.
fn ensure_render_state_for_surface(&mut self, surface: &Surface) {
self.adapter.get_or_insert_with(|| {
pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions {
async fn ensure_render_state_for_surface(
&mut self,
surface: &wgpu::Surface,
) -> Result<(), wgpu::RequestDeviceError> {
if self.adapter.is_none() {
self.adapter = self
.instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: self.configuration.power_preference,
compatible_surface: Some(surface),
force_fallback_adapter: false,
}))
.unwrap()
});
})
.await;
}
if self.render_state.is_none() {
let adapter = self.adapter.as_ref().unwrap();
let swapchain_format =
crate::preferred_framebuffer_format(&surface.get_supported_formats(adapter));
let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format));
match &self.adapter {
Some(adapter) => {
let swapchain_format = crate::preferred_framebuffer_format(
&surface.get_capabilities(adapter).formats,
);
let rs = self.init_render_state(adapter, swapchain_format).await?;
self.render_state = Some(rs);
}
None => return Err(wgpu::RequestDeviceError {}),
}
}
Ok(())
}
fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
crate::profile_function!();
let render_state = self
.render_state
.as_ref()
.expect("Render state should exist before surface configuration");
let format = render_state.target_format;
let config = wgpu::SurfaceConfiguration {
fn configure_surface(
surface_state: &SurfaceState,
render_state: &RenderState,
present_mode: wgpu::PresentMode,
) {
surface_state.surface.configure(
&render_state.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: width_in_pixels,
height: height_in_pixels,
present_mode: self.configuration.present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
};
let surface_state = self
.surface_state
.as_mut()
.expect("Surface state should exist before surface configuration");
surface_state
.surface
.configure(&render_state.device, &config);
surface_state.width = width_in_pixels;
surface_state.height = height_in_pixels;
format: render_state.target_format,
width: surface_state.width,
height: surface_state.height,
present_mode,
alpha_mode: surface_state.alpha_mode,
view_formats: vec![render_state.target_format],
},
);
}
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
@ -161,27 +171,53 @@ impl Painter {
/// The raw Window handle associated with the given `window` must be a valid object to create a
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
/// be cleared by passing `None`).
pub unsafe fn set_window(&mut self, window: Option<&winit::window::Window>) {
///
/// # Errors
/// If the provided wgpu configuration does not match an available device.
pub async unsafe fn set_window(
&mut self,
window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> {
match window {
Some(window) => {
let surface = self.instance.create_surface(&window);
let surface = self.instance.create_surface(&window)?;
self.ensure_render_state_for_surface(&surface);
self.ensure_render_state_for_surface(&surface).await?;
let alpha_mode = if self.support_transparent_backbuffer {
let supported_alpha_modes = surface
.get_capabilities(self.adapter.as_ref().unwrap())
.alpha_modes;
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else if supported_alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else {
tracing::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
wgpu::CompositeAlphaMode::Auto
}
} else {
wgpu::CompositeAlphaMode::Auto
};
let size = window.inner_size();
let width = size.width;
let height = size.height;
self.surface_state = Some(SurfaceState {
surface,
width,
height,
width: size.width,
height: size.height,
alpha_mode,
});
self.resize_and_generate_depth_texture_view(width, height);
self.resize_and_generate_depth_texture_view(size.width, size.height);
}
None => {
self.surface_state = None;
}
}
Ok(())
}
/// Returns the maximum texture dimension supported if known
@ -195,15 +231,22 @@ impl Painter {
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
}
pub fn resize_and_generate_depth_texture_view(
fn resize_and_generate_depth_texture_view(
&mut self,
width_in_pixels: u32,
height_in_pixels: u32,
) {
self.configure_surface(width_in_pixels, height_in_pixels);
let device = &self.render_state.as_ref().unwrap().device;
let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surface_state.as_mut().unwrap();
surface_state.width = width_in_pixels;
surface_state.height = height_in_pixels;
Self::configure_surface(surface_state, render_state, self.configuration.present_mode);
self.depth_texture_view = self.depth_format.map(|depth_format| {
device
render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
@ -217,6 +260,7 @@ impl Painter {
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
});
@ -233,9 +277,9 @@ impl Painter {
pub fn paint_and_update_textures(
&mut self,
pixels_per_point: f32,
clear_color: egui::Rgba,
clipped_primitives: &[egui::ClippedPrimitive],
textures_delta: &egui::TexturesDelta,
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
) {
crate::profile_function!();
@ -247,7 +291,6 @@ impl Painter {
Some(rs) => rs,
None => return,
};
let (width, height) = (surface_state.width, surface_state.height);
let output_frame = {
crate::profile_scope!("get_current_texture");
@ -260,7 +303,11 @@ impl Painter {
#[allow(clippy::single_match_else)]
Err(e) => match (*self.configuration.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => {
self.configure_surface(width, height);
Self::configure_surface(
surface_state,
render_state,
self.configuration.present_mode,
);
return;
}
SurfaceErrorAction::SkipFrame => {
@ -278,7 +325,7 @@ impl Painter {
// Upload all resources for the GPU.
let screen_descriptor = renderer::ScreenDescriptor {
size_in_pixels: [width, height],
size_in_pixels: [surface_state.width, surface_state.height],
pixels_per_point,
};
@ -313,10 +360,10 @@ impl Painter {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color.r() as f64,
g: clear_color.g() as f64,
b: clear_color.b() as f64,
a: clear_color.a() as f64,
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: true,
},

View file

@ -5,6 +5,21 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
## 0.21.1 - 2023-02-12
* Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
## 0.21.0 - 2023-02-08
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
* Fix bug where the cursor could get stuck using the wrong icon.
## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)).
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).

View file

@ -1,6 +1,6 @@
[package]
name = "egui-winit"
version = "0.20.0"
version = "0.21.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition = "2021"
@ -36,9 +36,6 @@ links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Experimental support for a screen reader.
screen_reader = ["tts"]
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde"]
@ -46,33 +43,34 @@ serde = ["egui/serde", "dep:serde"]
wayland = ["winit/wayland"]
[dependencies]
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"tracing",
] }
instant = { version = "0.1", features = [
"wasm-bindgen",
] } # We use instant so we can (maybe) compile for web
tracing = { version = "0.1", default-features = false, features = ["std"] }
winit = { version = "0.27.2", default-features = false }
winit = { version = "0.28", default-features = false }
#! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.10.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
# feature accesskit
accesskit_winit = { version = "0.7.1", optional = true }
puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
# feature screen_reader
tts = { version = "0.24", optional = true }
webbrowser = { version = "0.8", optional = true }
webbrowser = { version = "0.8.3", optional = true }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.6.3", optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.2", optional = true, default-features = false }
[target.'cfg(target_os = "android")'.dependencies]
# TODO(emilk): this is probably not the right place for specifying native-activity, but we need to do it somewhere for the CI
android-activity = { version = "0.4", features = ["native-activity"] }

View file

@ -30,6 +30,7 @@ impl Clipboard {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(),
#[cfg(all(
any(
target_os = "linux",
@ -41,6 +42,7 @@ impl Clipboard {
feature = "smithay-clipboard"
))]
smithay: init_smithay_clipboard(wayland_display),
clipboard: Default::default(),
}
}
@ -60,7 +62,7 @@ impl Clipboard {
return match clipboard.load() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("Paste error: {}", err);
tracing::error!("smithay paste error: {err}");
None
}
};
@ -71,7 +73,7 @@ impl Clipboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("Paste error: {}", err);
tracing::error!("arboard paste error: {err}");
None
}
};
@ -99,7 +101,7 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
tracing::error!("Copy/Cut error: {}", err);
tracing::error!("arboard copy/cut error: {err}");
}
return;
}
@ -110,10 +112,11 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> {
tracing::debug!("Initializing arboard clipboard…");
match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(err) => {
tracing::error!("Failed to initialize clipboard: {}", err);
tracing::warn!("Failed to initialize arboard clipboard: {err}");
None
}
}
@ -133,10 +136,11 @@ fn init_smithay_clipboard(
wayland_display: Option<*mut c_void>,
) -> Option<smithay_clipboard::Clipboard> {
if let Some(display) = wayland_display {
tracing::debug!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
} else {
tracing::error!("Cannot initialize smithay clipboard without a display handle!");
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
None
}
}

View file

@ -19,23 +19,12 @@ use egui::accesskit;
pub use winit;
pub mod clipboard;
pub mod screen_reader;
mod window_settings;
pub use window_settings::WindowSettings;
use winit::event_loop::EventLoopWindowTarget;
#[cfg(feature = "wayland")]
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
use winit::platform::unix::EventLoopWindowTargetExtUnix;
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
window.scale_factor() as f32
}
@ -70,12 +59,12 @@ pub struct State {
egui_input: egui::RawInput,
pointer_pos_in_points: Option<egui::Pos2>,
any_pointer_button_down: bool,
current_cursor_icon: egui::CursorIcon,
current_cursor_icon: Option<egui::CursorIcon>,
/// What egui uses.
current_pixels_per_point: f32,
clipboard: clipboard::Clipboard,
screen_reader: screen_reader::ScreenReader,
/// If `true`, mouse inputs will be treated as touches.
/// Useful for debugging touch support in egui.
@ -111,11 +100,10 @@ impl State {
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: egui::CursorIcon::Default,
current_cursor_icon: None,
current_pixels_per_point: 1.0,
clipboard: clipboard::Clipboard::new(wayland_display),
screen_reader: screen_reader::ScreenReader::default(),
simulate_touch_screen: false,
pointer_touch_id: None,
@ -380,8 +368,9 @@ impl State {
consumed: false,
}
}
WindowEvent::AxisMotion { .. }
| WindowEvent::CloseRequested
// Things that may require repaint:
WindowEvent::CloseRequested
| WindowEvent::CursorEntered { .. }
| WindowEvent::Destroyed
| WindowEvent::Occluded(_)
@ -391,10 +380,26 @@ impl State {
repaint: true,
consumed: false,
},
WindowEvent::Moved(_) => EventResponse {
repaint: false, // moving a window doesn't warrant a repaint
// Things we completely ignore:
WindowEvent::AxisMotion { .. }
| WindowEvent::Moved(_)
| WindowEvent::SmartMagnify { .. }
| WindowEvent::TouchpadRotate { .. } => EventResponse {
repaint: false,
consumed: false,
},
WindowEvent::TouchpadMagnify { delta, .. } => {
// Positive delta values indicate magnification (zooming in).
// Negative delta values indicate shrinking (zooming out).
let zoom_factor = (*delta as f32).exp();
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
}
}
@ -594,6 +599,7 @@ impl State {
self.egui_input.events.push(egui::Event::Key {
key,
pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers,
});
}
@ -614,11 +620,6 @@ impl State {
egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput,
) {
if egui_ctx.options().screen_reader {
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::PlatformOutput {
cursor_icon,
open_url,
@ -654,23 +655,26 @@ impl State {
}
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
// prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing
#[cfg(windows)]
if self.current_cursor_icon == cursor_icon {
if self.current_cursor_icon == Some(cursor_icon) {
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
// On other platforms: just early-out to save CPU.
return;
}
self.current_cursor_icon = cursor_icon;
if let Some(cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window {
window.set_cursor_icon(cursor_icon);
}
self.current_cursor_icon = Some(cursor_icon);
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor_icon(winit_cursor_icon);
} else {
window.set_cursor_visible(false);
}
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
}
}
}
@ -878,6 +882,7 @@ fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_v
target_os = "openbsd"
))]
{
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
return _event_loop.wayland_display();
}

View file

@ -1,49 +0,0 @@
pub struct ScreenReader {
#[cfg(feature = "tts")]
tts: Option<tts::Tts>,
}
#[cfg(not(feature = "tts"))]
#[allow(clippy::derivable_impls)] // False positive
impl Default for ScreenReader {
fn default() -> Self {
Self {}
}
}
#[cfg(feature = "tts")]
impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::Tts::default() {
Ok(screen_reader) => {
tracing::debug!("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
tracing::warn!("Failed to load screen reader: {}", err);
None
}
};
Self { tts }
}
}
impl ScreenReader {
#[cfg(not(feature = "tts"))]
#[allow(clippy::unused_self)]
pub fn speak(&mut self, _text: &str) {}
#[cfg(feature = "tts")]
pub fn speak(&mut self, text: &str) {
if text.is_empty() {
return;
}
if let Some(tts) = &mut self.tts {
tracing::debug!("Speaking: {:?}", text);
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
tracing::warn!("Failed to read: {}", err);
}
}
}
}

View file

@ -42,23 +42,25 @@ impl WindowSettings {
}
}
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
self.inner_size_points
}
pub fn initialize_window(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
if !cfg!(target_os = "windows") {
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the window is hidden and very difficult to find.
// So we don't restore window positions on Windows.
// If this happens on Windows, the clamping behavior is managed by the function
// clamp_window_to_sane_position.
if let Some(pos) = self.position {
window = window.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
}
if let Some(inner_size_points) = self.inner_size_points {
window
@ -74,4 +76,69 @@ impl WindowSettings {
window
}
}
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
use egui::NumExt as _;
if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows
let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size);
*size = size.at_most(max_size);
}
}
pub fn clamp_window_to_sane_position<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
}
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "egui"
version = "0.20.0"
version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021"
@ -54,8 +54,12 @@ persistence = ["serde", "epaint/serde", "ron"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
[dependencies]
epaint = { version = "0.20.0", path = "../epaint", default-features = false }
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
ahash = { version = "0.8.1", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
@ -66,7 +70,7 @@ nohash-hasher = "0.2"
#! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.8.1", optional = true }
accesskit = { version = "0.9.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }

View file

@ -9,6 +9,7 @@ pub(crate) struct AnimationManager {
#[derive(Clone, Debug)]
struct BoolAnim {
value: bool,
/// when did `value` last toggle?
toggle_time: f64,
}
@ -16,7 +17,9 @@ struct BoolAnim {
#[derive(Clone, Debug)]
struct ValueAnim {
from_value: f32,
to_value: f32,
/// when did `value` last toggle?
toggle_time: f64,
}

View file

@ -5,12 +5,14 @@
use crate::*;
/// State that is persisted between frames.
// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be?
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State {
/// Last known pos
pub pos: Pos2,
/// Last known pos of the pivot
pub pivot_pos: Pos2,
pub pivot: Align2,
/// Last know size. Used for catching clicks.
pub size: Vec2,
@ -21,8 +23,22 @@ pub(crate) struct State {
}
impl State {
pub fn left_top_pos(&self) -> Pos2 {
pos2(
self.pivot_pos.x - self.pivot.x().to_factor() * self.size.x,
self.pivot_pos.y - self.pivot.y().to_factor() * self.size.y,
)
}
pub fn set_left_top_pos(&mut self, pos: Pos2) {
self.pivot_pos = pos2(
pos.x + self.pivot.x().to_factor() * self.size.x,
pos.y + self.pivot.y().to_factor() * self.size.y,
);
}
pub fn rect(&self) -> Rect {
Rect::from_min_size(self.pos, self.size)
Rect::from_min_size(self.left_top_pos(), self.size)
}
}
@ -231,27 +247,25 @@ impl Area {
let layer_id = LayerId::new(order, id);
let state = ctx.memory().areas.get(id).copied();
let state = ctx.memory(|mem| mem.areas.get(id).copied());
let is_new = state.is_none();
if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
}
let mut state = state.unwrap_or_else(|| State {
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
pivot,
size: Vec2::ZERO,
interactable,
});
state.pos = new_pos.unwrap_or(state.pos);
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.interactable = interactable;
if pivot != Align2::LEFT_TOP {
state.pos.x -= pivot.x().to_factor() * state.size.x;
state.pos.y -= pivot.y().to_factor() * state.size.y;
}
if let Some((anchor, offset)) = anchor {
let screen = ctx.available_rect();
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
state.set_left_top_pos(
anchor.align_size_within_rect(state.size, screen).left_top() + offset,
);
}
// interact right away to prevent frame-delay
@ -278,31 +292,33 @@ impl Area {
// Important check - don't try to move e.g. a combobox popup!
if movable {
if move_response.dragged() {
state.pos += ctx.input().pointer.delta();
state.pivot_pos += ctx.input(|i| i.pointer.delta());
}
state.pos = ctx
.constrain_window_rect_to_area(state.rect(), drag_bounds)
.min;
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.min,
);
}
if (move_response.dragged() || move_response.clicked())
|| pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
{
ctx.memory().areas.move_to_top(layer_id);
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
ctx.request_repaint();
}
move_response
};
state.pos = ctx.round_pos_to_pixels(state.pos);
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
if constrain {
state.pos = ctx
.constrain_window_rect_to_area(state.rect(), drag_bounds)
.min;
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.left_top(),
);
}
Prepared {
@ -329,7 +345,7 @@ impl Area {
}
let layer_id = LayerId::new(self.order, self.id);
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
if let Some(area_rect) = area_rect {
let clip_rect = ctx.available_rect();
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
@ -358,7 +374,7 @@ impl Prepared {
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.input().screen_rect();
let screen_rect = ctx.screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds
@ -374,14 +390,16 @@ impl Prepared {
};
let max_rect = Rect::from_min_max(
self.state.pos,
bounds.max.at_least(self.state.pos + Vec2::splat(32.0)),
self.state.left_top_pos(),
bounds
.max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
);
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
let clip_rect = Rect::from_min_max(self.state.pos, bounds.max)
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max)
.expand(clip_rect_margin)
.intersect(bounds);
@ -410,7 +428,7 @@ impl Prepared {
state.size = content_ui.min_rect().size();
ctx.memory().areas.set_state(layer_id, state);
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
move_response
}
@ -418,7 +436,7 @@ impl Prepared {
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let any_pressed = ctx.input().pointer.any_pressed();
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
@ -426,13 +444,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
}
fn automatic_area_position(ctx: &Context) -> Pos2 {
let mut existing: Vec<Rect> = ctx
.memory()
.areas
let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas
.visible_windows()
.into_iter()
.map(State::rect)
.collect();
.collect()
});
existing.sort_by_key(|r| r.left().round() as i32);
let available_rect = ctx.available_rect();

View file

@ -26,13 +26,14 @@ pub struct CollapsingState {
impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data()
.get_persisted::<InnerState>(id)
ctx.data_mut(|d| {
d.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
})
}
pub fn store(&self, ctx: &Context) {
ctx.data().insert_persisted(self.id, self.state);
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
}
pub fn id(&self) -> Id {
@ -64,7 +65,7 @@ impl CollapsingState {
/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory().everything_is_visible() {
if ctx.memory(|mem| mem.everything_is_visible()) {
1.0
} else {
ctx.animate_bool(self.id, self.state.open)
@ -111,10 +112,7 @@ impl CollapsingState {
response.rect.center().y,
));
let openness = self.openness(ui.ctx());
let small_icon_response = Response {
rect: icon_rect,
..response.clone()
};
let small_icon_response = response.clone().with_new_rect(icon_rect);
icon_fn(ui, openness, &small_icon_response);
response
}
@ -143,9 +141,10 @@ impl CollapsingState {
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
) -> HeaderResponse<'_, HeaderRet> {
let header_response = ui.horizontal(|ui| {
let prev_item_spacing = ui.spacing_mut().item_spacing;
ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
let collapser = self.show_default_button_indented(ui);
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
ui.spacing_mut().item_spacing = prev_item_spacing;
(collapser, add_header(ui))
});
HeaderResponse {
@ -555,7 +554,7 @@ impl CollapsingHeader {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
});
@ -575,10 +574,7 @@ impl CollapsingHeader {
header_response.rect.left() + ui.spacing().indent / 2.0,
header_response.rect.center().y,
));
let icon_response = Response {
rect: icon_rect,
..header_response.clone()
};
let icon_response = header_response.clone().with_new_rect(icon_rect);
if let Some(icon) = icon {
icon(ui, openness, &icon_response);
} else {

View file

@ -162,9 +162,6 @@ impl ComboBox {
let button_id = ui.make_persistent_id(id_source);
ui.horizontal(|ui| {
if let Some(width) = width {
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
}
let mut ir = combo_box_dyn(
ui,
button_id,
@ -172,6 +169,7 @@ impl ComboBox {
menu_contents,
icon,
wrap_enabled,
width,
);
if let Some(label) = label {
ir.response
@ -240,21 +238,17 @@ fn combo_box_dyn<'c, R>(
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>,
wrap_enabled: bool,
width: Option<f32>,
) -> InnerResponse<Option<R>> {
let popup_id = button_id.with("popup");
let is_popup_open = ui.memory().is_popup_open(popup_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui
.ctx()
.memory()
.areas
.get(popup_id)
.map_or(100.0, |state| state.size.y);
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
< ui.ctx().input().screen_rect().bottom()
< ui.ctx().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
@ -263,23 +257,39 @@ fn combo_box_dyn<'c, R>(
let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
let icon_spacing = ui.spacing().icon_spacing;
// We don't want to change width when user selects something new
let full_minimum_width = if wrap_enabled {
ui.available_width() - ui.spacing().item_spacing.x * 2.0
// Currently selected value's text will be wrapped if needed, so occupy the available width.
ui.available_width()
} else {
ui.spacing().slider_width - 2.0 * margin.x
// Occupy at least the minimum width assigned to ComboBox.
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
width - 2.0 * margin.x
};
let icon_size = Vec2::splat(ui.spacing().icon_width);
let wrap_width = if wrap_enabled {
ui.available_width() - ui.spacing().item_spacing.x - icon_size.x
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
ui.available_width() - icon_spacing - icon_size.x
} else {
// Use all the width necessary to display the currently selected value's text.
f32::INFINITY
};
let galley =
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
// The width necessary to contain the whole widget with the currently selected value's text.
let width = if wrap_enabled {
full_minimum_width
} else {
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
galley.size().x + icon_spacing + icon_size.x
};
// Case : wrap_enabled : occupy all the available width.
// Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox,
// increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)).
let width = width.at_least(full_minimum_width);
let height = galley.size().y.max(icon_size.y);
@ -319,7 +329,7 @@ fn combo_box_dyn<'c, R>(
});
if button_response.clicked() {
ui.memory().toggle_popup(popup_id);
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
let inner = crate::popup::popup_above_or_below_widget(
ui,
@ -376,7 +386,7 @@ fn button_frame(
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
},
);

View file

@ -19,11 +19,16 @@ use epaint::*;
pub struct Frame {
/// Margin within the painted frame.
pub inner_margin: Margin,
/// Margin outside the painted frame.
pub outer_margin: Margin,
pub rounding: Rounding,
pub shadow: Shadow,
pub fill: Color32,
pub stroke: Stroke,
}
@ -72,7 +77,7 @@ impl Frame {
pub fn menu(style: &Style) -> Self {
Self {
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.widgets.noninteractive.rounding,
rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -82,8 +87,8 @@ impl Frame {
pub fn popup(style: &Style) -> Self {
Self {
inner_margin: style.spacing.window_margin,
rounding: style.visuals.widgets.noninteractive.rounding,
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),

View file

@ -28,7 +28,7 @@ pub struct PanelState {
impl PanelState {
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data().get_persisted(bar_id)
ctx.data_mut(|d| d.get_persisted(bar_id))
}
/// The size of the panel (from previous frame).
@ -37,7 +37,7 @@ impl PanelState {
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_persisted(bar_id, self);
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
}
}
@ -245,11 +245,12 @@ impl SidePanel {
&& (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side;
let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks
if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line {
ui.memory().set_dragged_id(resize_id);
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line
{
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
}
is_resizing = ui.memory().is_being_dragged(resize_id);
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs();
let width =
@ -257,12 +258,12 @@ impl SidePanel {
side.set_rect_width(&mut panel_rect, width);
}
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
let dragging_something_else =
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
}
}
}
@ -296,12 +297,12 @@ impl SidePanel {
{
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
ui.style().visuals.widgets.hovered.bg_stroke
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TOOD(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
};
@ -334,19 +335,19 @@ impl SidePanel {
let layer_id = LayerId::background();
let side = self.side;
let available_rect = ctx.available_rect();
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
Side::Left => ctx
.frame_state()
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
Side::Right => ctx
.frame_state()
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
Side::Left => ctx.frame_state_mut(|state| {
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
}),
Side::Right => ctx.frame_state_mut(|state| {
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
}),
}
inner_response
}
@ -682,7 +683,7 @@ impl TopBottomPanel {
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
let latest_pos = ui.input().pointer.latest_pos();
let latest_pos = ui.input(|i| i.pointer.latest_pos());
if let Some(pointer) = latest_pos {
let we_are_on_top = ui
.ctx()
@ -695,13 +696,12 @@ impl TopBottomPanel {
&& (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line
{
ui.memory().interaction.drag_id = Some(resize_id);
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id));
}
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone())
@ -709,12 +709,12 @@ impl TopBottomPanel {
side.set_rect_height(&mut panel_rect, height);
}
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
let dragging_something_else =
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeVertical;
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
}
}
}
@ -748,12 +748,12 @@ impl TopBottomPanel {
{
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
ui.style().visuals.widgets.hovered.bg_stroke
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TOOD(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
};
@ -787,7 +787,7 @@ impl TopBottomPanel {
let available_rect = ctx.available_rect();
let side = self.side;
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
@ -795,12 +795,14 @@ impl TopBottomPanel {
match side {
TopBottomSide::Top => {
ctx.frame_state()
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
ctx.frame_state_mut(|state| {
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
});
}
TopBottomSide::Bottom => {
ctx.frame_state()
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
ctx.frame_state_mut(|state| {
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
});
}
}
@ -1042,14 +1044,13 @@ impl CentralPanel {
let layer_id = LayerId::background();
let id = Id::new("central_panel");
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.frame_state()
.allocate_central_panel(inner_response.response.rect);
ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
inner_response
}

View file

@ -13,11 +13,11 @@ pub(crate) struct TooltipState {
impl TooltipState {
pub fn load(ctx: &Context) -> Option<Self> {
ctx.data().get_temp(Id::null())
ctx.data_mut(|d| d.get_temp(Id::null()))
}
fn store(self, ctx: &Context) {
ctx.data().insert_temp(Id::null(), self);
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
}
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let suggested_pos = ctx
.input()
.pointer
.hover_pos()
.input(|i| i.pointer.hover_pos())
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
show_tooltip_at(ctx, id, suggested_pos, add_contents)
}
@ -112,7 +110,7 @@ pub fn show_tooltip_for<R>(
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
let (above, position) = if ctx.input().any_touches() {
let (above, position) = if ctx.input(|i| i.any_touches()) {
(true, expanded_rect.left_top())
} else {
(false, expanded_rect.left_bottom())
@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
let mut frame_state =
ctx.frame_state()
.tooltip_state
ctx.frame_state(|fs| fs.tooltip_state)
.unwrap_or(crate::frame_state::TooltipFrameState {
common_id: individual_id,
rect: Rect::NOTHING,
@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
}
} else if let Some(position) = suggested_position {
position
} else if ctx.memory().everything_is_visible() {
} else if ctx.memory(|mem| mem.everything_is_visible()) {
Pos2::ZERO
} else {
return None; // No good place for a tooltip :(
@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
position.y -= expected_size.y;
}
position = position.at_most(ctx.input().screen_rect().max - expected_size);
position = position.at_most(ctx.screen_rect().max - expected_size);
// check if we intersect the avoid_rect
{
@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
}
}
let position = position.at_least(ctx.input().screen_rect().min);
let position = position.at_least(ctx.screen_rect().min);
let area_id = frame_state.common_id.with(frame_state.count);
@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state().tooltip_state = Some(frame_state);
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
Some(inner)
}
@ -263,8 +260,9 @@ fn show_tooltip_area_dyn<'c, R>(
Area::new(area_id)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.constrain(true)
.interactable(false)
.drag_bounds(Rect::EVERYTHING) // disable clip rect
.drag_bounds(ctx.screen_rect())
.show(ctx, |ui| {
Frame::popup(&ctx.style())
.show(ui, |ui| {
@ -283,7 +281,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if *individual_id == tooltip_id {
let area_id = common_id.with(count);
let layer_id = LayerId::new(Order::Tooltip, area_id);
if ctx.memory().areas.visible_last_frame(&layer_id) {
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
return true;
}
}
@ -314,6 +312,8 @@ pub fn popup_below_widget<R>(
///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
///
/// The opened popup will have the same width as the parent.
///
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
///
/// Returns `None` if the popup is not open.
@ -323,7 +323,7 @@ pub fn popup_below_widget<R>(
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() {
/// ui.memory().toggle_popup(popup_id);
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// }
/// let below = egui::AboveOrBelow::Below;
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
@ -340,7 +340,7 @@ pub fn popup_above_or_below_widget<R>(
above_or_below: AboveOrBelow,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
if ui.memory().is_popup_open(popup_id) {
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let (pos, pivot) = match above_or_below {
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
@ -368,8 +368,8 @@ pub fn popup_above_or_below_widget<R>(
})
.inner;
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
ui.memory().close_popup();
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
ui.memory_mut(|mem| mem.close_popup());
}
Some(inner)
} else {

View file

@ -18,11 +18,11 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -180,7 +180,7 @@ impl Resize {
.at_least(self.min_size)
.at_most(self.max_size)
.at_most(
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
);
State {
@ -305,7 +305,7 @@ impl Resize {
paint_resize_corner(ui, &corner_response);
if corner_response.hovered() || corner_response.dragged() {
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
}
}

View file

@ -48,11 +48,11 @@ impl Default for State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -97,8 +97,10 @@ pub struct ScrollArea {
id_source: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@ -141,6 +143,7 @@ impl ScrollArea {
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2],
}
}
@ -267,6 +270,18 @@ impl ScrollArea {
self
}
/// Can the user drag the scroll area to scroll?
///
/// This is useful for touch screens.
///
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self
}
/// For each axis, should the containing area shrink if the content is small?
///
/// * If `true`, egui will add blank space outside the scroll area.
@ -336,6 +351,7 @@ impl ScrollArea {
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
stick_to_end,
} = self;
@ -410,19 +426,40 @@ impl ScrollArea {
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
// Nice handling of forced resizing beyond the possible:
{
// Clip the content, but only when we really need to:
let clip_rect_margin = ui.visuals().clip_rect_margin;
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if !has_bar[d] {
if has_bar[d] {
if state.content_is_too_large[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
}
if state.show_scroll[d] {
// Make sure content doesn't cover scroll bars
let tiny_gap = 1.0;
content_clip_rect.max[1 - d] =
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
}
} else {
// Nice handling of forced resizing beyond the possible:
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
}
}
// Make sure we din't accidentally expand the clip rect
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
content_ui.set_clip_rect(content_clip_rect);
}
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) {
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
@ -431,8 +468,10 @@ impl ScrollArea {
if content_response.dragged() {
for d in 0..2 {
if has_bar[d] {
state.offset[d] -= ui.input().pointer.delta()[d];
state.vel[d] = ui.input().pointer.velocity()[d];
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d];
});
state.scroll_stuck_to_end[d] = false;
} else {
state.vel[d] = 0.0;
@ -441,7 +480,7 @@ impl ScrollArea {
} else {
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input().unstable_dt;
let dt = ui.input(|i| i.unstable_dt);
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
@ -585,7 +624,9 @@ impl Prepared {
for d in 0..2 {
if has_bar[d] {
// We take the scroll target so only this ScrollArea will use it:
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if let Some((scroll, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
@ -650,8 +691,7 @@ impl Prepared {
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 {
if has_bar[d] {
let mut frame_state = ui.ctx().frame_state();
let scroll_delta = frame_state.scroll_delta;
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
@ -659,7 +699,7 @@ impl Prepared {
if scrolling_up || scrolling_down {
state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it.
frame_state.scroll_delta[d] = 0.0;
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
state.scroll_stuck_to_end[d] = false;
}
}
@ -801,7 +841,7 @@ impl Prepared {
),
)
};
let min_handle_size = ui.spacing().scroll_bar_width;
let min_handle_size = ui.spacing().scroll_handle_min_length;
if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size(
handle_rect.center(),

View file

@ -10,7 +10,7 @@ use super::*;
///
/// You can customize:
/// * title
/// * default, minimum, maximum and/or fixed size
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
/// * if the window has a scroll area (off by default)
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
/// * if there should be a close button (none by default)
@ -30,6 +30,7 @@ pub struct Window<'open> {
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
default_open: bool,
with_title_bar: bool,
}
@ -51,6 +52,7 @@ impl<'open> Window<'open> {
.default_size([340.0, 420.0]), // Default inner size of a window
scroll: ScrollArea::neither(),
collapsible: true,
default_open: true,
with_title_bar: true,
}
}
@ -77,6 +79,18 @@ impl<'open> Window<'open> {
self
}
/// If `false` the window will be non-interactive.
pub fn interactable(mut self, interactable: bool) -> Self {
self.area = self.area.interactable(interactable);
self
}
/// If `false` the window will be immovable.
pub fn movable(mut self, movable: bool) -> Self {
self.area = self.area.movable(movable);
self
}
/// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
// TODO(emilk): I'm not sure this is a good interface for this.
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
@ -162,6 +176,12 @@ impl<'open> Window<'open> {
self
}
/// Set initial collapsed state of the window
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
/// Set initial size of the window.
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.default_size(default_size);
@ -275,12 +295,14 @@ impl<'open> Window<'open> {
resize,
scroll,
collapsible,
default_open,
with_title_bar,
} = self;
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
area.show_open_close_animation(ctx, &frame, is_open);
if !is_open {
@ -291,7 +313,7 @@ impl<'open> Window<'open> {
let area_layer_id = area.layer();
let resize_id = area_id.with("resize");
let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
let is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
@ -318,7 +340,7 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
let style = ctx.style();
title.font_height(&ctx.fonts(), &style) + title_content_spacing
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
} else {
0.0
};
@ -404,7 +426,7 @@ impl<'open> Window<'open> {
ctx.style().visuals.widgets.active,
);
} else if let Some(hover_interaction) = hover_interaction {
if ctx.input().pointer.has_pointer() {
if ctx.input(|i| i.pointer.has_pointer()) {
paint_frame_interaction(
&mut area_content_ui,
outer_rect,
@ -416,9 +438,12 @@ impl<'open> Window<'open> {
content_inner
};
area.state_mut().pos = ctx
{
let pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
.min;
.left_top();
area.state_mut().set_left_top_pos(pos);
}
let full_response = area.end(ctx, area_content_ui);
@ -499,13 +524,13 @@ pub(crate) struct WindowInteraction {
impl WindowInteraction {
pub fn set_cursor(&self, ctx: &Context) {
if (self.left && self.top) || (self.right && self.bottom) {
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
} else if (self.right && self.top) || (self.left && self.bottom) {
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
} else if self.left || self.right {
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
} else if self.bottom || self.top {
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
}
}
@ -528,7 +553,7 @@ fn interact(
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds());
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
area.state_mut().pos = new_rect.min;
area.state_mut().set_left_top_pos(new_rect.left_top());
if window_interaction.is_resize() {
if let Some(mut state) = resize::State::load(ctx, resize_id) {
@ -537,7 +562,7 @@ fn interact(
}
}
ctx.memory().areas.move_to_top(area_layer_id);
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
Some(window_interaction)
}
@ -545,11 +570,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
window_interaction.set_cursor(ctx);
// Only move/resize windows with primary mouse button:
if !ctx.input().pointer.primary_down() {
if !ctx.input(|i| i.pointer.primary_down()) {
return None;
}
let pointer_pos = ctx.input().pointer.interact_pos()?;
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
let mut rect = window_interaction.start_rect; // prevent drift
if window_interaction.is_resize() {
@ -571,8 +596,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
// but we want anything interactive in the window (e.g. slider) to steal
// the drag from us. It is therefor important not to move the window the first frame,
// but instead let other widgets to the steal. HACK.
if !ctx.input().pointer.any_pressed() {
let press_origin = ctx.input().pointer.press_origin()?;
if !ctx.input(|i| i.pointer.any_pressed()) {
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
let delta = pointer_pos - press_origin;
rect = rect.translate(delta);
}
@ -590,30 +615,31 @@ fn window_interaction(
rect: Rect,
) -> Option<WindowInteraction> {
{
let drag_id = ctx.memory().interaction.drag_id;
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
if drag_id.is_some() && drag_id != Some(id) {
return None;
}
}
let mut window_interaction = { ctx.memory().window_interaction };
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
if window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx);
let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks
if any_pressed && ctx.input().pointer.primary_down() {
ctx.memory().interaction.drag_id = Some(id);
ctx.memory().interaction.drag_is_window = true;
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
ctx.memory_mut(|mem| {
mem.interaction.drag_id = Some(id);
mem.interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction);
ctx.memory().window_interaction = window_interaction;
mem.window_interaction = window_interaction;
});
}
}
}
if let Some(window_interaction) = window_interaction {
let is_active = ctx.memory().interaction.drag_id == Some(id);
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
if is_active && window_interaction.area_layer_id == area_layer_id {
return Some(window_interaction);
@ -629,10 +655,9 @@ fn resize_hover(
area_layer_id: LayerId,
rect: Rect,
) -> Option<WindowInteraction> {
let pointer = ctx.input().pointer.interact_pos()?;
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
let any_down = ctx.input().pointer.any_down(); // avoid deadlocks
if any_down && !ctx.input().pointer.any_pressed() {
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
return None; // already dragging (something)
}
@ -642,7 +667,7 @@ fn resize_hover(
}
}
if ctx.memory().interaction.drag_interest {
if ctx.memory(|mem| mem.interaction.drag_interest) {
// Another widget will become active if we drag here
return None;
}
@ -804,8 +829,8 @@ fn show_title_bar(
collapsible: bool,
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = title
.font_height(&ui.fonts(), ui.style())
let height = ui
.fonts(|fonts| title.font_height(fonts, ui.style()))
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);

File diff suppressed because it is too large Load diff

View file

@ -187,13 +187,14 @@ pub enum Event {
/// Was it pressed or released?
pressed: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
},
/// A key was repeated while pressed.
KeyRepeat {
key: Key,
/// If this is a `pressed` event, is it a key-repeat?
///
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
/// Sometimes you will want to ignore such events, and this lets you do that.
///
/// egui will automatically detect such repeat events and mark them as such here.
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
repeat: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
@ -313,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
/// so those key presses are usually not reported to egui.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac).
@ -776,7 +777,7 @@ impl Key {
///
/// Can be used with [`crate::InputState::consume_shortcut`]
/// and [`crate::Context::format_shortcut`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct KeyboardShortcut {
pub modifiers: Modifiers,
pub key: Key,

View file

@ -70,7 +70,7 @@ pub struct PlatformOutput {
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output().copied_text = "some_text".to_string();
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
/// }
/// # });
/// ```

View file

@ -1,6 +1,6 @@
use std::ops::RangeInclusive;
use crate::*;
use crate::{id::IdSet, *};
#[derive(Clone, Copy, Debug)]
pub(crate) struct TooltipFrameState {
@ -12,7 +12,7 @@ pub(crate) struct TooltipFrameState {
#[cfg(feature = "accesskit")]
#[derive(Clone)]
pub(crate) struct AccessKitFrameState {
pub(crate) nodes: IdMap<Box<accesskit::Node>>,
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
pub(crate) parent_stack: Vec<Id>,
}
@ -51,6 +51,12 @@ pub(crate) struct FrameState {
#[cfg(feature = "accesskit")]
pub(crate) accesskit_state: Option<AccessKitFrameState>,
/// Highlight these widgets this next frame. Read from this.
pub(crate) highlight_this_frame: IdSet,
/// Highlight these widgets the next frame. Write to this.
pub(crate) highlight_next_frame: IdSet,
}
impl Default for FrameState {
@ -65,6 +71,8 @@ impl Default for FrameState {
scroll_target: [None, None],
#[cfg(feature = "accesskit")]
accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),
}
}
}
@ -81,6 +89,8 @@ impl FrameState {
scroll_target,
#[cfg(feature = "accesskit")]
accesskit_state,
highlight_this_frame,
highlight_next_frame,
} = self;
used_ids.clear();
@ -90,10 +100,13 @@ impl FrameState {
*tooltip_state = None;
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];
#[cfg(feature = "accesskit")]
{
*accesskit_state = None;
}
*highlight_this_frame = std::mem::take(highlight_next_frame);
}
/// How much space is still available after panels has been added.

View file

@ -8,14 +8,14 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_temp(id)
ctx.data_mut(|d| d.get_temp(id))
}
pub fn store(self, ctx: &Context, id: Id) {
// We don't persist Grids, because
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
// B) if the code changes, the grid _should_ change, and not remember old sizes
ctx.data().insert_temp(id, self);
ctx.data_mut(|d| d.insert_temp(id, self));
}
fn set_min_col_width(&mut self, col: usize, width: f32) {
@ -278,7 +278,7 @@ impl GridLayout {
pub struct Grid {
id_source: Id,
num_columns: Option<usize>,
striped: bool,
striped: Option<bool>,
min_col_width: Option<f32>,
min_row_height: Option<f32>,
max_cell_size: Vec2,
@ -292,7 +292,7 @@ impl Grid {
Self {
id_source: Id::new(id_source),
num_columns: None,
striped: false,
striped: None,
min_col_width: None,
min_row_height: None,
max_cell_size: Vec2::INFINITY,
@ -310,9 +310,9 @@ impl Grid {
/// If `true`, add a subtle background color to every other row.
///
/// This can make a table easier to read.
/// Default: `false`.
/// Default is whatever is in [`crate::Visuals::striped`].
pub fn striped(mut self, striped: bool) -> Self {
self.striped = striped;
self.striped = Some(striped);
self
}
@ -371,6 +371,7 @@ impl Grid {
spacing,
start_row,
} = self;
let striped = striped.unwrap_or(ui.visuals().striped);
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);

View file

@ -26,15 +26,15 @@ pub mod kb_shortcuts {
/// }
/// ```
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_RESET) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
if let Some(native_pixels_per_point) = native_pixels_per_point {
ctx.set_pixels_per_point(native_pixels_per_point);
}
} else {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
zoom_in(ctx);
}
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
zoom_out(ctx);
}
}

View file

@ -168,5 +168,8 @@ impl std::hash::BuildHasher for BuilIdHasher {
}
}
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
pub type IdSet = std::collections::HashSet<Id, BuilIdHasher>;
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;

View file

@ -169,17 +169,12 @@ impl InputState {
Event::Key {
key,
pressed,
modifiers,
repeat,
..
} => {
if *pressed {
// We only retain presses that are novel (i.e. the first Press event, not those generated by key-repeat)
// key repeats are represented by KeyRepeat.
if !keys_down.insert(*key) {
*event = Event::KeyRepeat {
key: *key,
modifiers: *modifiers,
};
}
let first_press = keys_down.insert(*key);
*repeat = !first_press;
} else {
keys_down.remove(key);
}
@ -262,6 +257,8 @@ impl InputState {
}
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
///
/// Includes key-repeat events.
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
let mut count = 0usize;
@ -271,7 +268,8 @@ impl InputState {
Event::Key {
key: ev_key,
modifiers: ev_mods,
pressed: true
pressed: true,
..
} if *ev_key == key && ev_mods.matches(modifiers)
);
@ -284,6 +282,8 @@ impl InputState {
}
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
///
/// Includes key-repeat events.
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
self.count_and_consume_key(modifiers, key) > 0
}
@ -291,28 +291,31 @@ impl InputState {
/// Check if the given shortcut has been pressed.
///
/// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
///
/// Includes key-repeat events.
pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool {
let KeyboardShortcut { modifiers, key } = *shortcut;
self.consume_key(modifiers, key)
}
/// Was the given key pressed this frame?
///
/// Includes key-repeat events.
pub fn key_pressed(&self, desired_key: Key) -> bool {
self.num_presses(desired_key) > 0
}
/// How many times was the given key pressed this frame?
///
/// Includes key-repeat events.
pub fn num_presses(&self, desired_key: Key) -> usize {
self.events
.iter()
.filter(|event| {
matches!(
event,
Event::Key {
key,
pressed: true,
..
} if *key == desired_key
Event::Key { key, pressed: true, .. }
if *key == desired_key
)
})
.count()
@ -365,7 +368,7 @@ impl InputState {
/// # egui::__run_test_ui(|ui| {
/// let mut zoom = 1.0; // no zoom
/// let mut rotation = 0.0; // no rotation
/// let multi_touch = ui.input().multi_touch();
/// let multi_touch = ui.input(|i| i.multi_touch());
/// if let Some(multi_touch) = multi_touch {
/// zoom *= multi_touch.zoom_delta;
/// rotation += multi_touch.rotation_delta;
@ -444,9 +447,10 @@ impl InputState {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Click {
pub pos: Pos2,
pub button: PointerButton,
/// 1 or 2 (double-click) or 3 (triple-click)
pub count: u32,
/// Allows you to check for e.g. shift-click
pub modifiers: Modifiers,
}
@ -468,7 +472,10 @@ pub(crate) enum PointerEvent {
position: Pos2,
button: PointerButton,
},
Released(Option<Click>),
Released {
click: Option<Click>,
button: PointerButton,
},
}
impl PointerEvent {
@ -477,11 +484,11 @@ impl PointerEvent {
}
pub fn is_release(&self) -> bool {
matches!(self, PointerEvent::Released(_))
matches!(self, PointerEvent::Released { .. })
}
pub fn is_click(&self) -> bool {
matches!(self, PointerEvent::Released(Some(_click)))
matches!(self, PointerEvent::Released { click: Some(_), .. })
}
}
@ -636,7 +643,6 @@ impl PointerState {
Some(Click {
pos,
button,
count,
modifiers,
})
@ -644,7 +650,8 @@ impl PointerState {
None
};
self.pointer_events.push(PointerEvent::Released(click));
self.pointer_events
.push(PointerEvent::Released { click, button });
self.press_origin = None;
self.press_start_time = None;
@ -772,11 +779,28 @@ impl PointerState {
self.pointer_events.iter().any(|event| event.is_release())
}
/// Was the button given pressed this frame?
pub fn button_pressed(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b))
}
/// Was the button given released this frame?
pub fn button_released(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(event, &PointerEvent::Released(Some(Click{button: b, ..})) if button == b))
.any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b))
}
/// Was the primary button pressed this frame?
pub fn primary_pressed(&self) -> bool {
self.button_pressed(PointerButton::Primary)
}
/// Was the secondary button pressed this frame?
pub fn secondary_pressed(&self) -> bool {
self.button_pressed(PointerButton::Secondary)
}
/// Was the primary button released this frame?
@ -808,16 +832,28 @@ impl PointerState {
/// Was the button given double clicked this frame?
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
self.pointer_events.iter().any(|event| {
matches!(
&event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_double()
)
})
}
/// Was the button given triple clicked this frame?
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
self.pointer_events.iter().any(|event| {
matches!(
&event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_triple()
)
})
}
/// Was the primary button clicked this frame?
@ -830,18 +866,6 @@ impl PointerState {
self.button_clicked(PointerButton::Secondary)
}
// /// Was this button pressed (`!down -> down`) this frame?
// /// This can sometimes return `true` even if `any_down() == false`
// /// because a press can be shorted than one frame.
// pub fn button_pressed(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_press())
// }
// /// Was this button released (`down -> !down`) this frame?
// pub fn button_released(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_release())
// }
/// Is this button currently down?
#[inline(always)]
pub fn button_down(&self, button: PointerButton) -> bool {

View file

@ -96,10 +96,14 @@ struct GestureState {
struct DynGestureState {
/// used for proportional zooming
avg_distance: f32,
/// used for non-proportional zooming
avg_abs_distance2: Vec2,
avg_pos: Pos2,
avg_force: f32,
heading: f32,
}

View file

@ -2,7 +2,7 @@
use crate::*;
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
let families = ui.fonts().families();
let families = ui.fonts(|f| f.families());
ui.horizontal(|ui| {
for alternative in families {
let text = alternative.to_string();
@ -12,7 +12,7 @@ pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
}
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
let families = ui.fonts().families();
let families = ui.fonts(|f| f.families());
ui.horizontal(|ui| {
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
for alternative in families {

View file

@ -39,6 +39,7 @@ impl Order {
Self::Tooltip,
Self::Debug,
];
pub const TOP: Self = Self::Debug;
#[inline(always)]
pub fn allow_interaction(&self) -> bool {

View file

@ -363,11 +363,11 @@ pub use {
input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order},
layout::*,
memory::Memory,
memory::{Memory, Options},
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,
style::{FontSelection, Style, TextStyle, Visuals},
style::{FontSelection, Margin, Style, TextStyle, Visuals},
text::{Galley, TextFormat},
ui::Ui,
widget_text::{RichText, WidgetText},
@ -513,18 +513,30 @@ pub mod special_emojis {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WidgetType {
Label, // TODO(emilk): emit Label events
/// e.g. a hyperlink
Link,
TextEdit,
Button,
Checkbox,
RadioButton,
SelectableLabel,
ComboBox,
Slider,
DragValue,
ColorButton,
ImageButton,
CollapsingHeader,
/// If you cannot fit any of the above slots.
@ -559,25 +571,3 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}
/// Return a tree update that the egui integration should provide to the
/// AccessKit adapter if it cannot immediately run the egui application
/// to get a full tree update after running [`Context::enable_accesskit`].
#[cfg(feature = "accesskit")]
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
use accesskit::{Node, Role, Tree, TreeUpdate};
use std::sync::Arc;
let root_id = accesskit_root_id().accesskit_id();
TreeUpdate {
nodes: vec![(
root_id,
Arc::new(Node {
role: Role::Window,
..Default::default()
}),
)],
tree: Some(Tree::new(root_id)),
focus: None,
}
}

View file

@ -52,9 +52,10 @@ pub struct Memory {
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut ctx = egui::Context::default();
/// let mut memory = ctx.memory();
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
/// ctx.memory_mut(|mem| {
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// });
/// ```
#[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage,
@ -102,9 +103,15 @@ pub struct Options {
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
/// This does not at all change the behavior of egui,
/// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
///
/// The only change to egui is that labels can be focused by pressing tab.
///
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
///
/// `eframe` supports it only on web, using the `web_screen_reader` feature flag,
/// but you should consider using [AccessKit](https://github.com/AccessKit/accesskit) instead,
/// which `eframe` supports.
pub screen_reader: bool,
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
@ -248,6 +255,7 @@ impl Focus {
key: crate::Key::Escape,
pressed: true,
modifiers: _,
..
}
) {
self.id = None;
@ -259,6 +267,7 @@ impl Focus {
key: crate::Key::Tab,
pressed: true,
modifiers,
..
} = event
{
if !self.is_focus_locked {
@ -403,7 +412,7 @@ impl Memory {
}
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&mut self, id: Id) -> bool {
pub fn has_lock_focus(&self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {
@ -477,6 +486,10 @@ impl Memory {
self.popup == Some(popup_id) || self.everything_is_visible()
}
pub fn any_popup_open(&self) -> bool {
self.popup.is_some() || self.everything_is_visible()
}
pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some(popup_id);
}

View file

@ -31,11 +31,11 @@ pub(crate) struct BarState {
impl BarState {
fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_temp(bar_id, self);
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
/// Show a menu at pointer if primary-clicked response.
@ -68,7 +68,7 @@ fn set_menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
@ -100,6 +100,20 @@ pub fn menu_button<R>(
stationary_menu_impl(ui, title, Box::new(add_contents))
}
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_image_button<R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
}
/// Construct a nested sub menu in another menu.
///
/// Opens on hover.
@ -129,9 +143,10 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.interactable(true)
.drag_bounds(Rect::EVERYTHING);
.drag_bounds(ctx.screen_rect());
let inner_response = area.show(ctx, |ui| {
set_menu_style(ui.style_mut());
@ -166,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.bg_fill);
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
@ -177,6 +192,25 @@ fn stationary_menu_impl<'c, R>(
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Build a top level menu with an image button.
///
/// Responds to primary clicks.
fn stationary_menu_image_impl<'c, R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(ImageButton::new(texture_id, image_size));
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub(crate) fn context_menu(
response: &Response,
@ -277,10 +311,9 @@ impl MenuRoot {
root: &mut MenuRootManager,
id: Id,
) -> MenuResponse {
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
let input = response.ctx.input();
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
if (response.clicked() && root.is_menu_open(id))
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
{
// menu open and button clicked or esc pressed
return MenuResponse::Close;
} else if (response.clicked() && !root.is_menu_open(id))
@ -288,12 +321,10 @@ impl MenuRoot {
{
// menu not open and button clicked
// or button hovered while other menu is open
drop(input);
let mut pos = response.rect.left_bottom();
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let screen_rect = response.ctx.input().screen_rect;
let screen_rect = response.ctx.input(|i| i.screen_rect);
if pos.y + menu_rect.height() > screen_rect.max.y {
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
@ -305,8 +336,11 @@ impl MenuRoot {
}
return MenuResponse::Create(pos, id);
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
if let Some(pos) = input.pointer.interact_pos() {
} else if response
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
{
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
if let Some(root) = root.inner.as_mut() {
if root.id == id {
// pressed somewhere while this menu is open
@ -329,7 +363,8 @@ impl MenuRoot {
id: Id,
) -> MenuResponse {
let response = response.interact(Sense::click());
let pointer = &response.ctx.input().pointer;
response.ctx.input(|input| {
let pointer = &input.pointer;
if pointer.any_pressed() {
if let Some(pos) = pointer.interact_pos() {
let mut destroy = false;
@ -349,6 +384,7 @@ impl MenuRoot {
}
}
MenuResponse::Stay
})
}
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
@ -410,7 +446,7 @@ impl SubMenuButton {
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) {
&ui.style().visuals.widgets.hovered
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
@ -439,7 +475,8 @@ impl SubMenuButton {
text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let desired_size = text_and_icon_size + 2.0 * button_padding;
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
@ -459,7 +496,7 @@ impl SubMenuButton {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.rounding,
visuals.bg_fill,
visuals.weak_bg_fill,
);
}
@ -571,15 +608,15 @@ impl MenuState {
/// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
let pointer = &ui.input().pointer.clone();
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(pointer) {
if self.moving_towards_current_submenu(&pointer) {
// ensure to repaint once even when pointer is not moving
ui.ctx().request_repaint();
} else if !open && button.hovered() {
let pos = button.rect.right_top();
self.open_submenu(sub_id, pos);
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
self.close_submenu();
}
}

View file

@ -7,7 +7,6 @@ use crate::{
Color32, Context, FontId,
};
use epaint::{
mutex::{RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke,
};
@ -105,10 +104,12 @@ impl Painter {
&self.ctx
}
/// Available fonts.
/// Read-only access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
#[inline(always)]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx.fonts()
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx.fonts(reader)
}
/// Where we paint
@ -152,8 +153,9 @@ impl Painter {
/// ## Low level
impl Painter {
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
#[inline]
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
}
fn transform_shape(&self, shape: &mut Shape) {
@ -167,11 +169,11 @@ impl Painter {
/// NOTE: all coordinates are screen coordinates!
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
self.paint_list().add(self.clip_rect, Shape::Noop)
self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
} else {
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list().add(self.clip_rect, shape)
self.paint_list(|l| l.add(self.clip_rect, shape))
}
}
@ -187,9 +189,9 @@ impl Painter {
self.transform_shape(&mut shape);
shape
});
self.paint_list().extend(self.clip_rect, shapes);
self.paint_list(|l| l.extend(self.clip_rect, shapes));
} else {
self.paint_list().extend(self.clip_rect, shapes);
self.paint_list(|l| l.extend(self.clip_rect, shapes));
};
}
@ -200,7 +202,7 @@ impl Painter {
}
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list().set(idx, self.clip_rect, shape);
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
}
}
@ -405,7 +407,7 @@ impl Painter {
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, wrap_width)
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
}
/// Will line break at `\n`.
@ -418,7 +420,7 @@ impl Painter {
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, f32::INFINITY)
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
}
/// Paint text that has already been layed out in a [`Galley`].

View file

@ -13,6 +13,7 @@ use crate::{
///
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
// TODO(emilk): we should be using bit sets instead of so many bools
#[derive(Clone)]
pub struct Response {
// CONTEXT:
@ -42,6 +43,10 @@ pub struct Response {
#[doc(hidden)]
pub hovered: bool,
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
pub highlighted: bool,
/// The pointer clicked this thing this frame.
#[doc(hidden)]
pub clicked: [bool; NUM_POINTER_BUTTONS],
@ -52,7 +57,7 @@ pub struct Response {
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
/// The thing was triple-clicked.
pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
/// The widgets is being dragged
#[doc(hidden)]
@ -90,6 +95,7 @@ impl std::fmt::Debug for Response {
sense,
enabled,
hovered,
highlighted,
clicked,
double_clicked,
triple_clicked,
@ -106,6 +112,7 @@ impl std::fmt::Debug for Response {
.field("sense", sense)
.field("enabled", enabled)
.field("hovered", hovered)
.field("highlighted", highlighted)
.field("clicked", clicked)
.field("double_clicked", double_clicked)
.field("triple_clicked", triple_clicked)
@ -173,7 +180,8 @@ impl Response {
// We do not use self.clicked(), because we want to catch all clicks within our frame,
// even if we aren't clickable (or even enabled).
// This is important for windows and such that should close then the user clicks elsewhere.
let pointer = &self.ctx.input().pointer;
self.ctx.input(|i| {
let pointer = &i.pointer;
if pointer.any_click() {
// We detect clicks/hover on a "interact_rect" that is slightly larger than
@ -190,6 +198,7 @@ impl Response {
} else {
false
}
})
}
/// Was the widget enabled?
@ -211,20 +220,24 @@ impl Response {
self.hovered
}
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
pub fn highlighted(&self) -> bool {
self.highlighted
}
/// This widget has the keyboard focus (i.e. is receiving key presses).
///
/// This function only returns true if the UI as a whole (e.g. window)
/// also has the keyboard focus. That makes this function suitable
/// for style choices, e.g. a thicker border around focused widgets.
pub fn has_focus(&self) -> bool {
// Access input and memory in separate statements to prevent deadlock.
let has_global_focus = self.ctx.input().raw.has_focus;
has_global_focus && self.ctx.memory().has_focus(self.id)
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
}
/// True if this widget has keyboard focus this frame, but didn't last frame.
pub fn gained_focus(&self) -> bool {
self.ctx.memory().gained_focus(self.id)
self.ctx.memory(|mem| mem.gained_focus(self.id))
}
/// The widget had keyboard focus and lost it,
@ -236,29 +249,29 @@ impl Response {
/// # let mut my_text = String::new();
/// # fn do_request(_: &str) {}
/// let response = ui.text_edit_singleline(&mut my_text);
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
/// do_request(&my_text);
/// }
/// # });
/// ```
pub fn lost_focus(&self) -> bool {
self.ctx.memory().lost_focus(self.id)
self.ctx.memory(|mem| mem.lost_focus(self.id))
}
/// Request that this widget get keyboard focus.
pub fn request_focus(&self) {
self.ctx.memory().request_focus(self.id);
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
}
/// Surrender keyboard focus for this widget.
pub fn surrender_focus(&self) {
self.ctx.memory().surrender_focus(self.id);
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
}
/// The widgets is being dragged.
///
/// To find out which button(s), query [`crate::PointerState::button_down`]
/// (`ui.input().pointer.button_down(…)`).
/// (`ui.input(|i| i.pointer.button_down(…))`).
///
/// Note that the widget must be sensing drags with [`Sense::drag`].
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
@ -270,12 +283,17 @@ impl Response {
}
pub fn dragged_by(&self, button: PointerButton) -> bool {
self.dragged() && self.ctx.input().pointer.button_down(button)
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
}
/// Did a drag on this widgets begin this frame?
pub fn drag_started(&self) -> bool {
self.dragged && self.ctx.input().pointer.any_pressed()
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
}
/// Did a drag on this widgets by the button begin this frame?
pub fn drag_started_by(&self, button: PointerButton) -> bool {
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
}
/// The widget was being dragged, but now it has been released.
@ -283,10 +301,15 @@ impl Response {
self.drag_released
}
/// The widget was being dragged by the button, but now it has been released.
pub fn drag_released_by(&self, button: PointerButton) -> bool {
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// If dragged, how many points were we dragged and in what direction?
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
self.ctx.input().pointer.delta()
self.ctx.input(|i| i.pointer.delta())
} else {
Vec2::ZERO
}
@ -302,7 +325,7 @@ impl Response {
/// None if the pointer is outside the response area.
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
self.ctx.input().pointer.hover_pos()
self.ctx.input(|i| i.pointer.hover_pos())
} else {
None
}
@ -392,11 +415,11 @@ impl Response {
}
fn should_show_hover_ui(&self) -> bool {
if self.ctx.memory().everything_is_visible() {
if self.ctx.memory(|mem| mem.everything_is_visible()) {
return true;
}
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false;
}
@ -404,8 +427,7 @@ impl Response {
// We only show the tooltip when the mouse pointer is still,
// but once shown we keep showing it until the mouse leaves the parent.
let is_pointer_still = self.ctx.input().pointer.is_still();
if !is_pointer_still && !self.is_tooltip_open() {
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
// wait for mouse to stop
self.ctx.request_repaint();
return false;
@ -414,8 +436,9 @@ impl Response {
// We don't want tooltips of things while we are dragging them,
// but we do want tooltips while holding down on an item on a touch screen.
if self.ctx.input().pointer.any_down()
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
if self
.ctx
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
{
return false;
}
@ -444,6 +467,17 @@ impl Response {
})
}
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
///
/// The highlight takes on frame to take effect if you call this after the widget has been fully rendered.
///
/// See also [`Context::highlight_widget`].
pub fn highlight(mut self) -> Self {
self.ctx.highlight_widget(self.id);
self.highlighted = true;
self
}
/// Show this text when hovering if the widget is disabled.
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_disabled_hover_ui(|ui| {
@ -454,7 +488,15 @@ impl Response {
/// When hovered, use this icon for the mouse cursor.
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() {
self.ctx.output().cursor_icon = cursor;
self.ctx.set_cursor_icon(cursor);
}
self
}
/// When hovered or dragged, use this icon for the mouse cursor.
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() || self.dragged() {
self.ctx.set_cursor_icon(cursor);
}
self
}
@ -503,8 +545,10 @@ impl Response {
/// # });
/// ```
pub fn scroll_to_me(&self, align: Option<Align>) {
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
self.ctx.frame_state_mut(|state| {
state.scroll_target[0] = Some((self.rect.x_range(), align));
state.scroll_target[1] = Some((self.rect.y_range(), align));
});
}
/// For accessibility.
@ -529,47 +573,47 @@ impl Response {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(&mut node, make_info());
}
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, make_info());
});
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(&mut node, event.widget_info().clone());
}
self.ctx.output().events.push(event);
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
});
self.ctx.output_mut(|o| o.events.push(event));
}
#[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
node.bounds = Some(accesskit::kurbo::Rect {
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
builder.set_bounds(accesskit::Rect {
x0: self.rect.min.x.into(),
y0: self.rect.min.y.into(),
x1: self.rect.max.x.into(),
y1: self.rect.max.y.into(),
});
if self.sense.focusable {
node.focusable = true;
builder.add_action(accesskit::Action::Focus);
}
if self.sense.click && node.default_action_verb.is_none() {
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
if self.sense.click && builder.default_action_verb().is_none() {
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
}
}
#[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info(
&self,
node: &mut accesskit::Node,
builder: &mut accesskit::NodeBuilder,
info: crate::WidgetInfo,
) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
self.fill_accesskit_node_common(node);
node.role = match info.typ {
self.fill_accesskit_node_common(builder);
builder.set_role(match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField,
@ -584,18 +628,18 @@ impl Response {
WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown,
};
});
if let Some(label) = info.label {
node.name = Some(label.into());
builder.set_name(label);
}
if let Some(value) = info.current_text_value {
node.value = Some(value.into());
builder.set_value(value);
}
if let Some(value) = info.value {
node.numeric_value = Some(value);
builder.set_numeric_value(value);
}
if let Some(selected) = info.selected {
node.checked_state = Some(if selected {
builder.set_checked_state(if selected {
CheckedState::True
} else {
CheckedState::False
@ -618,9 +662,9 @@ impl Response {
/// ```
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
node.labelled_by.push(id.accesskit_id());
}
self.ctx.accesskit_node_builder(self.id, |builder| {
builder.push_labelled_by(id.accesskit_id());
});
#[cfg(not(feature = "accesskit"))]
{
let _ = id;
@ -669,6 +713,7 @@ impl Response {
sense: self.sense.union(other.sense),
enabled: self.enabled || other.enabled,
hovered: self.hovered || other.hovered,
highlighted: self.highlighted || other.highlighted,
clicked: [
self.clicked[0] || other.clicked[0],
self.clicked[1] || other.clicked[1],
@ -700,6 +745,13 @@ impl Response {
}
}
impl Response {
/// Returns a response with a modified [`Self::rect`].
pub fn with_new_rect(self, rect: Rect) -> Self {
Self { rect, ..self }
}
}
/// To summarize the response from many widgets you can use this pattern:
///
/// ```

View file

@ -174,6 +174,9 @@ pub struct Style {
/// ```
pub text_styles: BTreeMap<TextStyle, FontId>,
/// The style to use for [`DragValue`] text.
pub drag_value_text_style: TextStyle,
/// If set, labels buttons wtc will use this to determine whether or not
/// to wrap the text at the right edge of the [`Ui`] they are in.
/// By default this is `None`.
@ -216,6 +219,7 @@ impl Style {
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
let mut visuals = *self.visuals.widgets.style(response);
if selected {
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
visuals.bg_fill = self.visuals.selection.bg_fill;
// visuals.bg_stroke = self.visuals.selection.stroke;
visuals.fg_stroke = self.visuals.selection.stroke;
@ -264,8 +268,11 @@ pub struct Spacing {
/// Anything clickable should be (at least) this size.
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
pub slider_width: f32, // TODO(emilk): rename big_interact_size ?
/// Default width of a [`Slider`].
pub slider_width: f32,
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
pub combo_width: f32,
/// Default width of a [`TextEdit`].
pub text_edit_width: f32,
@ -293,8 +300,12 @@ pub struct Spacing {
pub scroll_bar_width: f32,
/// Make sure the scroll handle is at least this big
pub scroll_handle_min_length: f32,
/// Margin between contents and scroll bar.
pub scroll_bar_inner_margin: f32,
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
pub scroll_bar_outer_margin: f32,
}
@ -471,6 +482,8 @@ pub struct Visuals {
pub window_fill: Color32,
pub window_stroke: Stroke,
pub menu_rounding: Rounding,
/// Panel background color
pub panel_fill: Color32,
@ -479,6 +492,7 @@ pub struct Visuals {
pub resize_corner_size: f32,
pub text_cursor_width: f32,
/// show where the text cursor would be if you clicked
pub text_cursor_preview: bool,
@ -490,6 +504,18 @@ pub struct Visuals {
/// Show a background behind collapsing headers.
pub collapsing_header_frame: bool,
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
pub indent_has_left_vline: bool,
/// Wether or not Grids and Tables should be striped by default
/// (have alternating rows differently colored).
pub striped: bool,
/// Show trailing color behind the circle of a [`Slider`]. Default is OFF.
///
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
pub slider_trailing_fill: bool,
}
impl Visuals {
@ -527,7 +553,7 @@ impl Visuals {
// TODO(emilk): replace with an alpha
#[inline(always)]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.bg_fill
self.widgets.noninteractive.weak_bg_fill
}
/// Returned a "grayed out" version of the given color.
@ -560,7 +586,9 @@ pub struct Widgets {
/// The style of an interactive widget, such as a button, at rest.
pub inactive: WidgetVisuals,
/// The style of an interactive widget while you hover it.
/// The style of an interactive widget while you hover it, or when it is highlighted.
///
/// See [`Response::hovered`], [`Response::highlighted`] and [`Response::highlight`].
pub hovered: WidgetVisuals,
/// The style of an interactive widget as you are clicking or dragging it.
@ -576,7 +604,7 @@ impl Widgets {
&self.noninteractive
} else if response.is_pointer_button_down_on() || response.has_focus() {
&self.active
} else if response.hovered() {
} else if response.hovered() || response.highlighted() {
&self.hovered
} else {
&self.inactive
@ -588,9 +616,17 @@ impl Widgets {
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetVisuals {
/// Background color of widget.
/// Background color of widgets that must have a background fill,
/// such as the slider background, a checkbox background, or a radio button background.
///
/// Must never be [`Color32::TRANSPARENT`].
pub bg_fill: Color32,
/// Background color of widgets that can _optionally_ have a background fill, such as buttons.
///
/// May be [`Color32::TRANSPARENT`].
pub weak_bg_fill: Color32,
/// For surrounding rectangle of things that need it,
/// like buttons, the box of the checkbox, etc.
/// Should maybe be called `frame_stroke`.
@ -657,6 +693,7 @@ impl Default for Style {
override_font_id: None,
override_text_style: None,
text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
wrap: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
@ -678,6 +715,7 @@ impl Default for Spacing {
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
interact_size: vec2(40.0, 18.0),
slider_width: 100.0,
combo_width: 100.0,
text_edit_width: 280.0,
icon_width: 14.0,
icon_width_inner: 8.0,
@ -685,6 +723,7 @@ impl Default for Spacing {
tooltip_width: 600.0,
combo_height: 200.0,
scroll_bar_width: 8.0,
scroll_handle_min_length: 12.0,
scroll_bar_inner_margin: 4.0,
scroll_bar_outer_margin: 0.0,
indent_ends_with_horizontal_line: false,
@ -711,7 +750,7 @@ impl Visuals {
widgets: Widgets::default(),
selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
faint_bg_color: Color32::from_gray(35),
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
code_bg_color: Color32::from_gray(64),
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
@ -722,6 +761,8 @@ impl Visuals {
window_fill: Color32::from_gray(27),
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
menu_rounding: Rounding::same(6.0),
panel_fill: Color32::from_gray(27),
popup_shadow: Shadow::small_dark(),
@ -731,6 +772,11 @@ impl Visuals {
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true,
collapsing_header_frame: false,
indent_has_left_vline: true,
striped: false,
slider_trailing_fill: false,
}
}
@ -741,7 +787,7 @@ impl Visuals {
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
faint_bg_color: Color32::from_gray(242),
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
code_bg_color: Color32::from_gray(230),
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
@ -791,6 +837,7 @@ impl Widgets {
pub fn dark() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
@ -798,13 +845,15 @@ impl Widgets {
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(60), // button background
weak_bg_fill: Color32::from_gray(60), // button background
bg_fill: Color32::from_gray(60), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(70),
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
@ -812,6 +861,7 @@ impl Widgets {
expansion: 1.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(55),
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
@ -819,6 +869,7 @@ impl Widgets {
expansion: 1.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
@ -831,6 +882,7 @@ impl Widgets {
pub fn light() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(248),
bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
@ -838,13 +890,15 @@ impl Widgets {
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(230), // button background
weak_bg_fill: Color32::from_gray(230), // button background
bg_fill: Color32::from_gray(230), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::BLACK),
@ -852,6 +906,7 @@ impl Widgets {
expansion: 1.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(165),
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
@ -859,6 +914,7 @@ impl Widgets {
expansion: 1.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
@ -885,6 +941,7 @@ impl Style {
override_font_id,
override_text_style,
text_styles,
drag_value_text_style,
wrap: _,
spacing,
interaction,
@ -926,6 +983,19 @@ impl Style {
});
ui.end_row();
ui.label("Text style of DragValue:");
crate::ComboBox::from_id_source("drag_value_text_style")
.selected_text(drag_value_text_style.to_string())
.show_ui(ui, |ui| {
let all_text_styles = ui.style().text_styles();
for style in all_text_styles {
let text =
crate::RichText::new(style.to_string()).text_style(style.clone());
ui.selectable_value(drag_value_text_style, style, text);
}
});
ui.end_row();
ui.label("Animation duration:");
ui.add(
Slider::new(animation_time, 0.0..=1.0)
@ -974,6 +1044,7 @@ impl Spacing {
indent,
interact_size,
slider_width,
combo_width,
text_edit_width,
icon_width,
icon_width_inner,
@ -982,6 +1053,7 @@ impl Spacing {
indent_ends_with_horizontal_line,
combo_height,
scroll_bar_width,
scroll_handle_min_length,
scroll_bar_inner_margin,
scroll_bar_outer_margin,
} = self;
@ -1002,6 +1074,10 @@ impl Spacing {
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
ui.label("Slider width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
ui.label("ComboBox width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
ui.label("TextEdit width");
@ -1010,6 +1086,10 @@ impl Spacing {
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
ui.label("Scroll-bar width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0));
ui.label("Scroll-bar handle min length");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
ui.label("Scroll-bar inner margin");
@ -1175,13 +1255,17 @@ impl Selection {
impl WidgetVisuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
bg_fill,
weak_bg_fill,
bg_fill: mandatory_bg_fill,
bg_stroke,
rounding,
fg_stroke,
expansion,
} = self;
ui_color(ui, bg_fill, "background fill");
ui_color(ui, weak_bg_fill, "optional background fill")
.on_hover_text("For buttons, combo-boxes, etc");
ui_color(ui, mandatory_bg_fill, "mandatory background fill")
.on_hover_text("For checkboxes, sliders, etc");
stroke_ui(ui, bg_stroke, "background stroke");
rounding_ui(ui, rounding);
@ -1243,6 +1327,8 @@ impl Visuals {
window_fill,
window_stroke,
menu_rounding,
panel_fill,
popup_shadow,
@ -1253,10 +1339,15 @@ impl Visuals {
clip_rect_margin,
button_frame,
collapsing_header_frame,
indent_has_left_vline,
striped,
slider_trailing_fill,
} = self;
ui.collapsing("Background Colors", |ui| {
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
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(
@ -1267,14 +1358,15 @@ impl Visuals {
});
ui.collapsing("Window", |ui| {
// Common shortcuts
ui_color(ui, window_fill, "Fill");
stroke_ui(ui, window_stroke, "Outline");
rounding_ui(ui, window_rounding);
shadow_ui(ui, window_shadow, "Shadow");
shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)");
});
ui.collapsing("Menus and popups", |ui| {
rounding_ui(ui, menu_rounding);
shadow_ui(ui, popup_shadow, "Shadow");
});
ui.collapsing("Widgets", |ui| widgets.ui(ui));
@ -1307,6 +1399,14 @@ impl Visuals {
ui.checkbox(button_frame, "Button has a frame");
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
ui.checkbox(
indent_has_left_vline,
"Paint a vertical line to the left of indented regions",
);
ui.checkbox(striped, "By default, add stripes to grids and tables?");
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
ui.vertical_centered(|ui| reset_button(ui, self));
}

View file

@ -3,11 +3,11 @@
use std::hash::Hash;
use std::sync::Arc;
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use epaint::mutex::RwLock;
use crate::{
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
widgets::*, *,
util::IdTypeMap, widgets::*, *,
};
// ----------------------------------------------------------------------------
@ -314,84 +314,9 @@ impl Ui {
self.painter().layer_id()
}
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
/// // This is fine!
/// }
///
/// let pos = ui.input().pointer.hover_pos();
/// if let Some(pos) = pos {
/// // This is also fine!
/// }
///
/// if let Some(pos) = ui.input().pointer.hover_pos() {
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
/// }
/// # });
/// ```
#[inline]
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
self.ctx().input()
}
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input_mut()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings
/// like for [`Self::input()`].
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
/// # });
/// ```
#[inline]
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
self.ctx().input_mut()
}
/// The [`Memory`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().memory()`.
#[inline]
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
self.ctx().memory()
}
/// Stores superficial widget state.
#[inline]
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
self.ctx().data()
}
/// The [`PlatformOutput`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().output()`.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output().copied_text = "some_text".to_string();
/// }
/// # });
#[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
self.ctx().output()
}
/// The [`Fonts`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().fonts()`.
#[inline]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx().fonts()
}
/// The height of text of this text style
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts().row_height(&style.resolve(self.style()))
self.fonts(|f| f.row_height(&style.resolve(self.style())))
}
/// Screen-space rectangle for clipping what we paint in this ui.
@ -413,6 +338,87 @@ impl Ui {
}
}
/// # Helpers for accessing the underlying [`Context`].
/// These functions all lock the [`Context`] owned by this [`Ui`].
/// Please see the documentation of [`Context`] for how locking works!
impl Ui {
/// Read-only access to the shared [`InputState`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.input(|i| i.key_pressed(egui::Key::A)) {
/// // …
/// }
/// # });
/// ```
#[inline]
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
self.ctx().input(reader)
}
/// Read-write access to the shared [`InputState`].
#[inline]
pub fn input_mut<R>(&self, writer: impl FnOnce(&mut InputState) -> R) -> R {
self.ctx().input_mut(writer)
}
/// Read-only access to the shared [`Memory`].
#[inline]
pub fn memory<R>(&self, reader: impl FnOnce(&Memory) -> R) -> R {
self.ctx().memory(reader)
}
/// Read-write access to the shared [`Memory`].
#[inline]
pub fn memory_mut<R>(&self, writer: impl FnOnce(&mut Memory) -> R) -> R {
self.ctx().memory_mut(writer)
}
/// Read-only access to the shared [`IdTypeMap`], which stores superficial widget state.
#[inline]
pub fn data<R>(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R {
self.ctx().data(reader)
}
/// Read-write access to the shared [`IdTypeMap`], which stores superficial widget state.
#[inline]
pub fn data_mut<R>(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R {
self.ctx().data_mut(writer)
}
/// Read-only access to the shared [`PlatformOutput`].
///
/// This is what egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
/// ```
#[inline]
pub fn output<R>(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R {
self.ctx().output(reader)
}
/// Read-write access to the shared [`PlatformOutput`].
///
/// This is what egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
/// ```
#[inline]
pub fn output_mut<R>(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R {
self.ctx().output_mut(writer)
}
/// Read-only access to [`Fonts`].
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx().fonts(reader)
}
}
// ------------------------------------------------------------------------
/// # Sizes etc
@ -961,7 +967,8 @@ impl Ui {
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 {
let range = rect.min[d]..=rect.max[d];
self.ctx().frame_state().scroll_target[d] = Some((range, align));
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
}
}
@ -990,7 +997,8 @@ impl Ui {
let target = self.next_widget_position();
for d in 0..2 {
let target = target[d];
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
}
}
@ -1022,7 +1030,8 @@ impl Ui {
/// # });
/// ```
pub fn scroll_with_delta(&self, delta: Vec2) {
self.ctx().frame_state().scroll_delta += delta;
self.ctx()
.frame_state_mut(|state| state.scroll_delta += delta);
}
}
@ -1563,7 +1572,7 @@ impl Ui {
/// }
/// ```
///
/// Se also [`crate::Image`] and [`crate::ImageButton`].
/// See also [`crate::Image`] and [`crate::ImageButton`].
#[inline]
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self)
@ -1780,25 +1789,32 @@ impl Ui {
};
let ret = add_contents(&mut child_ui);
let left_vline = self.visuals().indent_has_left_vline;
let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
if left_vline || end_with_horizontal_line {
if end_with_horizontal_line {
child_ui.add_space(4.0);
}
// draw a faint line on the left to mark the indented section
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
let left_top = self.painter().round_pos_to_pixels(left_top);
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
if left_vline {
// draw a faint line on the left to mark the indented section
self.painter.line_segment([left_top, left_bottom], stroke);
}
if end_with_horizontal_line {
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
self.painter
.line_segment([left_bottom, right_bottom], stroke);
}
}
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
InnerResponse::new(ret, response)
@ -2157,6 +2173,43 @@ impl Ui {
menu::menu_button(self, title, add_contents)
}
}
/// Create a menu button with an image that when clicked will show the given menu.
///
/// If called from within a menu this will instead create a button for a sub-menu.
///
/// ```ignore
/// use egui_extras;
///
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
/// "rss",
/// include_bytes!("rss.svg"),
/// egui_extras::image::FitTo::Size(24, 24),
/// );
///
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
/// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() {
/// ui.close_menu();
/// }
/// });
/// });
/// ```
///
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
#[inline]
pub fn menu_image_button<R>(
&mut self,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() {
menu::submenu_button(self, menu_state, String::new(), add_contents)
} else {
menu::menu_image_button(self, texture_id, image_size, add_contents)
}
}
}
// ----------------------------------------------------------------------------

View file

@ -460,18 +460,18 @@ impl IdTypeMap {
}
#[inline]
pub fn is_empty(&mut self) -> bool {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn len(&mut self) -> usize {
pub fn len(&self) -> usize {
self.0.len()
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&mut self) -> usize {
pub fn count_serialized(&self) -> usize {
self.0
.values()
.filter(|e| matches!(e, Element::Serialized { .. }))
@ -479,7 +479,7 @@ impl IdTypeMap {
}
/// Count the number of values are stored with the given type.
pub fn count<T: 'static>(&mut self) -> usize {
pub fn count<T: 'static>(&self) -> usize {
let key = TypeId::of::<T>();
self.0
.iter()

View file

@ -573,14 +573,14 @@ impl WidgetText {
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(text_job.job),
galley: ui.fonts(|f| f.layout_job(text_job.job)),
galley_has_color: text_job.job_has_color,
}
}
Self::LayoutJob(mut job) => {
job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(job),
galley: ui.fonts(|f| f.layout_job(job)),
galley_has_color: true,
}
}

View file

@ -30,6 +30,7 @@ pub struct Button {
small: bool,
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>,
}
@ -45,6 +46,7 @@ impl Button {
small: false,
frame: None,
min_size: Vec2::ZERO,
rounding: None,
image: None,
}
}
@ -117,6 +119,12 @@ impl Button {
self
}
/// Set the rounding of the button.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = Some(rounding.into());
self
}
/// Show some text on the right side of the button, in weak color.
///
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
@ -140,6 +148,7 @@ impl Widget for Button {
small,
frame,
min_size,
rounding,
image,
} = self;
@ -171,10 +180,10 @@ impl Widget for Button {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
desired_size.y = desired_size.y.max(shortcut_text.size().y);
}
desired_size += 2.0 * button_padding;
if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
}
desired_size += 2.0 * button_padding;
desired_size = desired_size.at_least(min_size);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
@ -184,14 +193,11 @@ impl Widget for Button {
let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.bg_fill);
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
ui.painter().rect(
rect.expand(visuals.expansion),
visuals.rounding,
fill,
stroke,
);
let rounding = rounding.unwrap_or(visuals.rounding);
ui.painter()
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if let Some(image) = image {
@ -221,7 +227,10 @@ impl Widget for Button {
if let Some(image) = image {
let image_rect = Rect::from_min_size(
pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)),
pos2(
rect.min.x + button_padding.x,
rect.center().y - 0.5 - (image.size().y / 2.0),
),
image.size(),
);
image.paint_at(ui, image_rect);
@ -260,6 +269,10 @@ impl<'a> Checkbox<'a> {
text: text.into(),
}
}
pub fn without_text(checked: &'a mut bool) -> Self {
Self::new(checked, WidgetText::default())
}
}
impl<'a> Widget for Checkbox<'a> {
@ -531,7 +544,7 @@ impl Widget for ImageButton {
(
expansion,
visuals.rounding,
visuals.bg_fill,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {

View file

@ -228,9 +228,9 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
if ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque {
ui.output().copied_text = format!("{}, {}, {}", r, g, b);
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
} else {
ui.output().copied_text = format!("{}, {}, {}, {}", r, g, b, a);
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
}
}
@ -341,20 +341,20 @@ 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().is_popup_open(popup_id);
let open = ui.memory(|mem| mem.is_popup_open(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().toggle_popup(popup_id);
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
const COLOR_SLIDER_WIDTH: f32 = 210.0;
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
if ui.memory().is_popup_open(popup_id) {
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let area_response = Area::new(popup_id)
.order(Order::Foreground)
.fixed_pos(button_response.rect.max)
@ -370,9 +370,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
.response;
if !button_response.clicked()
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
ui.memory().close_popup();
ui.memory_mut(|mem| mem.close_popup());
}
}
@ -436,5 +436,5 @@ fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
f(ctx.data().get_temp_mut_or_default(Id::null()))
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
}

View file

@ -368,30 +368,35 @@ impl<'a> Widget for DragValue<'a> {
custom_parser,
} = self;
let shift = ui.input().modifiers.shift_only();
let shift = ui.input(|i| i.modifiers.shift_only());
// The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id();
let is_slow_speed = shift && ui.memory().is_being_dragged(id);
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
// The following call ensures that when a `DragValue` receives focus,
// The following ensures that when a `DragValue` receives focus,
// it is immediately rendered in edit mode, rather than being rendered
// in button mode for just one frame. This is important for
// screen readers.
ui.memory().interested_in_focus(id);
let is_kb_editing = ui.memory().has_focus(id);
let is_kb_editing = ui.memory_mut(|mem| {
mem.interested_in_focus(id);
let is_kb_editing = mem.has_focus(id);
if mem.gained_focus(id) {
mem.drag_value.edit_string = None;
}
is_kb_editing
});
let old_value = get(&mut get_set_value);
let mut value = old_value;
let aim_rad = ui.input().aim_radius() as f64;
let aim_rad = ui.input(|i| i.aim_radius() as f64);
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
let auto_decimals = auto_decimals + is_slow_speed as usize;
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
let change = {
let change = ui.input_mut(|input| {
let mut change = 0.0;
let mut input = ui.input_mut();
if is_kb_editing {
// This deliberately doesn't listen for left and right arrow keys,
@ -415,16 +420,18 @@ impl<'a> Widget for DragValue<'a> {
}
change
};
});
#[cfg(feature = "accesskit")]
{
use accesskit::{Action, ActionData};
for request in ui.input().accesskit_action_requests(id, Action::SetValue) {
ui.input(|input| {
for request in input.accesskit_action_requests(id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
value = new_value;
}
}
});
}
if change != 0.0 {
@ -435,7 +442,7 @@ impl<'a> Widget for DragValue<'a> {
value = clamp_to_range(value, clamp_range.clone());
if old_value != value {
set(&mut get_set_value, value);
ui.memory().drag_value.edit_string = None;
ui.memory_mut(|mem| mem.drag_value.edit_string = None);
}
let value_text = match custom_formatter {
@ -449,22 +456,28 @@ impl<'a> Widget for DragValue<'a> {
}
};
let text_style = ui.style().drag_value_text_style.clone();
// some clones below are redundant if AccessKit is disabled
#[allow(clippy::redundant_clone)]
let mut response = if is_kb_editing {
let button_width = ui.spacing().interact_size.x;
let mut value_text = ui
.memory()
.drag_value
.edit_string
.take()
.memory_mut(|mem| mem.drag_value.edit_string.take())
.unwrap_or_else(|| value_text.clone());
let response = ui.add(
TextEdit::singleline(&mut value_text)
.clip_text(false)
.horizontal_align(ui.layout().horizontal_align())
.vertical_align(ui.layout().vertical_align())
.margin(ui.spacing().button_padding)
.min_size(ui.spacing().interact_size)
.id(id)
.desired_width(button_width)
.font(TextStyle::Monospace),
.desired_width(ui.spacing().interact_size.x)
.font(text_style),
);
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
// See https://github.com/emilk/egui/issues/2687
if response.lost_focus() {
let parsed_value = match custom_parser {
Some(parser) => parser(&value_text),
None => value_text.parse().ok(),
@ -473,12 +486,13 @@ impl<'a> Widget for DragValue<'a> {
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
set(&mut get_set_value, parsed_value);
}
ui.memory().drag_value.edit_string = Some(value_text);
}
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text));
response
} else {
ui.memory().drag_value.edit_string = None;
let button = Button::new(
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)).monospace(),
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
.text_style(text_style),
)
.wrap(false)
.sense(Sense::click_and_drag())
@ -497,9 +511,18 @@ impl<'a> Widget for DragValue<'a> {
}
if response.clicked() {
ui.memory().request_focus(id);
ui.memory_mut(|mem| {
mem.drag_value.edit_string = None;
mem.request_focus(id);
});
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
state.set_ccursor_range(Some(text::CCursorRange::two(
epaint::text::cursor::CCursor::default(),
epaint::text::cursor::CCursor::new(value_text.chars().count()),
)));
state.store(ui.ctx(), response.id);
} else if response.dragged() {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
let mdelta = response.drag_delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
@ -509,7 +532,7 @@ impl<'a> Widget for DragValue<'a> {
let delta_value = delta_points as f64 * speed;
if delta_value != 0.0 {
let mut drag_state = std::mem::take(&mut ui.memory().drag_value);
let mut drag_state = ui.memory_mut(|mem| std::mem::take(&mut mem.drag_value));
// Since we round the value being dragged, we need to store the full precision value in memory:
let stored_value = (drag_state.last_dragged_id == Some(response.id))
@ -530,7 +553,7 @@ impl<'a> Widget for DragValue<'a> {
drag_state.last_dragged_id = Some(response.id);
drag_state.last_dragged_value = Some(stored_value);
ui.memory().drag_value = drag_state;
ui.memory_mut(|mem| mem.drag_value = drag_state);
}
}
@ -542,28 +565,28 @@ impl<'a> Widget for DragValue<'a> {
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
ui.ctx().accesskit_node_builder(response.id, |builder| {
use accesskit::Action;
// If either end of the range is unbounded, it's better
// to leave the corresponding AccessKit field set to None,
// to allow for platform-specific default behavior.
if clamp_range.start().is_finite() {
node.min_numeric_value = Some(*clamp_range.start());
builder.set_min_numeric_value(*clamp_range.start());
}
if clamp_range.end().is_finite() {
node.max_numeric_value = Some(*clamp_range.end());
builder.set_max_numeric_value(*clamp_range.end());
}
node.numeric_value_step = Some(speed);
node.actions |= Action::SetValue;
builder.set_numeric_value_step(speed);
builder.add_action(Action::SetValue);
if value < *clamp_range.end() {
node.actions |= Action::Increment;
builder.add_action(Action::Increment);
}
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
builder.add_action(Action::Decrement);
}
// The name field is set to the current value by the button,
// but we don't want it set that way on this widget type.
node.name = None;
builder.clear_name();
// Always expose the value as a string. This makes the widget
// more stable to accessibility users as it switches
// between edit and button modes. This is particularly important
@ -584,9 +607,9 @@ impl<'a> Widget for DragValue<'a> {
// when in edit mode.
if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
node.value = Some(value_text.into());
}
builder.set_value(value_text);
}
});
response
}

View file

@ -38,7 +38,7 @@ impl Widget for Link {
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
}
if ui.is_rect_visible(response.rect) {
@ -110,17 +110,21 @@ impl Widget for Hyperlink {
let response = ui.add(Link::new(text));
if response.clicked() {
let modifiers = ui.ctx().input().modifiers;
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
let modifiers = ui.ctx().input(|i| i.modifiers);
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: modifiers.any(),
});
});
}
if response.middle_clicked() {
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: true,
});
});
}
response.on_hover_text(url)
}

View file

@ -72,7 +72,7 @@ impl Label {
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let sense = self.sense.unwrap_or_else(|| {
// We only want to focus labels if the screen reader is on.
if ui.memory().options.screen_reader {
if ui.memory(|mem| mem.options.screen_reader) {
Sense::focusable_noninteractive()
} else {
Sense::hover()
@ -120,7 +120,7 @@ impl Label {
if let Some(first_section) = text_job.job.sections.first_mut() {
first_section.leading_space = first_row_indentation;
}
let text_galley = text_job.into_galley(&ui.fonts());
let text_galley = ui.fonts(|f| text_job.into_galley(f));
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(
@ -153,7 +153,7 @@ impl Label {
text_job.job.justify = ui.layout().horizontal_justify();
};
let text_galley = text_job.into_galley(&ui.fonts());
let text_galley = ui.fonts(|f| text_job.into_galley(f));
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(),
@ -173,7 +173,7 @@ impl Widget for Label {
if ui.is_rect_visible(response.rect) {
let response_color = ui.style().interact(&response).text_color();
let underline = if response.has_focus() {
let underline = if response.has_focus() || response.highlighted() {
Stroke::new(1.0, response_color)
} else {
Stroke::NONE

View file

@ -185,7 +185,11 @@ impl RectElement for Bar {
fn default_values_format(&self, transform: &ScreenTransform) -> String {
let scale = transform.dvalue_dpos();
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
format!("\n{:.*}", y_decimals, self.value)
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
};
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
crate::plot::format_number(self.value, decimals)
}
}

View file

@ -269,9 +269,15 @@ impl RectElement for BoxElem {
fn default_values_format(&self, transform: &ScreenTransform) -> String {
let scale = transform.dvalue_dpos();
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
};
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
.at_most(6)
.at_least(1);
format!(
"\nMax = {max:.decimals$}\
"Max = {max:.decimals$}\
\nQuartile 3 = {q3:.decimals$}\
\nMedian = {med:.decimals$}\
\nQuartile 1 = {q1:.decimals$}\

View file

@ -34,6 +34,7 @@ pub(super) struct PlotConfig<'a> {
pub(super) trait PlotItem {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
/// For plot-items which are generated based on x values (plotting functions).
fn initialize(&mut self, x_range: RangeInclusive<f64>);
fn name(&self) -> &str;
@ -1647,6 +1648,7 @@ fn add_rulers_and_text(
let mut text = elem.name().to_owned(); // could be empty
if show_values {
text.push('\n');
text.push_str(&elem.default_values_format(plot.transform));
}
@ -1656,14 +1658,16 @@ fn add_rulers_and_text(
let font_id = TextStyle::Body.resolve(plot.ui.style());
let corner_value = elem.corner_value();
plot.ui.fonts(|f| {
shapes.push(Shape::text(
&plot.ui.fonts(),
f,
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
});
}
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
@ -1693,8 +1697,8 @@ pub(super) fn rulers_at_value(
let text = {
let scale = plot.transform.dvalue_dpos();
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
if let Some(custom_label) = label_formatter {
custom_label(name, &value)
} else if plot.show_x && plot.show_y {
@ -1712,15 +1716,16 @@ pub(super) fn rulers_at_value(
};
let font_id = TextStyle::Body.resolve(plot.ui.style());
plot.ui.fonts(|f| {
shapes.push(Shape::text(
&plot.ui.fonts(),
f,
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
});
}
fn find_closest_rect<'a, T>(

View file

@ -89,9 +89,7 @@ impl LegendEntry {
let font_id = text_style.resolve(ui.style());
let galley = ui
.fonts()
.layout_delayed_color(text, font_id, f32::INFINITY);
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
let icon_size = galley.size().y;
let icon_spacing = icon_size / 5.0;

View file

@ -108,11 +108,11 @@ struct PlotMemory {
impl PlotMemory {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -291,7 +291,10 @@ pub struct Plot {
legend_config: Option<Legend>,
show_background: bool,
show_axes: [bool; 2],
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl Plot {
@ -330,7 +333,10 @@ impl Plot {
legend_config: None,
show_background: true,
show_axes: [true; 2],
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
clamp_grid: false,
}
}
@ -555,6 +561,14 @@ impl Plot {
self
}
/// Clamp the grid to only be visible at the range of data where we have values.
///
/// Default: `false`.
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
self.clamp_grid = clamp_grid;
self
}
/// Expand bounds to include the given x value.
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
@ -617,6 +631,13 @@ impl Plot {
self
}
/// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an
/// undesired effect when shifting the plot bounds. Enabled by default.
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
self.sharp_grid_lines = enabled;
self
}
/// Resets the plot.
pub fn reset(mut self) -> Self {
self.reset = true;
@ -662,7 +683,10 @@ impl Plot {
show_axes,
linked_axes,
linked_cursors,
clamp_grid,
grid_spacers,
sharp_grid_lines,
} = self;
// Determine the size of the plot in the UI
@ -718,7 +742,7 @@ impl Plot {
});
let PlotMemory {
mut bounds_modified,
bounds_modified,
mut hovered_entry,
mut hidden_items,
last_screen_transform,
@ -730,6 +754,7 @@ impl Plot {
items: Vec::new(),
next_auto_color_idx: 0,
last_screen_transform,
bounds_modified,
response,
ctx: ui.ctx().clone(),
};
@ -738,6 +763,7 @@ impl Plot {
mut items,
mut response,
last_screen_transform,
mut bounds_modified,
..
} = plot_ui;
@ -929,9 +955,9 @@ impl Plot {
if let Some(hover_pos) = response.hover_pos() {
if allow_zoom {
let zoom_factor = if data_aspect.is_some() {
Vec2::splat(ui.input().zoom_delta())
Vec2::splat(ui.input(|i| i.zoom_delta()))
} else {
ui.input().zoom_delta_2d()
ui.input(|i| i.zoom_delta_2d())
};
if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos);
@ -939,7 +965,7 @@ impl Plot {
}
}
if allow_scroll {
let scroll_delta = ui.input().scroll_delta;
let scroll_delta = ui.input(|i| i.scroll_delta);
if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta);
bounds_modified = true.into();
@ -961,10 +987,12 @@ impl Plot {
axis_formatters,
show_axes,
transform: transform.clone(),
grid_spacers,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
draw_cursors,
grid_spacers,
sharp_grid_lines,
clamp_grid,
};
let plot_cursors = prepared.ui(ui, &response);
@ -1016,6 +1044,7 @@ pub struct PlotUi {
items: Vec<Box<dyn PlotItem>>,
next_auto_color_idx: usize,
last_screen_transform: ScreenTransform,
bounds_modified: AxisBools,
response: Response,
ctx: Context,
}
@ -1043,11 +1072,13 @@ impl PlotUi {
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
self.last_screen_transform.set_bounds(plot_bounds);
self.bounds_modified = true.into();
}
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
self.last_screen_transform.translate_bounds(delta_pos);
self.bounds_modified = true.into();
}
/// Returns `true` if the plot area is currently hovered.
@ -1068,7 +1099,7 @@ impl PlotUi {
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta();
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
let value = self.plot_from_screen(last_pos);
Some(value)
}
@ -1286,22 +1317,36 @@ struct PreparedPlot {
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
let mut shapes = Vec::new();
let mut axes_shapes = Vec::new();
for d in 0..2 {
if self.show_axes[d] {
self.paint_axis(ui, d, &mut shapes);
self.paint_axis(
ui,
d,
self.show_axes[1 - d],
&mut axes_shapes,
self.sharp_grid_lines,
);
}
}
// Sort the axes by strength so that those with higher strength are drawn in front.
axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
let transform = &self.transform;
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
@ -1369,11 +1414,21 @@ impl PreparedPlot {
cursors
}
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
fn paint_axis(
&self,
ui: &Ui,
axis: usize,
other_axis_shown: bool,
shapes: &mut Vec<(Shape, f32)>,
sharp_grid_lines: bool,
) {
#![allow(clippy::collapsible_else_if)]
let Self {
transform,
axis_formatters,
grid_spacers,
clamp_grid,
..
} = self;
@ -1387,7 +1442,6 @@ impl PreparedPlot {
let font_id = TextStyle::Body.resolve(ui.style());
// Where on the cross-dimension to show the label values
let bounds = transform.bounds();
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
let input = GridInput {
@ -1396,9 +1450,31 @@ impl PreparedPlot {
};
let steps = (grid_spacers[axis])(input);
let clamp_range = clamp_grid.then(|| {
let mut tight_bounds = PlotBounds::NOTHING;
for item in &self.items {
let item_bounds = item.bounds();
tight_bounds.merge_x(&item_bounds);
tight_bounds.merge_y(&item_bounds);
}
tight_bounds
});
for step in steps {
let value_main = step.value;
if let Some(clamp_range) = clamp_range {
if axis == 0 {
if !clamp_range.range_x().contains(&value_main) {
continue;
};
} else {
if !clamp_range.range_y().contains(&value_main) {
continue;
};
}
}
let value = if axis == 0 {
PlotPoint::new(value_main, value_cross)
} else {
@ -1408,29 +1484,47 @@ impl PreparedPlot {
let pos_in_gui = transform.position_from_point(&value);
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
let line_alpha = remap_clamp(
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
let line_strength = remap_clamp(
spacing_in_points,
(MIN_LINE_SPACING_IN_POINTS as f32)..=300.0,
0.0..=0.15,
MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
0.0..=1.0,
);
if line_alpha > 0.0 {
let line_color = color_from_alpha(ui, line_alpha);
let line_color = color_from_contrast(ui, line_strength);
let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui;
p0[1 - axis] = transform.frame().min[1 - axis];
p1[1 - axis] = transform.frame().max[1 - axis];
if let Some(clamp_range) = clamp_range {
if axis == 0 {
p0.y = transform.position_from_point_y(clamp_range.min[1]);
p1.y = transform.position_from_point_y(clamp_range.max[1]);
} else {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
}
if sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.ctx().round_pos_to_pixels(p0);
p1 = ui.ctx().round_pos_to_pixels(p1);
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
}
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
}
if text_alpha > 0.0 {
let color = color_from_alpha(ui, text_alpha);
const MIN_TEXT_SPACING: f32 = 40.0;
if spacing_in_points > MIN_TEXT_SPACING {
let text_strength =
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
let color = color_from_contrast(ui, text_strength);
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main, &axis_range)
@ -1438,8 +1532,11 @@ impl PreparedPlot {
emath::round_to_decimals(value_main, 5).to_string() // hack
};
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
// Custom formatters can return empty string to signal "no label at this resolution"
if !text.is_empty() {
if !text.is_empty() && !skip_origin_y {
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
@ -1449,17 +1546,20 @@ impl PreparedPlot {
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
.at_least(transform.frame().min[1 - axis] + 1.0);
shapes.push(Shape::galley(text_pos, galley));
shapes.push((Shape::galley(text_pos, galley), text_strength));
}
}
}
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
if ui.visuals().dark_mode {
Rgba::from_white_alpha(alpha).into()
} else {
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
}
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color;
let fg = ui.visuals().widgets.open.fg_stroke.color;
let mix = 0.5 * contrast.sqrt();
Color32::from_rgb(
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
)
}
}
@ -1552,3 +1652,16 @@ fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64,
});
out.extend(marks_iter);
}
/// Helper for formatting a number so that we always show at least a few decimals,
/// unless it is an integer, in which case we never show any decimals.
pub fn format_number(number: f64, num_decimals: usize) -> String {
let is_integral = number as i64 as f64 == number;
if is_integral {
// perfect integer - show it as such:
format!("{:.0}", number)
} else {
// make sure we tell the user it is not an integer by always showing a decimal or two:
format!("{:.*}", num_decimals.at_least(1), number)
}
}

View file

@ -224,6 +224,7 @@ impl ScreenTransform {
}
}
/// ui-space rectangle.
pub fn frame(&self) -> &Rect {
&self.frame
}
@ -263,18 +264,27 @@ impl ScreenTransform {
}
}
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
let x = remap(
value.x,
pub fn position_from_point_x(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[0]..=self.bounds.max[0],
(self.frame.left() as f64)..=(self.frame.right() as f64),
);
let y = remap(
value.y,
) as f32
}
pub fn position_from_point_y(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[1]..=self.bounds.max[1],
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
);
pos2(x as f32, y as f32)
) as f32
}
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
pos2(
self.position_from_point_x(value.x),
self.position_from_point_y(value.y),
)
}
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {

View file

@ -13,6 +13,7 @@ pub struct ProgressBar {
progress: f32,
desired_width: Option<f32>,
text: Option<ProgressBarText>,
fill: Option<Color32>,
animate: bool,
}
@ -23,6 +24,7 @@ impl ProgressBar {
progress: progress.clamp(0.0, 1.0),
desired_width: None,
text: None,
fill: None,
animate: false,
}
}
@ -33,6 +35,12 @@ impl ProgressBar {
self
}
/// The fill color of the bar.
pub fn fill(mut self, color: Color32) -> Self {
self.fill = Some(color);
self
}
/// A custom text to display on the progress bar.
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
self.text = Some(ProgressBarText::Custom(text.into()));
@ -60,6 +68,7 @@ impl Widget for ProgressBar {
progress,
desired_width,
text,
fill,
animate,
} = self;
@ -90,7 +99,8 @@ impl Widget for ProgressBar {
let (dark, bright) = (0.7, 1.0);
let color_factor = if animate {
lerp(dark..=bright, ui.input().time.cos().abs())
let time = ui.input(|i| i.time);
lerp(dark..=bright, time.cos().abs())
} else {
bright
};
@ -98,14 +108,17 @@ impl Widget for ProgressBar {
ui.painter().rect(
inner_rect,
rounding,
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
Color32::from(
Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
),
Stroke::NONE,
);
if animate {
let n_points = 20;
let start_angle = ui.input().time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
let circle_radius = rounding - 2.0;
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
@ -116,10 +129,8 @@ impl Widget for ProgressBar {
+ vec2(-rounding, 0.0)
})
.collect();
ui.painter().add(Shape::line(
points,
Stroke::new(2.0, visuals.faint_bg_color),
));
ui.painter()
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
}
if let Some(text_kind) = text {

View file

@ -61,11 +61,15 @@ impl Widget for SelectableLabel {
let visuals = ui.style().interact_selectable(&response, selected);
if selected || response.hovered() || response.has_focus() {
if selected || response.hovered() || response.highlighted() || response.has_focus() {
let rect = rect.expand(visuals.expansion);
ui.painter()
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
ui.painter().rect(
rect,
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
);
}
text.paint_with_visuals(ui.painter(), text_pos, &visuals);

View file

@ -14,6 +14,7 @@ use crate::*;
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Separator {
spacing: f32,
grow: f32,
is_horizontal_line: Option<bool>,
}
@ -21,6 +22,7 @@ impl Default for Separator {
fn default() -> Self {
Self {
spacing: 6.0,
grow: 0.0,
is_horizontal_line: None,
}
}
@ -28,12 +30,19 @@ impl Default for Separator {
impl Separator {
/// How much space we take up. The line is painted in the middle of this.
///
/// In a vertical layout, with a horizontal Separator,
/// this is the height of the separator widget.
///
/// In a horizontal layout, with a vertical Separator,
/// this is the width of the separator widget.
pub fn spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
/// Explicitly ask for a horizontal line.
///
/// By default you will get a horizontal line in vertical layouts,
/// and a vertical line in horizontal layouts.
pub fn horizontal(mut self) -> Self {
@ -42,18 +51,40 @@ impl Separator {
}
/// Explicitly ask for a vertical line.
///
/// By default you will get a horizontal line in vertical layouts,
/// and a vertical line in horizontal layouts.
pub fn vertical(mut self) -> Self {
self.is_horizontal_line = Some(false);
self
}
/// Extend each end of the separator line by this much.
///
/// The default is to take up the available width/height of the parent.
///
/// This will make the line extend outside the parent ui.
pub fn grow(mut self, extra: f32) -> Self {
self.grow += extra;
self
}
/// Contract each end of the separator line by this much.
///
/// The default is to take up the available width/height of the parent.
///
/// This effectively adds margins to the line.
pub fn shrink(mut self, shrink: f32) -> Self {
self.grow -= shrink;
self
}
}
impl Widget for Separator {
fn ui(self, ui: &mut Ui) -> Response {
let Separator {
spacing,
grow,
is_horizontal_line,
} = self;
@ -75,14 +106,14 @@ impl Widget for Separator {
let painter = ui.painter();
if is_horizontal_line {
painter.hline(
rect.x_range(),
(rect.left() - grow)..=(rect.right() + grow),
painter.round_to_pixel(rect.center().y),
stroke,
);
} else {
painter.vline(
painter.round_to_pixel(rect.center().x),
rect.y_range(),
(rect.top() - grow)..=(rect.bottom() + grow),
stroke,
);
}

View file

@ -79,10 +79,12 @@ pub struct Slider<'a> {
text: WidgetText,
/// Sets the minimal step of the widget value
step: Option<f64>,
drag_value_speed: Option<f64>,
min_decimals: usize,
max_decimals: Option<usize>,
custom_formatter: Option<NumFormatter<'a>>,
custom_parser: Option<NumParser<'a>>,
trailing_fill: Option<bool>,
}
impl<'a> Slider<'a> {
@ -123,10 +125,12 @@ impl<'a> Slider<'a> {
suffix: Default::default(),
text: Default::default(),
step: None,
drag_value_speed: None,
min_decimals: 0,
max_decimals: None,
custom_formatter: None,
custom_parser: None,
trailing_fill: None,
}
}
@ -212,6 +216,7 @@ impl<'a> Slider<'a> {
}
/// Sets the minimal change of the value.
///
/// Value `0.0` effectively disables the feature. If the new value is out of range
/// and `clamp_to_range` is enabled, you would not have the ability to change the value.
///
@ -221,8 +226,22 @@ impl<'a> Slider<'a> {
self
}
/// When dragging the value, how fast does it move?
///
/// Unit: values per point (logical pixel).
/// See also [`DragValue::speed`].
///
/// By default this is the same speed as when dragging the slider,
/// but you can change it here to for instance have a much finer control
/// by dragging the slider value rather than the slider itself.
pub fn drag_value_speed(mut self, drag_value_speed: f64) -> Self {
self.drag_value_speed = Some(drag_value_speed);
self
}
// TODO(emilk): we should also have a "min precision".
/// Set a minimum number of decimals to display.
///
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
@ -232,6 +251,7 @@ impl<'a> Slider<'a> {
// TODO(emilk): we should also have a "max precision".
/// Set a maximum number of decimals to display.
///
/// Values will also be rounded to this number of decimals.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
@ -241,6 +261,7 @@ impl<'a> Slider<'a> {
}
/// Set an exact number of decimals to display.
///
/// Values will also be rounded to this number of decimals.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
@ -250,6 +271,17 @@ impl<'a> Slider<'a> {
self
}
/// Display trailing color behind the slider's circle. Default is OFF.
///
/// This setting can be enabled globally for all sliders with [`Visuals::slider_trailing_fill`].
/// Toggling it here will override the above setting ONLY for this individual slider.
///
/// The fill color will be taken from `selection.bg_fill` in your [`Visuals`], the same as a [`ProgressBar`].
pub fn trailing_fill(mut self, trailing_fill: bool) -> Self {
self.trailing_fill = Some(trailing_fill);
self
}
/// Set custom formatter defining how numbers are converted into text.
///
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
@ -521,7 +553,7 @@ impl<'a> Slider<'a> {
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
let position = self.pointer_position(pointer_position_2d);
let new_value = if self.smart_aim {
let aim_radius = ui.input().aim_radius();
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(position - aim_radius, position_range.clone()),
self.value_from_position(position + aim_radius, position_range.clone()),
@ -543,19 +575,19 @@ impl<'a> Slider<'a> {
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
};
decrement += ui.input().num_presses(dec_key);
increment += ui.input().num_presses(inc_key);
ui.input(|input| {
decrement += input.num_presses(dec_key);
increment += input.num_presses(inc_key);
});
}
#[cfg(feature = "accesskit")]
{
use accesskit::Action;
decrement += ui
.input()
.num_accesskit_action_requests(response.id, Action::Decrement);
increment += ui
.input()
.num_accesskit_action_requests(response.id, Action::Increment);
ui.input(|input| {
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
});
}
let kb_step = increment as f32 - decrement as f32;
@ -567,7 +599,7 @@ impl<'a> Slider<'a> {
let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = ui.input().aim_radius();
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()),
@ -581,14 +613,13 @@ impl<'a> Slider<'a> {
#[cfg(feature = "accesskit")]
{
use accesskit::{Action, ActionData};
for request in ui
.input()
.accesskit_action_requests(response.id, Action::SetValue)
{
ui.input(|input| {
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
self.set_value(new_value);
}
}
});
}
// Paint it:
@ -598,22 +629,40 @@ impl<'a> Slider<'a> {
let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect));
let rail_rect = self.rail_rect(rect, rail_radius);
let position_1d = self.position_from_value(value, position_range);
let visuals = ui.style().interact(response);
ui.painter().add(epaint::RectShape {
rect: rail_rect,
rounding: ui.visuals().widgets.inactive.rounding,
fill: ui.visuals().widgets.inactive.bg_fill,
// fill: visuals.bg_fill,
// fill: ui.visuals().extreme_bg_color,
stroke: Default::default(),
// stroke: visuals.bg_stroke,
// stroke: ui.visuals().widgets.inactive.bg_stroke,
});
let widget_visuals = &ui.visuals().widgets;
ui.painter().rect_filled(
rail_rect,
widget_visuals.inactive.rounding,
widget_visuals.inactive.bg_fill,
);
let position_1d = self.position_from_value(value, position_range);
let center = self.marker_center(position_1d, &rail_rect);
// Decide if we should add trailing fill.
let trailing_fill = self
.trailing_fill
.unwrap_or_else(|| ui.visuals().slider_trailing_fill);
// Paint trailing fill.
if trailing_fill {
let mut trailing_rail_rect = rail_rect;
// The trailing rect has to be drawn differently depending on the orientation.
match self.orientation {
SliderOrientation::Vertical => trailing_rail_rect.min.y = center.y,
SliderOrientation::Horizontal => trailing_rail_rect.max.x = center.x,
};
ui.painter().rect_filled(
trailing_rail_rect,
widget_visuals.inactive.rounding,
ui.visuals().selection.bg_fill,
);
}
ui.painter().add(epaint::CircleShape {
center,
radius: self.handle_radius(rect) + visuals.expansion,
@ -679,18 +728,21 @@ impl<'a> Slider<'a> {
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
let change = {
// Hold one lock rather than 4 (see https://github.com/emilk/egui/pull/1380).
let input = ui.input();
let change = ui.input(|input| {
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
- input.num_presses(Key::ArrowDown) as i32
- input.num_presses(Key::ArrowLeft) as i32
});
let any_change = change != 0;
let speed = if let (Some(step), true) = (self.step, any_change) {
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
step
} else {
self.drag_value_speed
.unwrap_or_else(|| self.current_gradient(&position_range))
};
let speed = match self.step {
Some(step) if change != 0 => step,
_ => self.current_gradient(&position_range),
};
let mut value = self.get_value();
let response = ui.add({
let mut dv = DragValue::new(&mut value)
@ -740,20 +792,22 @@ impl<'a> Slider<'a> {
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
ui.ctx().accesskit_node_builder(response.id, |builder| {
use accesskit::Action;
node.min_numeric_value = Some(*self.range.start());
node.max_numeric_value = Some(*self.range.end());
node.numeric_value_step = self.step;
node.actions |= Action::SetValue;
builder.set_min_numeric_value(*self.range.start());
builder.set_max_numeric_value(*self.range.end());
if let Some(step) = self.step {
builder.set_numeric_value_step(step);
}
builder.add_action(Action::SetValue);
let clamp_range = self.clamp_range();
if value < *clamp_range.end() {
node.actions |= Action::Increment;
builder.add_action(Action::Increment);
}
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
}
builder.add_action(Action::Decrement);
}
});
let slider_response = response.clone();

View file

@ -48,8 +48,9 @@ impl Widget for Spinner {
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let start_angle = ui.input().time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);

View file

@ -19,7 +19,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
/// if response.changed() {
/// // …
/// }
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
/// // …
/// }
/// # });
@ -67,6 +67,9 @@ pub struct TextEdit<'t> {
desired_height_rows: usize,
lock_focus: bool,
cursor_at_end: bool,
min_size: Vec2,
align: Align2,
clip_text: bool,
}
impl<'t> WidgetWithState for TextEdit<'t> {
@ -89,6 +92,7 @@ impl<'t> TextEdit<'t> {
Self {
desired_height_rows: 1,
multiline: false,
clip_text: true,
..Self::multiline(text)
}
}
@ -112,6 +116,9 @@ impl<'t> TextEdit<'t> {
desired_height_rows: 4,
lock_focus: false,
cursor_at_end: true,
min_size: Vec2::ZERO,
align: Align2::LEFT_TOP,
clip_text: false,
}
}
@ -206,7 +213,7 @@ impl<'t> TextEdit<'t> {
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
/// layout_job.wrap.max_width = wrap_width;
/// ui.fonts().layout_job(layout_job)
/// ui.fonts(|f| f.layout_job(layout_job))
/// };
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
/// # });
@ -269,6 +276,37 @@ impl<'t> TextEdit<'t> {
self.cursor_at_end = b;
self
}
/// When `true` (default), overflowing text will be clipped.
///
/// When `false`, widget width will expand to make all text visible.
///
/// This only works for singleline [`TextEdit`].
pub fn clip_text(mut self, b: bool) -> Self {
// always show everything in multiline
if !self.multiline {
self.clip_text = b;
}
self
}
/// Set the horizontal align of the inner text.
pub fn horizontal_align(mut self, align: Align) -> Self {
self.align.0[0] = align;
self
}
/// Set the vertical align of the inner text.
pub fn vertical_align(mut self, align: Align) -> Self {
self.align.0[1] = align;
self
}
/// Set the minimum size of the [`TextEdit`].
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
}
// ----------------------------------------------------------------------------
@ -312,7 +350,7 @@ impl<'t> TextEdit<'t> {
output.response |= ui.interact(frame_rect, id, Sense::click());
}
if output.response.clicked() && !output.response.lost_focus() {
ui.memory().request_focus(output.response.id);
ui.memory_mut(|mem| mem.request_focus(output.response.id));
}
if frame {
@ -364,13 +402,16 @@ impl<'t> TextEdit<'t> {
layouter,
password,
frame: _,
margin: _,
margin,
multiline,
interactive,
desired_width,
desired_height_rows,
lock_focus,
cursor_at_end,
min_size,
align,
clip_text,
} = self;
let text_color = text_color
@ -381,7 +422,7 @@ impl<'t> TextEdit<'t> {
let prev_text = text.as_str().to_owned();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
let row_height = ui.fonts(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
@ -389,29 +430,31 @@ impl<'t> TextEdit<'t> {
available_width
} else {
desired_width.min(available_width)
};
} - margin.x * 2.0;
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
let text = mask_if_password(password, text);
ui.fonts().layout_job(if multiline {
let layout_job = if multiline {
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
})
};
ui.fonts(|f| f.layout_job(layout_job))
};
let layouter = layouter.unwrap_or(&mut default_layouter);
let mut galley = layouter(ui, text.as_str(), wrap_width);
let desired_width = if multiline {
galley.size().x.max(wrap_width) // always show everything in multiline
let desired_width = if clip_text {
wrap_width // visual clipping with scroll in singleline input.
} else {
wrap_width // visual clipping with scroll in singleline input. TODO(emilk): opt-in/out?
galley.size().x.max(wrap_width)
};
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
let desired_size = vec2(desired_width, galley.size().y.max(desired_height));
let desired_size = vec2(desired_width, galley.size().y.max(desired_height))
.at_least(min_size - margin * 2.0);
let (auto_id, rect) = ui.allocate_space(desired_size);
@ -428,8 +471,8 @@ impl<'t> TextEdit<'t> {
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
// Since currently copying selected text in not supported on `eframe` web,
// we prioritize touch-scrolling:
let any_touches = ui.input().any_touches(); // separate line to avoid double-locking the same mutex
let allow_drag_to_select = !any_touches || ui.memory().has_focus(id);
let allow_drag_to_select =
ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id));
let sense = if interactive {
if allow_drag_to_select {
@ -447,7 +490,7 @@ impl<'t> TextEdit<'t> {
if interactive {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.hovered() && text.is_mutable() {
ui.output().mutable_text_under_cursor = true;
ui.output_mut(|o| o.mutable_text_under_cursor = true);
}
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
@ -457,7 +500,7 @@ impl<'t> TextEdit<'t> {
if ui.visuals().text_cursor_preview
&& response.hovered()
&& ui.input().pointer.is_moving()
&& ui.input(|i| i.pointer.is_moving())
{
// preview:
paint_cursor_end(
@ -487,9 +530,9 @@ impl<'t> TextEdit<'t> {
secondary: galley.from_ccursor(ccursor_range.secondary),
}));
} else if allow_drag_to_select {
if response.hovered() && ui.input().pointer.any_pressed() {
ui.memory().request_focus(id);
if ui.input().modifiers.shift {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
ui.memory_mut(|mem| mem.request_focus(id));
if ui.input(|i| i.modifiers.shift) {
if let Some(mut cursor_range) = state.cursor_range(&galley) {
cursor_range.primary = cursor_at_pointer;
state.set_cursor_range(Some(cursor_range));
@ -499,7 +542,8 @@ impl<'t> TextEdit<'t> {
} else {
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
}
} else if ui.input().pointer.any_down() && response.is_pointer_button_down_on()
} else if ui.input(|i| i.pointer.any_down())
&& response.is_pointer_button_down_on()
{
// drag to select text:
if let Some(mut cursor_range) = state.cursor_range(&galley) {
@ -511,14 +555,14 @@ impl<'t> TextEdit<'t> {
}
}
if response.hovered() && interactive {
ui.output().cursor_icon = CursorIcon::Text;
if interactive && response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor_range(&galley);
if ui.memory().has_focus(id) && interactive {
ui.memory().lock_focus(id, lock_focus);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
let default_cursor_range = if cursor_at_end {
CursorRange::one(galley.end())
@ -545,11 +589,15 @@ impl<'t> TextEdit<'t> {
cursor_range = Some(new_cursor_range);
}
let mut text_draw_pos = response.rect.min;
let mut text_draw_pos = align
.align_size_within_rect(galley.size(), response.rect)
.intersect(response.rect) // limit pos to the response rect area
.min;
let align_offset = response.rect.left() - text_draw_pos.x;
// Visual clipping for singleline text editor with text larger than width
if !multiline {
let cursor_pos = match (cursor_range, ui.memory().has_focus(id)) {
if clip_text && align_offset == 0.0 {
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
_ => 0.0,
};
@ -571,6 +619,8 @@ impl<'t> TextEdit<'t> {
state.singleline_offset = offset_x;
text_draw_pos -= vec2(offset_x, 0.0);
} else {
state.singleline_offset = align_offset;
}
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
@ -594,7 +644,7 @@ impl<'t> TextEdit<'t> {
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
}
if ui.memory().has_focus(id) {
if ui.memory(|mem| mem.has_focus(id)) {
if let Some(cursor_range) = state.cursor_range(&galley) {
// We paint the cursor on top of the text, in case
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
@ -621,9 +671,13 @@ impl<'t> TextEdit<'t> {
// But `winit` and `egui_web` differs in how to set the
// position of IME.
if cfg!(target_arch = "wasm32") {
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
ui.ctx().output_mut(|o| {
o.text_cursor_pos = Some(cursor_pos.left_top());
});
} else {
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_bottom());
ui.ctx().output_mut(|o| {
o.text_cursor_pos = Some(cursor_pos.left_bottom());
});
}
}
}
@ -659,15 +713,16 @@ impl<'t> TextEdit<'t> {
}
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
{
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| {
use accesskit::{TextPosition, TextSelection};
let parent_id = response.id;
if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
node.text_selection = Some(TextSelection {
builder.set_text_selection(TextSelection {
anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
@ -679,23 +734,31 @@ impl<'t> TextEdit<'t> {
});
}
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
if self.multiline {
builder.set_multiline();
}
drop(node);
parent_id
});
if let Some(parent_id) = parent_id {
// drop ctx lock before further processing
use accesskit::{Role, TextDirection};
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
let mut node = ui.ctx().accesskit_node(id).unwrap();
node.role = Role::InlineTextBox;
ui.ctx().accesskit_node_builder(id, |builder| {
builder.set_role(Role::InlineTextBox);
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
node.text_direction = Some(TextDirection::LeftToRight);
builder.set_text_direction(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
@ -715,7 +778,8 @@ impl<'t> TextEdit<'t> {
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths.push((character_lengths.len() - last_word_start) as _);
word_lengths
.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
@ -734,14 +798,16 @@ impl<'t> TextEdit<'t> {
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
node.value = Some(value.into());
node.character_lengths = character_lengths.into();
node.character_positions = Some(character_positions.into());
node.character_widths = Some(character_widths.into());
node.word_lengths = word_lengths.into();
builder.set_value(value);
builder.set_character_lengths(character_lengths);
builder.set_character_positions(character_positions);
builder.set_character_widths(character_widths);
builder.set_word_lengths(word_lengths);
});
}
});
}
}
TextEditOutput {
response,
@ -811,19 +877,19 @@ fn events(
// We feed state to the undoer both before and after handling input
// so that the undoer creates automatic saves even when there are no events for a while.
state.undoer.lock().feed_state(
ui.input().time,
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
);
let copy_if_not_password = |ui: &Ui, text: String| {
if !password {
ui.ctx().output().copied_text = text;
ui.ctx().output_mut(|o| o.copied_text = text);
}
};
let mut any_change = false;
let events = ui.input().events.clone(); // avoid dead-lock by cloning. TODO(emilk): optimize
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
for event in &events {
let did_mutate_text = match event {
Event::Copy => {
@ -866,8 +932,9 @@ fn events(
key: Key::Tab,
pressed: true,
modifiers,
..
} => {
if multiline && ui.memory().has_lock_focus(id) {
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
let mut ccursor = delete_selected(text, &cursor_range);
if modifiers.shift {
// TODO(emilk): support removing indentation over a selection?
@ -891,7 +958,7 @@ fn events(
// TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
Some(CCursorRange::one(ccursor))
} else {
ui.memory().surrender_focus(id); // End input with enter
ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
break;
}
}
@ -899,6 +966,7 @@ fn events(
key: Key::Z,
pressed: true,
modifiers,
..
} if modifiers.command && !modifiers.shift => {
// TODO(emilk): redo
if let Some((undo_ccursor_range, undo_txt)) = state
@ -917,12 +985,9 @@ fn events(
key,
pressed: true,
modifiers,
..
} => on_key_press(&mut cursor_range, text, galley, *key, modifiers),
Event::KeyRepeat { key, modifiers } => {
on_key_press(&mut cursor_range, text, galley, *key, modifiers)
}
Event::CompositionStart => {
state.has_ime = true;
None
@ -997,7 +1062,7 @@ fn events(
state.set_cursor_range(Some(cursor_range));
state.undoer.lock().feed_state(
ui.input().time,
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
);

View file

@ -34,11 +34,11 @@ pub struct TextEditState {
impl TextEditState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
/// The the currently selected range of characters.

View file

@ -1,6 +1,6 @@
[package]
name = "egui_demo_app"
version = "0.20.0"
version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
@ -20,7 +20,7 @@ default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
screen_reader = ["eframe/screen_reader"] # experimental
web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
@ -30,11 +30,11 @@ wgpu = ["eframe/wgpu", "bytemuck"]
[dependencies]
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
eframe = { version = "0.20.0", path = "../eframe", default-features = false }
egui = { version = "0.20.0", path = "../egui", features = [
eframe = { version = "0.21.0", path = "../eframe", default-features = false }
egui = { version = "0.21.0", path = "../egui", features = [
"extra_debug_asserts",
] }
egui_demo_lib = { version = "0.20.0", path = "../egui_demo_lib", features = [
egui_demo_lib = { version = "0.21.0", path = "../egui_demo_lib", features = [
"chrono",
] }
tracing = "0.1"
@ -42,7 +42,7 @@ tracing = "0.1"
# Optional dependencies:
bytemuck = { version = "1.7.1", optional = true }
egui_extras = { version = "0.20.0", optional = true, path = "../egui_extras" }
egui_extras = { version = "0.21.0", optional = true, path = "../egui_extras" }
# feature "http":
ehttp = { version = "0.2.0", optional = true }

View file

@ -146,12 +146,12 @@ impl RotatingTriangle {
),
);
gl.compile_shader(shader);
if !gl.get_shader_compile_status(shader) {
panic!(
"Failed to compile custom_3d_glow: {}",
assert!(
gl.get_shader_compile_status(shader),
"Failed to compile custom_3d_glow {shader_type}: {}",
gl.get_shader_info_log(shader)
);
}
gl.attach_shader(program, shader);
shader
})

View file

@ -1,8 +1,8 @@
use std::{num::NonZeroU64, sync::Arc};
use eframe::{
egui_wgpu::wgpu::util::DeviceExt,
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
pub struct Custom3d {

View file

@ -35,7 +35,7 @@ impl Default for FractalClock {
impl FractalClock {
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if !self.paused {
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time));
ui.ctx().request_repaint();
}

View file

@ -131,7 +131,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
trigger_fetch = true;
}
if ui.button("Random image").clicked() {
let seed = ui.input().time;
let seed = ui.input(|i| i.time);
let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
trigger_fetch = true;
@ -187,7 +187,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
if let Some(text) = &text {
let tooltip = "Click to copy the response body";
if ui.button("📋").on_hover_text(tooltip).clicked() {
ui.output().copied_text = text.clone();
ui.output_mut(|o| o.copied_text = text.clone());
}
ui.separator();
}
@ -245,7 +245,7 @@ impl ColoredText {
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
let mut layout_job = self.0.clone();
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
ui.fonts(|f| f.layout_job(layout_job))
};
let mut text = self.0.text.as_str();
@ -258,7 +258,7 @@ impl ColoredText {
} else {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts().layout_job(job);
let galley = ui.fonts(|f| f.layout_job(job));
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
painter.add(egui::Shape::galley(response.rect.min, galley));
}

View file

@ -81,7 +81,7 @@ impl Default for BackendPanel {
impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
match self.run_mode {
RunMode::Continuous => {
@ -130,10 +130,12 @@ impl BackendPanel {
ui.separator();
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "web_screen-reader")]
{
let mut screen_reader = ui.ctx().options().screen_reader;
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options().screen_reader = screen_reader;
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
}
#[cfg(not(target_arch = "wasm32"))]
@ -172,10 +174,14 @@ impl BackendPanel {
ui.horizontal(|ui| {
{
let mut fullscreen = frame.info().window_info.fullscreen;
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
.on_hover_text("Fullscreen the window");
if ui
.checkbox(&mut fullscreen, "🗖 Fullscreen (F11)")
.on_hover_text("Fullscreen the window")
.changed()
{
frame.set_fullscreen(fullscreen);
}
}
if ui
.button("📱 Phone Size")
@ -335,9 +341,11 @@ impl EguiWindows {
output_event_history,
} = self;
for event in &ctx.output().events {
ctx.output(|o| {
for event in &o.events {
output_event_history.push_back(event.clone());
}
});
while output_event_history.len() > 1000 {
output_event_history.pop_front();
}

View file

@ -95,19 +95,21 @@ impl FrameHistory {
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(Shape::text(
&ui.fonts(),
shapes.push(ui.fonts(|f| {
Shape::text(
f,
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
));
)
}));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input().time; // Time at right side of screen
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;

View file

@ -3,7 +3,18 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
fn main() {
fn main() -> Result<(), eframe::Error> {
{
// Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206)
let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] {
if !rust_log.contains(&format!("{loud_crate}=")) {
rust_log += &format!(",{loud_crate}=warn");
}
}
std::env::set_var("RUST_LOG", rust_log);
}
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
@ -21,5 +32,5 @@ fn main() {
"egui demo app",
options,
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
);
)
}

View file

@ -175,8 +175,8 @@ impl eframe::App for WrapApp {
eframe::set_value(storage, eframe::APP_KEY, &self.state);
}
fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
visuals.panel_fill.into()
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
visuals.panel_fill.to_normalized_gamma_f32()
}
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
@ -190,6 +190,11 @@ impl eframe::App for WrapApp {
self.state.selected_anchor = selected_anchor;
}
#[cfg(not(target_arch = "wasm32"))]
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) {
frame.set_fullscreen(!frame.info().window_info.fullscreen);
}
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
egui::trace!(ui);
ui.horizontal_wrapped(|ui| {
@ -233,7 +238,8 @@ impl WrapApp {
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// The backend-panel can be toggled on/off.
// We show a little animation when the user switches it.
let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible();
let is_open =
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
egui::SidePanel::left("backend_panel")
.resizable(false)
@ -258,13 +264,13 @@ impl WrapApp {
.on_hover_text("Forget scroll, positions, sizes etc")
.clicked()
{
*ui.ctx().memory() = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
if ui.button("Reset everything").clicked() {
self.state = Default::default();
*ui.ctx().memory() = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
});
@ -274,7 +280,7 @@ impl WrapApp {
let mut found_anchor = false;
let selected_anchor = self.state.selected_anchor.clone();
for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
app.update(ctx, frame);
found_anchor = true;
}
@ -308,7 +314,7 @@ impl WrapApp {
{
selected_anchor = anchor.to_owned();
if frame.is_web() {
ui.output().open_url(format!("#{}", anchor));
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
}
}
}
@ -320,7 +326,7 @@ impl WrapApp {
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
self.state.selected_anchor = "clock".to_owned();
if frame.is_web() {
ui.output().open_url("#clock");
ui.output_mut(|o| o.open_url("#clock"));
}
}
}
@ -334,9 +340,10 @@ impl WrapApp {
use std::fmt::Write as _;
// Preview hovering files:
if !ctx.input().raw.hovered_files.is_empty() {
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
let text = ctx.input(|i| {
let mut text = "Dropping files:\n".to_owned();
for file in &ctx.input().raw.hovered_files {
for file in &i.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
@ -345,11 +352,13 @@ impl WrapApp {
text += "\n???";
}
}
text
});
let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.input().screen_rect();
let screen_rect = ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
@ -361,9 +370,11 @@ impl WrapApp {
}
// Collect dropped files:
if !ctx.input().raw.dropped_files.is_empty() {
self.dropped_files = ctx.input().raw.dropped_files.clone();
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
self.dropped_files = i.raw.dropped_files.clone();
}
});
// Show dropped files (if any):
if !self.dropped_files.is_empty() {

View file

@ -1,6 +1,6 @@
[package]
name = "egui_demo_lib"
version = "0.20.0"
version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Example library for egui"
edition = "2021"
@ -30,8 +30,8 @@ syntax_highlighting = ["syntect"]
[dependencies]
egui = { version = "0.20.0", path = "../egui", default-features = false }
egui_extras = { version = "0.20.0", path = "../egui_extras" }
egui = { version = "0.21.0", path = "../egui", default-features = false }
egui_extras = { version = "0.21.0", path = "../egui_extras" }
enum-map = { version = "2", features = ["serde"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
unicode_names2 = { version = "0.6.0", default-features = false }

View file

@ -38,7 +38,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
if false {
let ctx = egui::Context::default();
ctx.memory().set_everything_is_visible(true); // give us everything
ctx.memory_mut(|m| m.set_everything_is_visible(true)); // give us everything
let mut demo_windows = egui_demo_lib::DemoWindows::default();
c.bench_function("demo_full_no_tessellate", |b| {
b.iter(|| {

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