Compare commits

...

311 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
Emil Ernerfeldt
54675f8983 Update changelog for #2416 2022-12-09 09:55:16 +01:00
ItsEthra
6cc43dbdd4
Fixed backspace repeat with TextEdit (#2416) 2022-12-09 09:54:00 +01:00
Emil Ernerfeldt
85f615f21c Fix incorrect date in CHANGELOGs 2022-12-09 08:29:44 +01:00
Emil Ernerfeldt
7d91e90481 Release 0.20.0 - AccessKit, prettier text, overlapping widgets 2022-12-08 15:11:54 +01:00
Emil Ernerfeldt
124ef8ddf8 Update poll-promise and unicode_names2 2022-12-08 15:08:59 +01:00
Emil Ernerfeldt
896a299bf9 New web demo 2022-12-08 14:55:12 +01:00
Emil Ernerfeldt
4af7dc9e35 Very small doc and changelog tweaks 2022-12-08 14:49:13 +01:00
Emil Ernerfeldt
da0a178701
Style tweaks (#2406)
* Note the namechange of egui::color to egui::ecolor

* Use a solid triangle for collapsing headers and windows

* Add Shadow::NONE

* Add Visuals::panel_fill, window_fill and window_stroke

* Bug fix: ComboBox::width sets the outer width of the ComboBox

* egui_extras::Table: add functions to access the `Ui` for the header/body

* ComboBox: use solid triangle

* Tweak default menu margin

* Nudge panel separator lines so they stay visible

* Update changelogs
2022-12-08 10:55:13 +01:00
Andreas Reich
5effc68ba4
Split out ecolor crate (#2399)
* split out ecolor crate

* split up ecolor crate in lots of modules

* add changelog notes

* add readme to ecolor

* put clippy::manual_range_contains on cranky allow list

* fix hex color issues

* doc fixes

* more hex_color fixes

* Document features

* Rename hex_color module to avoid warning

* Sort the feature names

* fix link in CHANGELOG.md

* better wording

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-12-06 20:42:25 +01:00
Emil Ernerfeldt
e30ac7f91a
Only add glutin dependency if you are using the glow backend (#2402) 2022-12-06 14:29:58 +01:00
Red Artist
9145893066
fix wrong variable name (#2400)
* fix wrong variable name

* fmt
2022-12-06 13:50:58 +01:00
René Rössler
c3932f7f7f
Chrono update (#2397)
* limit day to last day of month if the month or year is changed

* update chrono to 0.4.23, switch to NaiveDate and remove use of deprecated functions.
2022-12-06 11:42:53 +01:00
Emil Ernerfeldt
9e3da91a59 Add pronunciation guide at the top of the README 2022-12-06 10:58:51 +01:00
Emil Ernerfeldt
452541d7c2 Add a "sponsored by Rerun" section at the end of the README.md 2022-12-06 10:58:51 +01:00
Luke Jones
be4a5be145
egui_glow: update example to latest glutin (#2396)
This lifts the context handling from commit hash 8eb687c as this does
all the required handling for us that the older glutin once did.
2022-12-06 10:02:20 +01:00
Matt Campbell
32144d3cb9
Update README section on accessibility (#2392) 2022-12-05 20:40:01 +01:00
Emil Ernerfeldt
b8a1670781 Change demo keyboard shortcuts to some that work in the browser 2022-12-05 17:57:11 +01:00
Emil Ernerfeldt
a86ec5a749 Remove extra separator in color test 2022-12-05 17:38:38 +01:00
Emil Ernerfeldt
8ae4d49e6e More changelog updates 2022-12-05 17:38:30 +01:00
Mike Krüger
ee2582964d
Added KeyRepeat event. (#2389)
KeyPress no longer repeats.

Co-authored-by: Mike Krueger <mkrueger@posteo.de>
2022-12-05 17:37:25 +01:00
Emil Ernerfeldt
3519358b7b Clippy fix 2022-12-05 13:41:25 +01:00
Emil Ernerfeldt
be6d23eed1 Replace Stroke::none() with Stroke::NONE 2022-12-05 12:59:02 +01:00
Emil Ernerfeldt
df01db2df1 Add show_separator_line to SidePanel and TopBottomPanel
So as not to force it onto all users since #2261
2022-12-05 12:58:26 +01:00
Emil Ernerfeldt
aa503008ae Update changelogs with recent additions 2022-12-05 12:51:00 +01:00
Emil Ernerfeldt
d7fa40ebba
cargo update (#2386)
* Better cargo deny check in check.sh

* cargo update

    Updating ab_glyph v0.2.16 -> v0.2.18
    Updating ab_glyph_rasterizer v0.1.5 -> v0.1.7
    Removing adler32 v1.2.0
    Removing ahash v0.4.7
    Removing ahash v0.8.1
      Adding ahash v0.8.2
    Updating aho-corasick v0.7.18 -> v0.7.20
    Updating android_system_properties v0.1.4 -> v0.1.5
    Removing ansi_term v0.12.1
    Updating anyhow v1.0.62 -> v1.0.66
    Updating ash v0.37.0+1.3.209 -> v0.37.1+1.3.235
    Updating async-channel v1.7.1 -> v1.8.0
    Updating async-executor v1.4.1 -> v1.5.0
    Updating async-io v1.8.0 -> v1.12.0
    Updating async-lock v2.5.0 -> v2.6.0
    Updating async-recursion v0.3.2 -> v1.0.0
    Updating async-trait v0.1.57 -> v0.1.59
    Updating base64 v0.13.0 -> v0.13.1
    Updating bindgen v0.61.0 -> v0.63.0
      Adding block-buffer v0.10.3
    Updating bumpalo v3.11.0 -> v3.11.1
    Updating bytemuck v1.12.1 -> v1.12.3
    Updating bytemuck_derive v1.2.1 -> v1.3.0
    Updating bytes v1.2.1 -> v1.3.0
    Removing cache-padded v1.2.0
    Updating calloop v0.10.1 -> v0.10.3
    Updating cc v1.0.73 -> v1.0.77
    Updating cfg-expr v0.10.3 -> v0.11.0
    Updating chrono v0.4.22 -> v0.4.23
    Updating clang-sys v1.3.3 -> v1.4.0
    Updating clap v3.2.17 -> v3.2.23
    Updating cmake v0.1.48 -> v0.1.49
    Updating cocoa v0.24.0 -> v0.24.1
    Updating concurrent-queue v1.2.4 -> v2.0.0
      Adding cpufeatures v0.2.5
    Updating crossbeam-utils v0.8.11 -> v0.8.14
    Updating crossfont v0.5.0 -> v0.5.1
      Adding crypto-common v0.1.6
      Adding cxx v1.0.83
      Adding cxx-build v1.0.83
      Adding cxxbridge-flags v1.0.83
      Adding cxxbridge-macro v1.0.83
    Updating dark-light v0.2.2 -> v0.2.3
    Removing deflate v1.0.0
      Adding digest v0.10.6
    Updating dlv-list v0.2.3 -> v0.3.0
    Updating document-features v0.2.3 -> v0.2.6
    Updating flate2 v1.0.24 -> v1.0.25
    Updating fontconfig-parser v0.5.0 -> v0.5.1
    Updating fontdb v0.9.1 -> v0.9.3
    Updating form_urlencoded v1.0.1 -> v1.1.0
    Updating futures-core v0.3.23 -> v0.3.25
    Updating futures-io v0.3.23 -> v0.3.25
    Updating futures-sink v0.3.23 -> v0.3.25
    Updating futures-task v0.3.23 -> v0.3.25
    Updating futures-util v0.3.23 -> v0.3.25
      Adding generic-array v0.14.6
    Updating getrandom v0.2.7 -> v0.2.8
    Removing hashbrown v0.9.1
    Updating iana-time-zone v0.1.46 -> v0.1.53
      Adding iana-time-zone-haiku v0.1.1
    Updating idna v0.2.3 -> v0.3.0
    Updating image v0.24.3 -> v0.24.5
    Updating indexmap v1.9.1 -> v1.9.2
    Updating itertools v0.10.3 -> v0.10.5
    Updating itoa v1.0.3 -> v1.0.4
      Adding jni v0.20.0
    Updating jobserver v0.1.24 -> v0.1.25
      Adding jpeg-decoder v0.3.0
    Updating libc v0.2.132 -> v0.2.138
    Updating libloading v0.7.3 -> v0.7.4
    Updating libm v0.2.5 -> v0.2.6
      Adding link-cplusplus v1.0.7
      Adding litrs v0.2.3
    Updating lock_api v0.4.7 -> v0.4.9
    Updating memmap2 v0.5.7 -> v0.5.8
    Removing miniz_oxide v0.5.3
      Adding miniz_oxide v0.5.4
      Adding miniz_oxide v0.6.2
    Updating mio v0.8.4 -> v0.8.5
    Removing nix v0.22.3
    Removing nix v0.23.1
    Removing nix v0.24.2
      Adding nix v0.24.3
      Adding nix v0.25.1
      Adding nu-ansi-term v0.46.0
    Removing num_threads v0.1.6
    Updating once_cell v1.13.1 -> v1.16.0
    Updating ordered-multimap v0.3.1 -> v0.4.3
    Updating ordered-stream v0.0.1 -> v0.1.2
    Updating os_str_bytes v6.3.0 -> v6.4.1
      Adding overload v0.1.1
    Updating owned_ttf_parser v0.15.1 -> v0.17.1
    Updating parking_lot_core v0.9.3 -> v0.9.5
    Updating percent-encoding v2.1.0 -> v2.2.0
    Updating pkg-config v0.3.25 -> v0.3.26
    Updating png v0.17.5 -> v0.17.7
    Updating polling v2.2.0 -> v2.5.1
    Updating ppv-lite86 v0.2.16 -> v0.2.17
    Updating proc-macro2 v1.0.43 -> v1.0.47
    Updating profiling v1.0.6 -> v1.0.7
    Updating rand_core v0.6.3 -> v0.6.4
    Updating regex v1.6.0 -> v1.7.0
    Updating regex-syntax v0.6.27 -> v0.6.28
    Updating rgb v0.8.33 -> v0.8.34
      Adding roxmltree v0.15.1
    Updating rust-ini v0.17.0 -> v0.18.0
    Updating rustls v0.20.6 -> v0.20.7
    Updating scoped-tls v1.0.0 -> v1.0.1
      Adding scratch v1.0.2
    Updating sctk-adwaita v0.4.2 -> v0.4.3
    Updating serde v1.0.143 -> v1.0.149
    Updating serde_derive v1.0.143 -> v1.0.149
    Updating serde_json v1.0.83 -> v1.0.89
    Updating sha1 v0.6.1 -> v0.10.5
    Removing sha1_smol v1.0.0
    Updating smallvec v1.9.0 -> v1.10.0
    Updating socket2 v0.4.4 -> v0.4.7
    Updating svgtypes v0.8.1 -> v0.8.2
    Updating syn v1.0.99 -> v1.0.105
    Removing synstructure v0.12.6
    Updating system-deps v6.0.2 -> v6.0.3
    Updating textwrap v0.15.0 -> v0.16.0
    Updating thiserror v1.0.32 -> v1.0.37
    Updating thiserror-impl v1.0.32 -> v1.0.37
    Removing time v0.1.44
    Removing time v0.3.13
      Adding time v0.1.45
      Adding time v0.3.17
      Adding time-core v0.1.0
      Adding time-macros v0.2.6
    Updating tracing v0.1.36 -> v0.1.37
    Updating tracing-attributes v0.1.22 -> v0.1.23
    Updating tracing-core v0.1.29 -> v0.1.30
    Updating tracing-subscriber v0.3.15 -> v0.3.16
      Adding ttf-parser v0.17.1
      Adding typenum v1.15.0
    Updating unicode-ident v1.0.3 -> v1.0.5
    Updating unicode-normalization v0.1.21 -> v0.1.22
    Updating unicode-script v0.5.4 -> v0.5.5
    Updating unicode-width v0.1.9 -> v0.1.10
    Updating unicode-xid v0.2.3 -> v0.2.4
    Updating url v2.2.2 -> v2.3.1
    Updating version-compare v0.1.0 -> v0.1.1
    Updating wayland-client v0.29.4 -> v0.29.5
    Updating wayland-commons v0.29.4 -> v0.29.5
    Updating wayland-cursor v0.29.4 -> v0.29.5
    Updating wayland-egl v0.29.4 -> v0.29.5
    Updating wayland-protocols v0.29.4 -> v0.29.5
    Updating wayland-scanner v0.29.4 -> v0.29.5
    Updating wayland-sys v0.29.4 -> v0.29.5
    Updating webbrowser v0.8.0 -> v0.8.2
    Updating webpki-roots v0.22.4 -> v0.22.5
    Updating wgpu v0.14.0 -> v0.14.2
    Updating wgpu-core v0.14.0 -> v0.14.2
    Updating wgpu-hal v0.14.0 -> v0.14.1
    Updating wgpu-types v0.14.0 -> v0.14.1
    Updating which v4.2.5 -> v4.3.0
    Updating widestring v0.5.1 -> v1.0.2
      Adding windows-sys v0.42.0
    Updating winreg v0.8.0 -> v0.10.1
    Updating x11-dl v2.20.0 -> v2.20.1
    Updating xmlparser v0.13.3 -> v0.13.5
    Updating zbus v2.3.2 -> v3.6.0
    Updating zbus_macros v2.3.2 -> v3.6.0
    Updating zbus_names v2.2.0 -> v2.4.0
    Updating zerocopy-derive v0.3.1 -> v0.3.2
    Updating zstd-sys v2.0.1+zstd.1.5.2 -> v2.0.4+zstd.1.5.2
    Updating zvariant v3.6.0 -> v3.9.0
    Updating zvariant_derive v3.6.0 -> v3.9.0

* downgrade image, webbrowser and fontconfig-parser to pass cargo deny

avoiding duplicate crate

* Remove unused dependency on egui_extras from screenshot demo

* Downgrade `chrono` to avoid having to fix egui-extras now
2022-12-05 12:12:51 +01:00
Red Artist
8eb687cf04
Glutin Upgrade (#2187)
* working. but x11 blurry

* fixed x11 blurry. was just accidentally using multisampling even when user didnt request it

* allow dbg macro temporarily

* add windows WGL fallback support when EGL fails

* fmt

* glutin features explicitly added

* extract glutin context creation into a fn

* fix warnings
2022-12-05 11:34:28 +01:00
Emil Ernerfeldt
3ba39c3022
Hide all Area:s and Window:s the first frame to hide first-frame-jitters (#2385) 2022-12-05 11:18:01 +01:00
Emil Ernerfeldt
0b2d56cff0
Grid: special-treat first frame to fix bug causing expanding grids (#2384) 2022-12-05 11:16:37 +01:00
Emil Ernerfeldt
4e59296cbb Minor code tweaks 2022-12-05 09:45:07 +01:00
Emil Ernerfeldt
5093b67e9b Enable and fix some more clippy lints 2022-12-05 09:29:59 +01:00
Matt Campbell
e1f348e4b2
Implement accessibility APIs via AccessKit (#2294)
* squash before rebase

* Update AccessKit, introducing support for editable spinners on Windows and an important fix for navigation order on macOS

* Restore support for increment and decrement actions in DragValue

* Avoid VoiceOver race condition bug

* fix clippy lint

* Tell AccessKit that the default action for a text edit (equivalent to a click) is to set the focus. This matters to some platform adapters.

* Refactor InputState functions for AccessKit actions

* Support the AccessKit SetValue for DragValue; this is the only way for a Windows AT to programmatically adjust the value

* Same for Slider

* Properly associate the slider label with both the slider and the drag value

* Lazily activate egui's AccessKit support

* fix clippy lint

* Update AccessKit

* More documentation, particularly around lazy activation

* Tweak one of the doc comments

* See if I can get AccessKit exempted from the 'missing backticks' lint

* Make PlatformOutput::accesskit_update an Option

* Refactor lazy activation

* Refactor node mutation (again)

* Eliminate the need for an explicit is_accesskit_active method, at least for now

* Fix doc comment

* More refactoring of tree construction; don't depend on Arc::get_mut

* Override a clippy lint; I seem to have no other choice

* Final planned refactor: a more flexible approach to hierarchy

* Last AccessKit update for this PR; includes an important macOS DPI fix

* Move and document the optional accesskit dependency

* Fix comment typo

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

* reformat

* More elegant code for conditionally creating a node

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

* Set step to 1.0 for all integer sliders

* Add doc example for Response::labelled_by

* Clarify a TODO comment I left for myself

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-12-04 19:17:12 +01:00
Emil Ernerfeldt
48666e1d7a
Automatically generate screenshots for all examples (#2379) 2022-12-04 17:27:40 +01:00
winne42
b774159fc8
let monospace text style be monospaced (#2201)
* let monospace text style be monospaced

* use monospace shortcut
2022-12-04 15:39:55 +01:00
Emil Ernerfeldt
ec0b2d8e9a Deprecate ui.centered
It didn't do what it advertised to do, and there is no simple way
of doing that right now.
2022-12-04 10:16:37 +01:00
Emil Ernerfeldt
c8771cd13e Add some minor improvements for debugging id clashes 2022-12-02 09:52:26 +01:00
Emil Ernerfeldt
e613315866 Improve issue templates 2022-12-02 09:51:21 +01:00
Emil Ernerfeldt
7133818c59
Make sure scroll bars are always visible (#2371)
* Nicer debug rectangles

* Move scrollbars into the clip-rect so they are always visible

* Improve table demo

* Add options for controlling inner and outer margin of the scroll bars

* Add line to changelog

* Update egui_extras changelog with recent Table improvements

* Refactor Table:s scroll options

* Add Table::auto_size

* Rename it auto_shrink
2022-11-30 22:58:00 +01:00
JP
85f8eeb9d5
Fix key pressed event (#2334)
* Fix key press event

* Add example with key presses

* Changelog line for key_press fix

* PR review improvements

* Add PR link in changelog

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-30 20:40:11 +01:00
Emil Ernerfeldt
2dc2a5540d
egui_extras::Table improvements (#2369)
* Use simple `ui.interact` for the resize line

* Introduce TableReizeState

* Simplify some code

* Add striped options to table demo

* Auto-size table columns by double-clicking the resize line

* Table: add option to auto-size the columns

* Table: don't let column width gets too small, unless clipping is on

* egui_extras: always use serde

Otherwise using `get_persisted` etc is impossible,
and working around that tedious.

* Avoid clipping last column in a resizable table

* Some better naming

* Table: Use new `Column` for setting column sizes and properties

Also make `clip` a per-column property

* All Table:s store state for auto-sizing purposes

* Customize each column wether or not it is resizable

* fix some auto-sizing bugs

* Fix shrinkage of adaptive column content

* Rename `scroll` to `vscroll` for clarity

* Add Table::scroll_to_row

* scroll_to_row takes alignment

* Fix bug in table sizing

* Strip: turn clipping OFF by default, because it is dangerous and sucks

* Add TableBody::mac_rect helper

* Table: add options to control the scroll area height.

* Docstring fixes

* Cleanup
2022-11-30 19:56:06 +01:00
Matt Campbell
0336816faf
Fix keyboard support in DragValue (#2342)
* Enable incrementing and decrementing `DragValue` with the keyboard

* As soon as a DragValue is focused, render it in edit mode

* Simpler, more reliable approach to managing the drag value's edit string

* Add changelog entry

* Update doc comment

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

* Add comment explaining why we don't listen for left/right arrow

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-29 15:27:14 +01:00
Fotis Gimian
975cbac83a
Update the glow example to avoid a white flash when the app starts. (#2345) 2022-11-29 15:04:17 +01:00
Emil Ernerfeldt
a3f1e5961f Fix bug in keyboard shortcut formatting 2022-11-27 22:03:42 +01:00
Erlend Walstad
c5e6def65e
Only update pixels_per_point when it has changed (#2352) 2022-11-27 17:22:45 +01:00
Erlend Walstad
d2f70cdcd1
Make it easier to convert strings to Id (#2350) 2022-11-27 13:15:18 +01:00
Nagy Tibor
502e1aa229
Do not emit changed responses on unchanged values in selectable_value/radio_value (#2343) 2022-11-25 22:23:56 +01:00
Lukas Hermann
f9066ff285
[WGPU] Allow for depth buffer in web target (#2335)
* Add depth stencil initialization to `Painter`

* Move depth stencil initialization into `resize_and_generate_depth_texture_view`, and call it in `set_window` and `on_window_resized`

* Allow for depth texture in WASM builds

* change `map` to `if let` statement

* use reference for render state

* Clean up descriptors and move texture generation to on resize events

* remove unused field from WebOptions
2022-11-24 20:40:53 +01:00
Emil Ernerfeldt
7d8154971b
Update ndk-sys v0.4.0 -> v0.4.1+23.1.7779620 (#2340)
Closes https://github.com/emilk/egui/issues/2336
2022-11-24 10:09:32 +01:00
Felix Zwettler
f3633534e7
add set_plot_bounds method, giving users the ability to set the plot bounds themselves. (#2320)
* add set_plot_bounds method

* call it from_min_max for consistency with Rect

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-22 15:05:23 +01:00
Emil Ernerfeldt
bde47c9957
epaint: cover default fonts in the license field (#2327) 2022-11-22 13:44:01 +01:00
Emil Ernerfeldt
8602326af5 Plot: round hlines, vlines, and grid lines to pixels to avoid aliasing 2022-11-22 13:42:52 +01:00
Emil Ernerfeldt
8671aa26e1 Added support for thin space
https://en.wikipedia.org/wiki/Thin_space
2022-11-21 17:33:23 +01:00
Emil Ernerfeldt
1c8cf9e3d5
Move egui::util::History to emath::History (#2329)
* Move egui::util::History to emath::History

It is a nice thing to use outside of egui,
and it is more math-related than gui-related.

* Fix doctest
2022-11-21 14:14:33 +01:00
Lukas Hermann
f4d8ab9779
[wgpu] Add depth stencil initialization to Painter (#2316) 2022-11-21 10:08:24 +01:00
Clement Rey
dfc1f2c470
add plot_secondary_clicked to plot_ui (#2318) 2022-11-17 17:07:42 +01:00
Emil Ernerfeldt
eca5e6a4d2
Update to Rust 1.65 (#2314)
* Update to Rust 1.65

Because then you can use dynamic linking on Linux

* Fix a bunch of clippy lints

* Update changelogs

* More clippy fixes
2022-11-16 19:08:03 +01:00
Clement Rey
f7019926dc
wgpu backend: do not try to render zero-sized surfaces (#2313)
* wgpu backend: do not try to render zero-sized surfaces

* reverse if

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-16 18:03:57 +01:00
Emil Ernerfeldt
9268f43896
eframe: make RequestRepaintEvent into an enum UserEvent (#2311)
Preparation for https://github.com/emilk/egui/pull/2294
to make that a smaller diff.
2022-11-16 12:17:41 +01:00
Robert Walter
0ff1ee3893
feat(combobox): implement text wrap for selected text (#2272)
* feat(combobox): implement text wrap for selected text

* chore(changelog): add line to changelog

* feat(combobox-text-wrap): make wrap boolean

- specifying a wrap width didn't really make sense so now it's boolean
- the selected text will now use the maximum available width while still
  respecting the spacing and icon coming after it

* feat(combobox-text-wrap): update changelog
2022-11-16 11:57:27 +01:00
Arshia Soleimani
0ba04184d5
Improve mouse selection accuracy (#2304)
* updated

* Update crates/egui/src/context.rs

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

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-16 11:26:02 +01:00
Matt Fellenz
690dc2d2e8
Add 'none' alias to Sense::hover (#2306) 2022-11-14 11:44:46 +01:00
Emil Ernerfeldt
ef931c406c
Add Window::pivot and position combo boxes better (#2303)
* Paint ComboBox icon differently if opening upwards

* Add Area::pivot and Window::pivot

* Add Window::contrain

* ComboBox: pop up above if it doesn't fit below

* Add line to changelog
2022-11-13 22:17:33 +01:00
LoganDark
f0f41d60e1
eframe: Don't show window until after initialization (#2279)
* Don't show window until after initialization

Shortens #1802, but does not completely solve it

* format code

* Present first frame immediately before showing window

This resolves the white flash almost completely, but is a hack. Window
visibility should be derived from the AppOutput, and the first frame
should not be painted before the event loop has processed initial
events.

Working on a better implementation.

* Integrate window showing with AppOutput

This allows an app to keep the window hidden (never shown) by calling
Frame.set_visible(false) on the first update. This includes a slightly
less nasty hack than the last commit did.

Also fixes an accidental cross-contamination of pull requests.

* fmt

* add comments

* add comments

* add comments

* add comments

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-13 20:30:52 +01:00
LoganDark
5bac853d9c
eframe: Repaint immediately on RepaintAsap, fixes #903 (#2280)
* eframe: Repaint immediately on RepaintAsap, fixes #903

This completely eliminates the white flickering seen on Windows when
rapidly resizing a window on the glow backend. The reason that happens
is because DWM only waits for the resize event to be delivered before
displaying the window at its new size. You must repaint synchronously
inside that iteration of the event loop or else you get flickering.

* Differentiate between RepaintAsap and RepaintNext

RepaintNext looks like it is indeed needed in at least one case instead
of RepaintAsap.

* Use RepaintNext in more situations

Starting to understand why this was the behavior. It looks like only a
few special cases should be given RepaintAsap, such as the window being
resized. All other cases should be RepaintNext, as it can wait.

Using RepaintAsap in all situations will cause things like lag when
changing a slider by keyboard with a high key repeat rate.

* Add explanatory comments

I am a total hypocrite for forgetting to add these.

* Rename RepaintAsap to RepaintNow

There is no notion of "possibility" here like there is when waiting for
RedrawEventsCleared. RepaintNow causes an immediate repaint no matter
what.

* Fix RepaintNow comment

"Delays" is ambiguous.
2022-11-13 20:30:39 +01:00
ItsEthra
f790e248e4
Fixed color edit popup going outside the screen (#2270)
* Fixed color edit popup going outside the screen

* Added option to constrain areas

* Constrain color picker area

* Constrain popups

* Updated changelog
2022-11-11 14:09:54 +01:00
Red Artist
8ff139687a
bump msrv 1.64 and tts + bindgen dep (#2274)
* bump msrv 1.64 and tts + bindgen dep

* Update crates/egui-winit/Cargo.toml

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

* Update crates/eframe/Cargo.toml

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

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-11 10:27:25 +01:00
Emil Ernerfeldt
e225c6b8d0 Opt-in logging of egui-wgpu using puffin 2022-11-10 16:28:08 +01:00
WesleyCh3n
b2edbe617e
Fixed Plot Line::fill does not fill last segment correctly (#2275)
* Fix missing vertex to fill the triangle

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-10 15:00:20 +01:00
Emil Ernerfeldt
b1e71d308f
Only show tooltips when mouse pointer is still (#2263)
* Only show tooltips when mouse pointer is still

Revert to the old behavior by setting
`style.interaction.show_tooltips_only_when_still = false`.

* Area: take `impl Into<Id>`

* refactor tooltips

* Fix was_tooltip_open_last_frame

* Bug fix

* Add some spacing between tooltips
2022-11-09 19:35:08 +01:00
Emil Ernerfeldt
51ff32797d
Give each tooltip area its own unique Id (#2264)
* Don't check for clicks on non-interactive areas

* Give each tooltip area its own unique Id
2022-11-08 11:04:54 +01:00
Emil Ernerfeldt
35213606c4 Bug fix: don't paint panel separation lines on top of everything else 2022-11-08 08:27:49 +01:00
Emil Ernerfeldt
5515d2db77 Add ui.centered 2022-11-08 01:07:51 +01:00
Emil Ernerfeldt
c3edc1b88e fix mistake added in #2261 2022-11-08 00:51:50 +01:00
Emil Ernerfeldt
4aacb4575b
Panel visual improvements (#2261)
* Remove stroke around panels and replace with separator single line

* Remove item_spacing between panels

* Update changelog
2022-11-08 00:34:31 +01:00
qthree
75a825e9a1
Fix ScrollArea::show_rows (#2258) 2022-11-07 23:26:01 +01:00
blusk
3805a3282f
feat: add a way to provide customable scaling to SVG rasterization (#2252) 2022-11-07 16:52:14 +01:00
Emil Ernerfeldt
b3ab47a594 eframe: warn if web_sys_unstable_apis was not set 2022-11-07 14:00:00 +01:00
Emil Ernerfeldt
d5eb8779cb
Allow overlapping interactive widgets (#2244)
* Turn off optimization for debug builds again

* Optimize rect_contains_pointer

* Fix for colorpicker: make popup immovable

* Area: interact first

* ScrollArea: do interaction first

* Window: shrink double-clickable area of titelbar

* Only the top-most (latest added) interactive widget gets `hovered=true`

* Add Frame::total_margin

* Update changelog

* Add debug-options to visualize what widgets cover which other widget
2022-11-07 12:44:35 +01:00
Emil Ernerfeldt
8c76b8caff
Update op puffin 0.14.0 (#2257) 2022-11-07 12:37:37 +01:00
Emil Ernerfeldt
eec18290a4
eframe: make sure to update native_pixels_per_point when dpi changes (#2256)
* eframe: make sure to update native_pixels_per_point when dpi changes

* Update changelog
2022-11-07 12:36:17 +01:00
axxop
940b896cbb
use RandomState::with_seeds replace AHasher::default (#2254)
* use `RandomState::with_seeds` replace `AHasher::default`

* remove history checkout

* update seeds to `1,2,3,4`
2022-11-07 10:53:07 +01:00
Emil Ernerfeldt
17501d7e3e
Update arboard, fixing copy-paste on X11 (#2238)
* Revert "Fix copy-paste on Windows (#2120)"

This reverts commit f61044cef7.

* Update arboard to 3.2
2022-11-07 09:54:42 +01:00
Max Wase
22a917c00a
Painter extend accepts IntoIter (#2249)
* `Painter` extend accepts `IntoIter`

* Update painter.rs
2022-11-07 09:32:28 +01:00
Emil Ernerfeldt
e48602059d
Update ahash to 0.8.1 (#2255) 2022-11-07 09:23:45 +01:00
Steven Casper
7434a7d7d5
Implement Debug for egui::Context (#2248)
* Implement Debug for Context

* Update changelog
2022-11-06 22:49:29 +01:00
Emil Ernerfeldt
a0b3f1126b
Add helpers for zooming an app using Ctrl+Plus and Ctrl+Minus (#2239)
* Using tracing-subscriber in hello_world example

* Add Key::Plus/Minus/Equals

* Warn if failing to guess OS from User-Agent

* Remove jitter when using Context::set_pixels_per_point

* Demo app: zoom in/out using ⌘+ and ⌘-

* Demo app: make backend panel GUI scale slider better

* Optimize debug builds a bit

* typo

* Update changelog

* Add helper module `egui::gui_zoom` for zooming an app

* Better names, and update changelog

* Combine Plus and Equals keys

* Last fix

* Fix docs
2022-11-05 11:18:13 +01:00
Mikhail Sheldyakov
25718f2774
fix comment for WebOptions follow_system_theme (#2233) 2022-11-05 09:20:53 +01:00
Clement Rey
fcb00723bc
wgpu: add support for user-level command buffers + viewport clarification (#2230)
* wgpu: add support for user-level command buffers
* updated wgpu demo app
2022-11-04 09:54:29 +01:00
ItsEthra
4d1e858a52
Use total_cmp for clamping drag value in order to avoid floating point ambiguities (#2213)
* Use total_cmp for clamping DragValue

* Added test for clamping

* Increase MSRV in all crates

* Increased rust version for github actions and lib.rs

* Inversed ranges are now working properply with clamp_to_range

* Added more tests
2022-11-02 19:38:39 +01:00
Emil Ernerfeldt
34e6e12f00
Specify deifferent minification and magnification filters (#2224)
* Specify deifferent minification and magnification filters

* Fixes

* Update changelogs

* Doctest fixes

* Add deprecation notice for RetainedImage::with_texture_filter
2022-11-02 17:54:06 +01:00
heretik31
8e79a5a8ae
Add egui_speedy2d link to README.md (#2211) 2022-11-01 21:24:12 +01:00
ItsEthra
76d0cf5034
Fixed datepicker not being marked as changed (#2208) 2022-10-31 21:40:49 +01:00
Antoine Marras
1fb19f08ce
Typo in egui, layout: alignmen => alignment (#2209) 2022-10-31 21:39:49 +01:00
Andreas Reich
4c82519fb8
configurable wgpu backend (#2207)
* introduce new wgpu configuration option to allow configuring wgpu renderer

* use new options with wgpu web painter

* use on_surface_error callback

* changelog update

* cleanup

* changelog and comment fixes
2022-10-31 17:57:32 +01:00
ItsEthra
f71cbc2475
Fixed menu popups going outside of the screen (#2191)
* Fixed menu popups going outside of the screen

* Made menu appear above above the button instead
2022-10-31 17:57:15 +01:00
Emil Ernerfeldt
02b9d2d082
Keyboard shortcut helpers (#2202)
* eframe web: Add WebInfo::user_agent

* Deprecate `Modifier::ALT_SHIFT`

* Add code for formatting Modifiers and Key

* Add type KeyboardShortcut

* Code cleanup

* Add Context::os/set_os to query/set what OS egui believes it is on

* Add Fonts::has_glyph(s)

* Add helper function for formatting keyboard shortcuts

* Faster code

* Add way to set a shortcut text on menu buttons

* Cleanup

* format_keyboard_shortcut -> format_shortcut

* Add TODO about supporting more keyboard sumbols

* Modifiers::plus

* Use the new keyboard shortcuts in emark editor demo

* Explain why ALT+SHIFT is a bad modifier combo

* Fix doctest
2022-10-31 12:58:26 +01:00
Mason Feurer
d97282cd92
Interact with non-Rect shapes (#2199)
* added Ui::interact_with_hovered

* fixed typo
2022-10-30 20:59:19 +01:00
Emil Ernerfeldt
2b1341095d
CI: Run cargo clippy for wasm32-unknown-unknown (#2200)
* CI: Run cargo clippy for wasm32-unknown-unknown

* wasm32 clippy fixes

* Document when AppRunner::new can fail
2022-10-30 20:55:07 +01:00
Andreas Reich
53b800502a
wgpu renderer no longer creates a sampler with every texture (#2198) 2022-10-30 15:14:45 +01:00
Emil Ernerfeldt
f7a15a34f9
Panel collapse/expansion animation (#2190)
* Add API for querying the size of a panel

* demo app: animate backend panel collapse

* Add helper function for animating panels

* More animation functions

* Add line to changelog
2022-10-28 11:51:56 +02:00
Emil Ernerfeldt
da96fcacd3 Improve panel sizing API 2022-10-28 10:16:02 +02:00
Lily M. Lyons
1fadc7396a
Add CollapsingHeader::show_unindented (#2154) 2022-10-24 17:10:13 +02:00
Emil Ernerfeldt
aebec6329a Use ⚠ instead of ‼ in "Debug build" warning
The latter is not supported by the default fonts
2022-10-24 15:00:50 +02:00
Emil Ernerfeldt
7b8c17042c eframe: Make fullsize_content mac-only 2022-10-21 08:55:56 +02:00
Paul Hazen
5e44c13b48
Allows creating a ColorImage struct without an alpha channel (#2167)
* Added functionality to image.rs that allows for creating an image from rgb instead of just rgba

* remove "unmultiplied"

* remove "unmultiplied"

* rgba -> rgb

Co-authored-by: Mingun <Alexander_Sergey@mail.ru>

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Mingun <Alexander_Sergey@mail.ru>
2022-10-19 15:01:53 +02:00
Clement Rey
3d36a20376
table: fix deadlocks caused by lock fairness (#2156) 2022-10-16 20:10:48 +02:00
Andreas Reich
944159d514
using a shared vertex & index buffer in wgpu renderer (#2148)
* using a shared vertex & index buffer in wgpu renderer
capacity each doubles when exceeded.
This change means a lot less allocation during egui's lifetime.

* changelog update

* minor code cleanup and changelog fix

* fix linter issue
2022-10-14 10:53:19 +02:00
Parth
02d1e7492a
fix typo in text_layout_types (#2150) 2022-10-14 08:03:47 +02:00
Andrew Langmeier
a1f6f68213
Allow for changing of plot bounds from PlotUI (#2145) 2022-10-13 08:00:11 +02:00
Nicolas Musset
ae5294e63c
Use WidgetText for the slider's text (#2143) 2022-10-12 17:20:32 +02:00
Andreas Reich
c414af7aa2
wgpu renderer now always requires a RenderPass being passed in, pass command encoder to prepare callback (#2136)
* wgpu renderer now always requires a RenderPass being passed in
This also implies that it no longer owns the depth buffer! (why would it anyways!)

* wgpu-renderer now passes a command encoder to prepare

* add changelog entries

* fixup changelogs, fix variable name
2022-10-12 14:27:24 +02:00
Emil Ernerfeldt
7803285221 Remove Debug trait constraint on Id sources 2022-10-10 22:34:08 +02:00
Clement Rey
367378d75d
Fix false positives wrt RwLock deadlock detection (#2121)
* add a test suite demonstrating the issue

* fix false positives in RwLock, thus passing the new test suite

* friendler output

* augmented test suite with RWlock specifics (failing as expected!)

* full support for RWLocks

* implement support for guard remappings

* Add some newlines

* join `use` statements

* pass cranky

* addressing PR comments

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-10-10 12:07:44 +02:00
Emil Ernerfeldt
f4d2aa5b4a
Only call the request_repaint_callback at most once per frame (#2126)
egui-winit adds new repaint events to the event loop on each call, and on some platforms this becomes very expensive.
2022-10-10 10:47:20 +02:00
Andrew Langmeier
0fe0c8115c
Add ability to control double click reset in plot widget (#2115)
* Add ability to control double click reset in plot widget

* improve docstring

* small optimization

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-10-10 09:57:25 +02:00
Parth
510aa1710e
fix typo in architecture.md (#2123) 2022-10-10 09:42:50 +02:00
Emil Ernerfeldt
eba3927ace
Remove .with_srgb(false) when constructing window (#2110) 2022-10-08 11:43:48 +02:00
Emil Ernerfeldt
f61044cef7
Fix copy-paste on Windows (#2120)
Closes https://github.com/emilk/egui/issues/2109

Since arboard 3.0 you must absolutely not hold onto `arboard::Clipbaord`
longer than you are using it.

See https://github.com/1Password/arboard/issues/84
2022-10-07 14:46:42 +02:00
Andreas Reich
cd74c74f6f
Update to wgpu 0.14 (#2114) 2022-10-06 15:50:46 +02:00
Emil Ernerfeldt
3ec170cc1e Tweak warning color in bright mode 2022-10-06 11:49:12 +02:00
Emil Ernerfeldt
4dd5ffa254 Fix: make ScrollAreaOutput public 2022-10-06 09:03:18 +02:00
Andreas Reich
c2a37f4bd8
eframe support for wgpu on the web (#2107)
* basic working wgpu @ webgl on websys

* fix glow compile error

* introduced WebPainter trait, provide wgpu renderstate

* WebPainterWgpu destroy implemented

* make custom3d demo work on wgpu backend

* changelog entry for wgpu support eframe wasm

* remove temporary logging hack

* stop using pollster for web
we're actually not allowed to block - this only worked because wgpu on webgl doesn't actually cause anything blocking. However, when trying webgpu this became an issue

* revert cargo update

* compile error if neither glow nor wgpu features are enabled

* code cleanup

* Error handling

* Update changelog with link

* Make sure --all-features work

* Select best framebuffer format from the available ones

* update to wasm-bindgen 0.2.83

* Fix typo

* Clean up Cargo.toml

* Log about using the wgpu painter

* fixup wgpu labels

* fix custom3d_wgpu_shader ub padding

* remove duplicated uniforms struct in wgsl shader for custom3d

* Update docs: add async/await to the web 'start' function

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-10-05 20:14:18 +02:00
Emil Ernerfeldt
c441f33ecf Cargo deny: also check wasm32-unknown-unknown 2022-10-03 10:37:50 +02:00
Emil Ernerfeldt
dd67f3631f Add Painter::image convenience function 2022-10-03 10:26:19 +02:00
Emil Ernerfeldt
77df8ca0b0
Update some crates (#2100)
* Update crate webbrowser 0.7 -> 0.8

* Update criterion 0.3 -> 0.4

* Update tts 0.20 -> 0.24

* revert tts update
2022-10-02 20:33:45 +02:00
Tobias Schmitz
981fdb3932
fix: default to changing y axis when changing data aspect (#2087) 2022-10-02 14:32:37 +02:00
Paul Rouget
aa1536f1a7
Make Layout properties public (#2096) 2022-10-02 08:51:29 +02:00
Michael Galos
9901ad4bb0
eframe: Mouse passthrough option (#2080) 2022-10-02 08:45:37 +02:00
Zoxc
53ff83737b
Plot: Add auto_bounds_x, auto_bounds_y and reset (#2029)
* Make double click reset to the initial view

* Allow forcing auto bounds

* Allow plots to be reset

* Rename `modified` to `bounds_modified`
2022-10-02 08:44:59 +02:00
Caleb Leinz (he/him)
5e91a3033d
Added ability to set color of spinner (#2088) 2022-10-01 15:18:44 +02:00
Paul Rouget
2dca01ecd5
Bug Fix: Allocate Frame outer margins (#2089) 2022-10-01 14:18:15 +02:00
Emil Ernerfeldt
2ee9ab3151 Added link to egui-glutin-gl 2022-09-27 23:08:43 +02:00
Emil Ernerfeldt
91b2d5da6f Fix typo in wgpu warning message 2022-09-25 16:46:40 +02:00
Emil Ernerfeldt
4ac1e28eae
Improve text redering and do all color operation in gamma space (#2071) 2022-09-24 17:53:11 +02:00
Emil Ernerfeldt
29fa63317e
Fix text sizes being too small (#2069)
Closes https://github.com/emilk/egui/issues/2068

Before this PR, the default font, Ubuntu-Light, was ~11% smaller
than it should have been, and the default monospace font, Hack,
was ~14% smaller. This means that setting the font size `12` in egui
would yield smaller text than using that font size in any other app.
Ooops!

The change is that this PR now takes into account the ttf properties
`units_per_em` and `height_unscaled`.

If your egui application has specified you own font sizes or text styles
you will see the text in your application grow
larger, unless you go in and compensate by dividing all font sizes by
~1.21 for Ubuntu-Light/Proportional and ~1.16 for Hack/Monospace,
and with something else if you are using a custom font!
This effects any use of `FontId`, `RichText::size`, etc.

This PR changes the default `Style::text_styles` to compensate,
so the default egui style should look the same before and after this PR.
2022-09-21 21:31:08 +02:00
Emil Ernerfeldt
990a8c8b44
Fix sRGB blending and cleanup the relevant code (#2070)
* Fix sRGBA blending on Mac

* Clean up handling of sRGB support

* Assume sRGB support if any extension has sRGB in it

* improve logging very slightly
2022-09-21 21:30:02 +02:00
setzer22
4aae638e15
Change several methods from pub(crate) to pub (#2051)
* Change several methods from pub(crate) to pub

* document next_auto_id

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-21 20:08:26 +02:00
Emil Ernerfeldt
12dc61ed0f
update arboard to 3.0 (#2067) 2022-09-21 09:14:55 +02:00
Emil Ernerfeldt
2b0bf82b51
Make eframe::App::as_any_mut optional to implement (#2061) 2022-09-20 13:58:55 +02:00
Chris Hamons
311eb66cae
Add spacing.menu_margin for customizing menu spacing (#2036)
- Fixes https://github.com/emilk/egui/discussions/287
2022-09-17 11:29:19 +02:00
Adia Robbie
bc6a823103
eframe: Add center to NativeOptions and monitor_size to WindowInfo (#2035)
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-17 11:20:40 +02:00
Paul Rouget
c4117066cf
MacOS: Support fullsize content (no titlebar, but window controls) (#2049)
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-16 14:31:21 +02:00
Sheldon Nico
0605bcfca7
Add IME support for native (#2046) 2022-09-15 17:21:43 +02:00
Emil Ernerfeldt
c5495d69fb
egui-wgpu: Fix crash on zero-sized scissor rects (#2039) 2022-09-13 09:32:05 +02:00
Zoxc
2842d390c9
Allow box zoom in any direction (#2028) 2022-09-09 08:30:14 +02:00
Stanislav
ebc4fc866d
eframe web: access app from WebHandle (#1886)
Co-authored-by: Stanislav <enomado@users.noreply.github.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-09 08:22:46 +02:00
Emil Ernerfeldt
e0c7533ede eframe native: Make sure we only shut down once
I don't trust winit to actually exit on ControlFlow::Exit

Perhaps this will solve https://github.com/emilk/egui/issues/2027
2022-09-08 10:50:35 +02:00
Emil Ernerfeldt
1fe08bf7e4 eframe: reduce repaint log level from debug to trace 2022-09-07 16:27:53 +02:00
Luis Wirth
4b6826575e
egui-wgpu renderer renaming (#2021)
- `RenderPass` -> `Renderer`
- `RenderPass::execute` -> `Renderer::render`
- `RenderPass::execute_with_renderpass` -> `Renderer::render_onto_renderpass`
- reexport `Renderer` in `lib.rs`
2022-09-07 14:20:21 +02:00
bigfarts
0e62c0e50b
Improve mixed CJK/Latin linebreaking. (#1986) 2022-09-06 22:20:17 +02:00
Emil Ernerfeldt
5500895845
eframe: make sure we save app state on Cmd-Q (#2013)
Cmd-Q emits a `winit::event::Event::LoopDestroyed` and then the app closes, without `run_return` returning (despite its name).
2022-09-06 16:07:58 +02:00
Adia Robbie
eeb97fa4ce
Check srgb_support on other GL | ES as well. (#2012) 2022-09-06 15:58:22 +02:00
Romet Tagobert
64aac30f36
Add depth buffer support for egui-wgpu's render pass (#2002)
* add a depth texture for wgpu callbacks

* egui-wgpu: use depth from native_options

* add wgpu caveat to depth_buffer docstring
2022-09-06 14:37:07 +02:00
ComLarsic
162210f90e
Add sdl2_egui_platform to the list of integrations (#2011) 2022-09-06 14:28:59 +02:00
Adia Robbie
b43a8626cf
Add custom shader_version on glow renderer (#1993)
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-06 10:08:16 +02:00
Luke Jones
7fae634dc3
egui_glow: default to RGBA8 if sRGB not supported (#2007)
The Vivante GPU on many STM32MP1 based boards does not support sRGB
as an internal format.

Introduce a check for sRGB support and default to `RGBA8` internal format
if not supported.

Additionally the STM32MP1 needs to be checked for `GL_ARB_vertex_array_object`

Closes #1991
2022-09-06 10:07:49 +02:00
Emil Ernerfeldt
1d5f20b46c Improve docstrings
Closes https://github.com/emilk/egui/issues/2001
2022-09-05 12:05:29 +02:00
njust
124ed5e4ba
Fixed typo (#1990)
* Fixed typo

* fix grammar too

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-09-05 12:02:31 +02:00
RadonCoding
613a664c45
Fixed a typo (#1985)
custom_window_frame was spelled as custon_window_frame
2022-08-30 10:12:52 +02:00
Koute
be9f363c53
egui_winit: support winit with default features disabled (#1971)
* egui_winit: support winit with default features disabled

* Link to the PR in the changelog
2022-08-30 08:49:32 +02:00
Joshua Batty
153524023f
egui-wgpu: port over missing update_wgpu_texture methods from egui_wgpu_backend (#1981)
* add back missing update_egui_texture_from_wgpu_texture methods to renderer module

* clippy
2022-08-29 14:09:34 +02:00
Emil Ernerfeldt
ac4d75e9b4
egui-winit: don't repaint when just moving window (#1980)
* Bug fix: reset repaint countdown when we pass it

* eframe: debug-print what winit event caused a repaint

* egui-winit: don't repaint when only moving window

* fix docstring
2022-08-29 11:20:19 +02:00
Adam Gąsior
9b2c3d1026
Add custom parser to DragValue and Slider, with additional support for binary, octal, and hexadecimal numbers. (#1967)
* Add `custom_parser` to `DragValue`

* Add `custom_parser` to `Slider`

* Add `binary_u64`, `octal_u64`, and `hexadecimal_u64` to `DragValue`

* Add `binary_u64`, `octal_u64`, and `hexadecimal_u64` to `Slider`

* Fix formatting and errors in docs

* Update CHANGELOG.md

* Fix CI errors

* Replace manual number parsing with i64::from_str_radix. Add support for signed integers.

* Update CHANGELOG.md

* Change documentation.

* Fix documentation.

* Fix documentation.

* Remove unnecessary links.
2022-08-28 18:16:12 +02:00
Emil Ernerfeldt
3142c52b94
Minor wgpu-web releated stuff (#1977)
* egui_demo_app: remove wgpu and pollster as direct dependencies

* eframe: use same web-sys version as wgpu crate

* Make note that web_sys_unstable_apis is required by the wgpu crate

* Rename the glow web painter in eframe

* Remove trait DummyWebGLConstructor from web_glow_painter.rs

* cargo fmt

* Fix check.sh
2022-08-28 10:58:58 +02:00
njust
dbfaa4527b
egui_extras: Add Table::vertical_scroll_offset (#1946)
* egui_extras: Add Table::vertical_scroll_offset

* Added example for TableBuilder::vertical_scroll_offset

* Format code

* Add link to PR in the changelog

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-08-28 09:37:23 +02:00
MaximOsipenko
af63101fdc
eliminate some possible deadlocks (#1968) 2022-08-28 09:36:23 +02:00
Zoxc
9be060fe69
Allow cursors to be linked for plots (#1722)
* Allow cursors to be linked

* Link cursors in demo

* Refactor cursor memory to deal with removal of plots

* Refactor PlotItem::on_hover to produce a list of cursors

* Use a separate `LinkedCursorsGroup` type to link cursors.

* Refactor `Cursor`.

* Inline `push_argument_ruler` and `push_value_ruler`.

* Update documentation

* Update doc reference
2022-08-28 09:36:08 +02:00
Emil Ernerfeldt
b978b06159
Simplify, clean up and speed up CI (#1970)
Use https://github.com/Swatinem/rust-cache
2022-08-27 09:54:47 +02:00
Tomáš Král
f3f6946eb2
egui_glium correct texture filtering (#1962)
* correct texture filter in egui_glium

* update example

* cargo fmt

* rename UserTexture
2022-08-25 08:05:58 +02:00
Emil Ernerfeldt
d5933daee5 egui_glow: allow empty (zero-sized) textures
See https://github.com/emilk/egui/issues/1960
2022-08-24 14:34:34 +02:00
Robert Bragg
fb92434aac
Android support for EFrame (#1952)
* eframe: allow hooking into EventLoop building

This enables native applications to add an `event_loop_builder` callback
to the `NativeOptions` struct that lets them modify the Winit
`EventLoopBuilder` before the final `EventLoop` is built and run.

This makes it practical for applications to change platform
specific config options that Egui doesn't need to be directly aware of.

For example the `android-activity` glue crate that supports writing
Android applications in Rust requires that the Winit event loop be
passed a reference to the `AndroidApp` that is given to the
`android_main` entrypoint for the application.

Since the `AndroidApp` itself is abstracted by Winit then there's no
real need for Egui/EFrame to have a dependency on the `android-activity`
crate just for the sake of associating this state with the event loop.

Addresses: #1951

* eframe: defer graphics state initialization until app Resumed

Conceptually the Winit `Resumed` event signifies that the application is
ready to run and render and since
https://github.com/rust-windowing/winit/pull/2331 all platforms now
consistently emit a Resumed event that can be used to initialize
graphics state for an application.

On Android in particular it's important to wait until the application
has Resumed before initializing graphics state since it won't have an
associated SurfaceView while paused.

Without this change then Android applications are likely to just show
a black screen.

This updates the Wgpu+Winit and Glow+Winit integration for eframe but
it's worth noting that the Glow integration is still not able to fully
support suspend and resume on Android due to limitations with Glutin's
API that mean we can't destroy and create a Window without also
destroying the GL context, and therefore (practically) the entire
application.

There is a plan (and progress on) to improve the Glutin API here:
https://github.com/rust-windowing/glutin/pull/1435 and with that change
it should be an incremental change to enable Android suspend/resume
support with Glow later.

In the mean time the Glow changes keep the implementation consistent
with the wgpu integration and it should now at least be possible to
start an Android application - even though it won't be able to suspend
correctly.

Fixes #1951
2022-08-23 14:43:22 +02:00
Zoxc
b9bb7baaa7
Only require something GLES 3 compatible from wgpu (#1956) 2022-08-23 14:39:01 +02:00
Daniel Buch Hansen
4629f2cd12
Example: three-d downgrade depth_buffer to 24 bit (#1950)
Closes: #1949
2022-08-21 19:23:34 +02:00
Emil Ernerfeldt
8797c02ed9 Publish new web demo that works on Es100 2022-08-20 19:55:54 +02:00
Emil Ernerfeldt
bd5250f85d
Disable custom 3D painting example in the egui demo app for Es100 (#1945)
Closes https://github.com/emilk/egui/issues/1944
2022-08-20 19:54:18 +02:00
Emil Ernerfeldt
97ce103209 Release 0.19.0 - wgpu backend, repaint_after, continue-after-close 2022-08-20 16:49:58 +02:00
Emil Ernerfeldt
13f5d62b4b Commit a new web demo 2022-08-20 16:37:59 +02:00
Emil Ernerfeldt
127931ba45
eframe: rename quit/exit to "close" (#1943)
Since https://github.com/emilk/egui/pull/1919 we can continue
the application after closing the native window. It therefore makes
more sense to call `frame.close()` to close the native window,
instead of `frame.quit()`.
2022-08-20 16:08:59 +02:00
Emil Ernerfeldt
2453756782 Fix broken GitHub source links due to #1940 2022-08-20 15:18:02 +02:00
Emil Ernerfeldt
8737933101 Improve documentation and logging 2022-08-20 15:06:43 +02:00
Emil Ernerfeldt
725aa5277b Update crate rfd to 0.10 (in file_dialog example)
Also update ab_glyph from 0.2.15 to 0.2.16
2022-08-20 14:42:37 +02:00
Emil Ernerfeldt
eb10ef94f7 Build-fix for --no-default-features 2022-08-20 14:40:33 +02:00
Emil Ernerfeldt
f4cc1c5465
eframe: Don't follow system theme by default (#1941)
I have gone back and forth on this a bit, but I think the arguments
AGAINST following the system theme are many:

* `dark-light` is a big dependency with problems on Linux.
* Many people prefer the dark mode and ask how to set it as the default
  (even though they are using light mode in their OS).
* A developer may be surprised when the app changes theme when
  they run it on another computer.

So, the path of least surprise is to make this an opt-in feature
with dark mode as the default mode.

On native, you add the `dark-light` feature to enable it.
On web, you set `WebOptions::follow_system_theme`.
2022-08-20 11:11:07 +02:00
Emil Ernerfeldt
40e440b2f7 Update changelog with recent PR merges 2022-08-20 10:42:31 +02:00
Emil Ernerfeldt
041f2e64ba
Move all crates into a crates directory (#1940) 2022-08-20 10:41:49 +02:00
366 changed files with 18865 additions and 11207 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

@ -8,7 +8,7 @@ assignees: ''
--- ---
<!-- <!--
First look if there is already a similar bug report. If there is, add a comment to it instead! First look if there is already a similar bug report. If there is, upvote the issue with 👍
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml: Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:

View file

@ -8,7 +8,7 @@ assignees: ''
--- ---
<!-- <!--
First look if there is already a similar feature request. If there is, add a comment to it instead! First look if there is already a similar feature request. If there is, upvote the issue with 👍
--> -->

View file

@ -3,210 +3,209 @@ on: [push, pull_request]
name: CI name: CI
env: env:
# This is required to enable the web_sys clipboard API which eframe web uses # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
# as well as by the wasm32-backend of the wgpu crate.
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings
RUSTDOCFLAGS: -D warnings RUSTDOCFLAGS: -D warnings
jobs: jobs:
check_default: fmt-crank-check-test:
name: cargo check (default features) name: Format + check + test
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: default
toolchain: 1.61.0 toolchain: 1.65.0
override: true override: true
- name: Install packages (Linux) - name: Install packages (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev #uses: awalsh128/cache-apt-pkgs-action@v1.2.2
- uses: actions-rs/cargo@v1 #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: with:
command: check packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
args: --locked version: 1.0
execute_install_scripts: true
check_all_features: - name: Set up cargo cache
name: cargo check --all-features uses: Swatinem/rust-cache@v2
strategy:
matrix:
os: [ubuntu-20.04, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
check_web_default: - name: Rustfmt
name: cargo check web (default features) uses: actions-rs/cargo@v1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
check_egui_demo_app:
name: cargo check -p egui_demo_app
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- name: check
run: cargo check -p egui_demo_app
check_wasm_eframe_with_features:
name: cargo check wasm eframe
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- name: check
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
check_web_all_features:
name: cargo check web --all-features
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
test:
name: cargo test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.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: actions-rs/cargo@v1
with:
command: test
args: --all-features
fmt:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with: with:
command: fmt command: fmt
args: --all -- --check args: --all -- --check
cranky: - name: Install cargo-cranky
name: cargo cranky uses: baptiste0928/cargo-install@v1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with: with:
profile: minimal crate: cargo-cranky
toolchain: 1.61.0
override: true - name: check --all-features
- run: cargo install cargo-cranky uses: actions-rs/cargo@v1
- name: Install packages (Linux) with:
if: runner.os == 'Linux' command: check
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev # libgtk-3-dev is used by rfd args: --locked --all-features --all-targets
- uses: actions-rs/cargo@v1
- name: check default features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --all-targets
- name: check --no-default-features
uses: actions-rs/cargo@v1
with:
command: check
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: with:
command: cranky command: cranky
args: --workspace --all-targets --all-features -- -D warnings args: --all-targets --all-features -- -D warnings
doc: # ---------------------------------------------------------------------------
name: cargo doc
runs-on: ubuntu-latest check_wasm:
name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.65.0
target: wasm32-unknown-unknown
override: true override: true
- name: Install packages (Linux)
if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install libgtk-3-dev
run: sudo apt-get update && sudo apt-get install libspeechd-dev
- run: cargo doc --lib --no-deps --all-features - name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Install cargo-cranky
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-cranky
- name: Check wasm32 egui_demo_app
uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: Check wasm32 egui_demo_app --all-features
uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: Check wasm32 eframe
uses: actions-rs/cargo@v1
with:
command: check
args: -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.84"
- run: ./sh/wasm_bindgen_check.sh --skip-setup
- name: Cranky wasm32
uses: actions-rs/cargo@v1
with:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
# ---------------------------------------------------------------------------
cargo-deny: cargo-deny:
runs-on: ubuntu-20.04 name: cargo deny
runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
wasm_bindgen:
name: wasm-bindgen
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with: with:
profile: minimal rust-version: "1.65.0"
toolchain: 1.61.0
target: wasm32-unknown-unknown # ---------------------------------------------------------------------------
override: true
- run: ./sh/setup_web.sh
- run: ./sh/wasm_bindgen_check.sh
android: android:
name: android name: android
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.65.0
target: aarch64-linux-android
override: true override: true
- run: cargo check --features wgpu --target aarch64-linux-android - name: Set up cargo cache
working-directory: eframe 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
**/target_ra
**/target_wasm
/.*.json /.*.json
/.vscode /.vscode
/media/* /media/*
.DS_Store

29
.vscode/settings.json vendored
View file

@ -1,5 +1,32 @@
{ {
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"editor.formatOnSave": 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

@ -1,4 +1,4 @@
# Arcitecture # Architecture
This document describes how the crates that make up egui are all connected. This document describes how the crates that make up egui are all connected.
Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) for what to do before opening a PR. Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) for what to do before opening a PR.

View file

@ -1,10 +1,116 @@
# egui changelog # egui changelog
All notable changes to the `egui` crate will be documented in this file. All notable changes to the `egui` crate will be documented in this file.
NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.md), [`egui_glium`](egui_glium/CHANGELOG.md), and [`egui_glow`](egui_glow/CHANGELOG.md) have their own changelogs! NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
## Unreleased ## 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 🐛
* 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
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)).
### Added ⭐
* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)).
* Added `Context::os/Context::set_os` to query/set what operating system egui believes it is running on ([#2202](https://github.com/emilk/egui/pull/2202)).
* Added `Button::shortcut_text` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
* Added `egui::KeyboardShortcut` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
* Texture loading now takes a `TexureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)).
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
* Added `spacing.menu_margin` for customizing menu spacing ([#2036](https://github.com/emilk/egui/pull/2036))
* Added possibility to enable text wrap for the selected text of `egui::ComboBox` ([#2272](https://github.com/emilk/egui/pull/2272))
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds ([#2270](https://github.com/emilk/egui/pull/2270)).
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position ([#2303](https://github.com/emilk/egui/pull/2303)).
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
* Added `panel_fill`, `window_fill` and `window_stroke` to `Visuals` for your theming pleasure ([#2406](https://github.com/emilk/egui/pull/2406)).
* Plots:
* Allow linking plot cursors ([#1722](https://github.com/emilk/egui/pull/1722)).
* Added `Plot::auto_bounds_x/y` and `Plot::reset` ([#2029](https://github.com/emilk/egui/pull/2029)).
* Added `PlotUi::translate_bounds` ([#2145](https://github.com/emilk/egui/pull/2145)).
* Added `PlotUi::set_plot_bounds` ([#2320](https://github.com/emilk/egui/pull/2320)).
* Added `PlotUi::plot_secondary_clicked` ([#2318](https://github.com/emilk/egui/pull/2318)).
### Changed 🔧
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
* Make it slightly easier to click buttons ([#2304](https://github.com/emilk/egui/pull/2304)).
* `egui::color` has been renamed `egui::ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)).
### Fixed 🐛
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
* Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)).
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
* Constrain menu popups to the screen ([#2191](https://github.com/emilk/egui/pull/2191)).
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
* Fixed popups and color edit going outside the screen.
* Fixed keyboard support in `DragValue` ([#2342](https://github.com/emilk/egui/pull/2342)).
* If you nest `ScrollAreas` inside each other, the inner area will now move its scroll bar so it is always visible ([#2371](https://github.com/emilk/egui/pull/2371)).
* Ignore key-repeats for `input.key_pressed` ([#2334](https://github.com/emilk/egui/pull/2334), [#2389](https://github.com/emilk/egui/pull/2389)).
* Fixed issue with calling `set_pixels_per_point` each frame ([#2352](https://github.com/emilk/egui/pull/2352)).
* Fix bug in `ScrollArea::show_rows` ([#2258](https://github.com/emilk/egui/pull/2258)).
* Fix bug in `plot::Line::fill` ([#2275](https://github.com/emilk/egui/pull/2275)).
* Only emit `changed` events in `radio_value` and `selectable_value` if the value actually changed ([#2343](https://github.com/emilk/egui/pull/2343)).
* Fixed sizing bug in `Grid` ([#2384](https://github.com/emilk/egui/pull/2384)).
* `ComboBox::width` now correctly sets the outer width ([#2406](https://github.com/emilk/egui/pull/2406)).
## 0.19.0 - 2022-08-20
### Added ⭐ ### Added ⭐
* Added `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)). * Added `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)).
* Added `PointerButton::Extra1` and `PointerButton::Extra2` ([#1592](https://github.com/emilk/egui/pull/1592)). * Added `PointerButton::Extra1` and `PointerButton::Extra2` ([#1592](https://github.com/emilk/egui/pull/1592)).
@ -15,17 +121,20 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)). * You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
* Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)). * Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)).
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)). * Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)).
* Added `Contex::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)). * Added `Context::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)). * `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
* Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)).
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)). * Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
* Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)). * Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)).
* Added `custom_formatter`, `binary`, `octal`, and `hexadecimal` to `DragValue` and `Slider` ([#1953](https://github.com/emilk/egui/issues/1953))
### Changed ### Changed 🔧
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* `PaintCallback` shapes now require the whole callback to be put in an `Arc<dyn Any>` with the value being a backend-specific callback type ([#1684](https://github.com/emilk/egui/pull/1684)). * `PaintCallback` shapes now require the whole callback to be put in an `Arc<dyn Any>` with the value being a backend-specific callback type ([#1684](https://github.com/emilk/egui/pull/1684)).
* Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode ([#1694](https://github.com/emilk/egui/pull/1694)). * Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode ([#1694](https://github.com/emilk/egui/pull/1694)).
* `Layout::left_to_right` and `Layout::right_to_left` now takes the vertical align as an argument. Previous default was `Align::Center`. * `Layout::left_to_right` and `Layout::right_to_left` now takes the vertical align as an argument. Previous default was `Align::Center`.
* Improved ergonomics of adding plot items. All plot items that take a series of 2D coordinates can now be created directly from `Vec<[f64; 2]>`. The `Value` and `Values` types were removed in favor of `PlotPoint` and `PlotPoints` respectively. * Improved ergonomics of adding plot items. All plot items that take a series of 2D coordinates can now be created directly from `Vec<[f64; 2]>`. The `Value` and `Values` types were removed in favor of `PlotPoint` and `PlotPoints` respectively ([#1816](https://github.com/emilk/egui/pull/1816)).
* `TextBuffer` no longer needs to implement `AsRef<str>` ([#1824](https://github.com/emilk/egui/pull/1824)).
### Fixed 🐛 ### Fixed 🐛
* Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)). * Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)).
@ -33,6 +142,8 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
* Fixed `Plot` auto-bounds bug ([#1599](https://github.com/emilk/egui/pull/1599)). * Fixed `Plot` auto-bounds bug ([#1599](https://github.com/emilk/egui/pull/1599)).
* Fixed dead-lock when alt-tabbing while also showing a tooltip ([#1618](https://github.com/emilk/egui/pull/1618)). * Fixed dead-lock when alt-tabbing while also showing a tooltip ([#1618](https://github.com/emilk/egui/pull/1618)).
* Fixed `ScrollArea` scrolling when editing an unrelated `TextEdit` ([#1779](https://github.com/emilk/egui/pull/1779)). * Fixed `ScrollArea` scrolling when editing an unrelated `TextEdit` ([#1779](https://github.com/emilk/egui/pull/1779)).
* Fixed `Slider` not always generating events on change ([#1854](https://github.com/emilk/egui/pull/1854)).
* Fixed jitter of anchored windows for the first frame ([#1856](https://github.com/emilk/egui/pull/1856)).
* Fixed focus behavior when pressing Tab in a UI with no focused widget ([#1861](https://github.com/emilk/egui/pull/1861)). * Fixed focus behavior when pressing Tab in a UI with no focused widget ([#1861](https://github.com/emilk/egui/pull/1861)).
* Fixed automatic plot bounds ([#1865](https://github.com/emilk/egui/pull/1865)). * Fixed automatic plot bounds ([#1865](https://github.com/emilk/egui/pull/1865)).

2568
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,20 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"egui_demo_app", "crates/ecolor",
"egui_demo_lib", "crates/egui_demo_app",
"egui_extras", "crates/egui_demo_lib",
"egui_glium", "crates/egui_extras",
"egui_glow", "crates/egui_glow",
"egui-wgpu", "crates/egui-wgpu",
"egui-winit", "crates/egui-winit",
"egui", "crates/egui",
"emath", "crates/emath",
"epaint", "crates/epaint",
"examples/*", "examples/*",
] ]
[profile.dev]
split-debuginfo = "unpacked" # faster debug builds on mac
[profile.release] [profile.release]
# lto = true # VERY slightly smaller wasm # lto = true # VERY slightly smaller wasm
# opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3` # opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3`
@ -26,3 +23,13 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
# opt-level = 3 # unecessarily large wasm for no performance gain # opt-level = 3 # unecessarily large wasm for no performance gain
# debug = true # include debug symbols, useful when profiling wasm # debug = true # include debug symbols, useful when profiling wasm
[profile.dev]
# 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):
[profile.dev.package."*"]
opt-level = 2

View file

@ -1,19 +1,21 @@
# https://github.com/ericseppanen/cargo-cranky # https://github.com/ericseppanen/cargo-cranky
# cargo install cargo-cranky && cargo cranky # cargo install cargo-cranky && cargo cranky
deny = [ deny = ["unsafe_code"]
"unsafe_code",
]
warn = [ warn = [
"clippy::all", "clippy::all",
"clippy::await_holding_lock", "clippy::await_holding_lock",
"clippy::bool_to_int_with_if",
"clippy::char_lit_as_u8", "clippy::char_lit_as_u8",
"clippy::checked_conversions", "clippy::checked_conversions",
"clippy::cloned_instead_of_copied",
"clippy::dbg_macro", "clippy::dbg_macro",
"clippy::debug_assert_with_mut_call", "clippy::debug_assert_with_mut_call",
"clippy::derive_partial_eq_without_eq",
"clippy::disallowed_methods", "clippy::disallowed_methods",
"clippy::disallowed_script_idents", "clippy::disallowed_script_idents",
"clippy::doc_link_with_quotes",
"clippy::doc_markdown", "clippy::doc_markdown",
"clippy::empty_enum", "clippy::empty_enum",
"clippy::enum_glob_use", "clippy::enum_glob_use",
@ -37,6 +39,8 @@ warn = [
"clippy::inefficient_to_string", "clippy::inefficient_to_string",
"clippy::invalid_upcast_comparisons", "clippy::invalid_upcast_comparisons",
"clippy::iter_not_returning_iterator", "clippy::iter_not_returning_iterator",
"clippy::iter_on_empty_collections",
"clippy::iter_on_single_items",
"clippy::large_digit_groups", "clippy::large_digit_groups",
"clippy::large_stack_arrays", "clippy::large_stack_arrays",
"clippy::large_types_passed_by_value", "clippy::large_types_passed_by_value",
@ -45,7 +49,9 @@ warn = [
"clippy::lossy_float_literal", "clippy::lossy_float_literal",
"clippy::macro_use_imports", "clippy::macro_use_imports",
"clippy::manual_assert", "clippy::manual_assert",
"clippy::manual_instant_elapsed",
"clippy::manual_ok_or", "clippy::manual_ok_or",
"clippy::manual_string_new",
"clippy::map_err_ignore", "clippy::map_err_ignore",
"clippy::map_flatten", "clippy::map_flatten",
"clippy::map_unwrap_or", "clippy::map_unwrap_or",
@ -55,6 +61,7 @@ warn = [
"clippy::match_wildcard_for_single_variants", "clippy::match_wildcard_for_single_variants",
"clippy::mem_forget", "clippy::mem_forget",
"clippy::mismatched_target_os", "clippy::mismatched_target_os",
"clippy::mismatching_type_param_order",
"clippy::missing_enforced_import_renames", "clippy::missing_enforced_import_renames",
"clippy::missing_errors_doc", "clippy::missing_errors_doc",
"clippy::missing_safety_doc", "clippy::missing_safety_doc",
@ -86,6 +93,8 @@ warn = [
"clippy::unimplemented", "clippy::unimplemented",
"clippy::unnecessary_wraps", "clippy::unnecessary_wraps",
"clippy::unnested_or_patterns", "clippy::unnested_or_patterns",
"clippy::unused_peekable",
"clippy::unused_rounding",
"clippy::unused_self", "clippy::unused_self",
"clippy::useless_transmute", "clippy::useless_transmute",
"clippy::verbose_file_reads", "clippy::verbose_file_reads",
@ -101,11 +110,15 @@ warn = [
"unused_extern_crates", "unused_extern_crates",
"unused_import_braces", "unused_import_braces",
"unused_lifetimes", "unused_lifetimes",
# "clippy::cloned_instead_of_copied",
# "clippy::mod_module_files",
# "trivial_casts",
# "unused_qualifications",
] ]
allow = [ allow = [
"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",
"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

@ -11,7 +11,7 @@
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈 👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon). egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust.
@ -60,17 +60,17 @@ If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/di
## Demo ## Demo
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/eframe). [Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
To test the demo app locally, run `cargo run --release -p egui_demo_app`. 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/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: 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: 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! **NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
@ -84,7 +84,7 @@ On Fedora Rawhide you need to run:
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)). * A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
* No callbacks * No callbacks
* Pure immediate mode * Pure immediate mode
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/demo/toggle_switch.rs) * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
* Modular: You should be able to use small parts of egui and combine them in new ways * Modular: You should be able to use small parts of egui and combine them in new ways
* Safe: there is no `unsafe` code in egui * Safe: there is no `unsafe` code in egui
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot) * Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot)
@ -114,7 +114,7 @@ The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-
* egui is pure Rust * egui is pure Rust
* egui is easily compiled to WASM * egui is easily compiled to WASM
* egui lets you use native Rust string types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings) * egui lets you use native Rust string types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings)
* [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/demo/toggle_switch.rs) * [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
egui also tries to improve your experience in other small ways: egui also tries to improve your experience in other small ways:
@ -158,37 +158,42 @@ An integration needs to do the following each frame:
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui * **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Run the application code * Run the application code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …) * **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/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 ### Official integrations
These are the official egui integrations: These are the official egui integrations:
* [`eframe`](https://github.com/emilk/egui/tree/master/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui_glow` and `egui-winit`. * [`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_glium`](https://github.com/emilk/egui/tree/master/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium). * [`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_glow`](https://github.com/emilk/egui/tree/master/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-wgpu`](https://github.com/emilk/egui/tree/master/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-winit`](https://github.com/emilk/egui/tree/master/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 ### 3rd party integrations
* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/). * [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/).
* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/). * [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/).
* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw). * [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw).
* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin).
* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2). * [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2).
* [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2).
* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano). * [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano). * [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad). * [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad).
* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad). * [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad).
* [`egui_speedy2d`](https://github.com/heretik31/egui_speedy2d) for [Speedy2d](https://github.com/QuantumBadger/Speedy2D).
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework. * [`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). * [`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). * [`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). * [`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). * [`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). * [`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). * [`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/). * [`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! Missing an integration for the thing you're working on? Create one, it's easy!
@ -219,7 +224,7 @@ loop {
} }
``` ```
For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/egui_glow/src/painter.rs). For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
### Debugging your integration ### Debugging your integration
@ -319,11 +324,11 @@ If you call `.await` in your GUI code, the UI will freeze, which is very bad UX.
* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html) * [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)
### What about accessibility, such as screen readers? ### What about accessibility, such as screen readers?
There is experimental support for a screen reader. In [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab. egui includes optional support for [AccessKit](https://accesskit.dev/), which currently implements the native accessibility APIs on Windows and macOS. This feature is enabled by default in eframe. For platforms that AccessKit doesn't yet support, including web, there is an experimental built-in screen reader; in [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
Read more at <https://github.com/emilk/egui/issues/167>. The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/eframe)? ### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc. `egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen. `egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
@ -343,14 +348,14 @@ Examples:
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_three-d.rs> * <https://github.com/emilk/egui/blob/master/examples/custom_3d_three-d.rs>
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow.rs> * <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow.rs>
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all. `Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
#### Render-to-texture #### Render-to-texture
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use. You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
Examples: Examples:
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs * Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium): <https://github.com/emilk/egui/blob/master/egui_glium/examples/native_texture.rs>. * Using [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium): <https://github.com/emilk/egui/blob/master/crates/egui_glium/examples/native_texture.rs>.
## Other ## Other
@ -367,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>. 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 ### 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. 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.
@ -392,6 +399,7 @@ Notable contributions by:
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543). * [@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). * [@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). * [@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). * 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). egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
@ -404,3 +412,12 @@ Default fonts:
* `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md) * `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md)
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL) * `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence) * `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
---
<div align="center">
<img src="media/rerun_io_logo.png" width="50%">
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing<br>
visualizations for computer vision and robotics.
</div>

1
clippy.toml Normal file
View file

@ -0,0 +1 @@
doc-valid-idents = ["AccessKit", ".."]

View file

@ -0,0 +1,13 @@
# Changelog for ecolor
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`

50
crates/ecolor/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
[package]
name = "ecolor"
version = "0.21.0"
authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>",
]
description = "Color structs and color conversion utilities"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
[lib]
[features]
default = []
## Enable additional checks if debug assertions are enabled (debug builds).
extra_debug_asserts = []
## Always enable additional checks.
extra_asserts = []
[dependencies]
#! ### Optional dependencies
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `ecolor` types to `&[u8]`.
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
cint = { version = "0.3.1", optional = true }
## Enable the [`hex_color`] macro.
color-hex = { version = "0.2.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = { version = "1", optional = true, features = ["derive"] }

11
crates/ecolor/README.md Normal file
View file

@ -0,0 +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

@ -0,0 +1,161 @@
use super::*;
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----
impl From<Alpha<EncodedSrgb<u8>>> for Color32 {
fn from(srgba: Alpha<EncodedSrgb<u8>>) -> Self {
let Alpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
Color32::from_rgba_unmultiplied(r, g, b, a)
}
}
// No From<Color32> for Alpha<_> because Color32 is premultiplied
impl From<PremultipliedAlpha<EncodedSrgb<u8>>> for Color32 {
fn from(srgba: PremultipliedAlpha<EncodedSrgb<u8>>) -> Self {
let PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
Color32::from_rgba_premultiplied(r, g, b, a)
}
}
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<u8>> {
fn from(col: Color32) -> Self {
let (r, g, b, a) = col.to_tuple();
PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
}
}
}
impl From<PremultipliedAlpha<EncodedSrgb<f32>>> for Color32 {
fn from(srgba: PremultipliedAlpha<EncodedSrgb<f32>>) -> Self {
let PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
// This is a bit of an abuse of the function name but it does what we want.
let r = linear_u8_from_linear_f32(r);
let g = linear_u8_from_linear_f32(g);
let b = linear_u8_from_linear_f32(b);
let a = linear_u8_from_linear_f32(a);
Color32::from_rgba_premultiplied(r, g, b, a)
}
}
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<f32>> {
fn from(col: Color32) -> Self {
let (r, g, b, a) = col.to_tuple();
// This is a bit of an abuse of the function name but it does what we want.
let r = linear_f32_from_linear_u8(r);
let g = linear_f32_from_linear_u8(g);
let b = linear_f32_from_linear_u8(b);
let a = linear_f32_from_linear_u8(a);
PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
}
}
}
impl ColorInterop for Color32 {
type CintTy = PremultipliedAlpha<EncodedSrgb<u8>>;
}
// ---- Rgba ----
impl From<PremultipliedAlpha<LinearSrgb<f32>>> for Rgba {
fn from(srgba: PremultipliedAlpha<LinearSrgb<f32>>) -> Self {
let PremultipliedAlpha {
color: LinearSrgb { r, g, b },
alpha: a,
} = srgba;
Rgba([r, g, b, a])
}
}
impl From<Rgba> for PremultipliedAlpha<LinearSrgb<f32>> {
fn from(col: Rgba) -> Self {
let (r, g, b, a) = col.to_tuple();
PremultipliedAlpha {
color: LinearSrgb { r, g, b },
alpha: a,
}
}
}
impl ColorInterop for Rgba {
type CintTy = PremultipliedAlpha<LinearSrgb<f32>>;
}
// ---- Hsva ----
impl From<Alpha<Hsv<f32>>> for Hsva {
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
let Alpha {
color: Hsv { h, s, v },
alpha: a,
} = srgba;
Hsva::new(h, s, v, a)
}
}
impl From<Hsva> for Alpha<Hsv<f32>> {
fn from(col: Hsva) -> Self {
let Hsva { h, s, v, a } = col;
Alpha {
color: Hsv { h, s, v },
alpha: a,
}
}
}
impl ColorInterop for Hsva {
type CintTy = Alpha<Hsv<f32>>;
}
// ---- HsvaGamma ----
impl ColorInterop for HsvaGamma {
type CintTy = Alpha<Hsv<f32>>;
}
impl From<Alpha<Hsv<f32>>> for HsvaGamma {
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
let Alpha {
color: Hsv { h, s, v },
alpha: a,
} = srgba;
Hsva::new(h, s, v, a).into()
}
}
impl From<HsvaGamma> for Alpha<Hsv<f32>> {
fn from(col: HsvaGamma) -> Self {
let Hsva { h, s, v, a } = col.into();
Alpha {
color: Hsv { h, s, v },
alpha: a,
}
}
}

View file

@ -0,0 +1,216 @@
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba};
/// This format is used for space-efficient color representation (32 bits).
///
/// Instead of manipulating this directly it is often better
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
///
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
/// Alpha channel is in linear space.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Color32(pub(crate) [u8; 4]);
impl std::ops::Index<usize> for Color32 {
type Output = u8;
#[inline(always)]
fn index(&self, index: usize) -> &u8 {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> for Color32 {
#[inline(always)]
fn index_mut(&mut self, index: usize) -> &mut u8 {
&mut self.0[index]
}
}
impl Color32 {
// Mostly follows CSS names:
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
/// An ugly color that is planned to be replaced before making it to the screen.
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
#[inline(always)]
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 255])
}
#[inline(always)]
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 0])
}
/// From `sRGBA` with premultiplied alpha.
#[inline(always)]
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
/// From `sRGBA` WITHOUT premultiplied alpha.
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
if a == 255 {
Self::from_rgb(r, g, b) // common-case optimization
} else if a == 0 {
Self::TRANSPARENT // common-case optimization
} else {
let r_lin = linear_f32_from_gamma_u8(r);
let g_lin = linear_f32_from_gamma_u8(g);
let b_lin = linear_f32_from_gamma_u8(b);
let a_lin = linear_f32_from_linear_u8(a);
let r = gamma_u8_from_linear_f32(r_lin * a_lin);
let g = gamma_u8_from_linear_f32(g_lin * a_lin);
let b = gamma_u8_from_linear_f32(b_lin * a_lin);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
#[inline(always)]
pub const fn from_gray(l: u8) -> Self {
Self([l, l, l, 255])
}
#[inline(always)]
pub const fn from_black_alpha(a: u8) -> Self {
Self([0, 0, 0, a])
}
pub fn from_white_alpha(a: u8) -> Self {
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
}
#[inline(always)]
pub const fn from_additive_luminance(l: u8) -> Self {
Self([l, l, l, 0])
}
#[inline(always)]
pub const fn is_opaque(&self) -> bool {
self.a() == 255
}
#[inline(always)]
pub const fn r(&self) -> u8 {
self.0[0]
}
#[inline(always)]
pub const fn g(&self) -> u8 {
self.0[1]
}
#[inline(always)]
pub const fn b(&self) -> u8 {
self.0[2]
}
#[inline(always)]
pub const fn a(&self) -> u8 {
self.0[3]
}
/// Returns an opaque version of self
pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into()
}
/// Returns an additive version of self
#[inline(always)]
pub const fn additive(self) -> Self {
let [r, g, b, _] = self.to_array();
Self([r, g, b, 0])
}
/// Premultiplied RGBA
#[inline(always)]
pub const fn to_array(&self) -> [u8; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
/// Premultiplied RGBA
#[inline(always)]
pub const fn to_tuple(&self) -> (u8, u8, u8, u8) {
(self.r(), self.g(), self.b(), self.a())
}
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
Rgba::from(*self).to_srgba_unmultiplied()
}
/// 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

@ -0,0 +1,39 @@
/// Construct a [`crate::Color32`] from a hex RGB or RGBA string.
///
/// ```
/// # use ecolor::{hex_color, Color32};
/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22));
/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12));
/// ```
#[macro_export]
macro_rules! hex_color {
($s:literal) => {{
let array = color_hex::color_from_hex!($s);
if array.len() == 3 {
$crate::Color32::from_rgb(array[0], array[1], array[2])
} else {
#[allow(unconditional_panic)]
$crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3])
}
}};
}
#[test]
fn test_from_rgb_hex() {
assert_eq!(
crate::Color32::from_rgb(0x20, 0x21, 0x22),
hex_color!("#202122")
);
assert_eq!(
crate::Color32::from_rgb_additive(0x20, 0x21, 0x22),
hex_color!("#202122").additive()
);
}
#[test]
fn test_from_rgba_hex() {
assert_eq!(
crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50),
hex_color!("20212250")
);
}

231
crates/ecolor/src/hsva.rs Normal file
View file

@ -0,0 +1,231 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32, Color32, Rgba,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].
/// No premultiplied alpha.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Hsva {
/// hue 0-1
pub h: f32,
/// saturation 0-1
pub s: f32,
/// value 0-1
pub v: f32,
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32,
}
impl Hsva {
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
Self { h, s, v, a }
}
/// From `sRGBA` with premultiplied alpha
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
Self::from_rgba_premultiplied(
linear_f32_from_gamma_u8(srgba[0]),
linear_f32_from_gamma_u8(srgba[1]),
linear_f32_from_gamma_u8(srgba[2]),
linear_f32_from_linear_u8(srgba[3]),
)
}
/// From `sRGBA` without premultiplied alpha
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
Self::from_rgba_unmultiplied(
linear_f32_from_gamma_u8(srgba[0]),
linear_f32_from_gamma_u8(srgba[1]),
linear_f32_from_gamma_u8(srgba[2]),
linear_f32_from_linear_u8(srgba[3]),
)
}
/// From linear RGBA with premultiplied alpha
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
if a == 0.0 {
if r == 0.0 && b == 0.0 && a == 0.0 {
Hsva::default()
} else {
Hsva::from_additive_rgb([r, g, b])
}
} else {
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
Hsva { h, s, v, a }
}
}
/// From linear RGBA without premultiplied alpha
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
let (h, s, v) = hsv_from_rgb([r, g, b]);
Hsva { h, s, v, a }
}
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva {
h,
s,
v,
a: -0.5, // anything negative is treated as additive
}
}
pub fn from_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva { h, s, v, a: 1.0 }
}
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
Self::from_rgb([
linear_f32_from_gamma_u8(r),
linear_f32_from_gamma_u8(g),
linear_f32_from_gamma_u8(b),
])
}
// ------------------------------------------------------------------------
pub fn to_opaque(self) -> Self {
Self { a: 1.0, ..self }
}
pub fn to_rgb(&self) -> [f32; 3] {
rgb_from_hsv((self.h, self.s, self.v))
}
pub fn to_srgb(&self) -> [u8; 3] {
let [r, g, b] = self.to_rgb();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
]
}
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
let additive = a < 0.0;
if additive {
[r, g, b, 0.0]
} else {
[a * r, a * g, a * b, a]
}
}
/// Represents additive colors using a negative alpha.
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
let Hsva { h, s, v, a } = *self;
let [r, g, b] = rgb_from_hsv((h, s, v));
[r, g, b, a]
}
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_premultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a),
]
}
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a.abs()),
]
}
}
impl From<Hsva> for Rgba {
fn from(hsva: Hsva) -> Rgba {
Rgba(hsva.to_rgba_premultiplied())
}
}
impl From<Rgba> for Hsva {
fn from(rgba: Rgba) -> Hsva {
Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
}
}
impl From<Hsva> for Color32 {
fn from(hsva: Hsva) -> Color32 {
Color32::from(Rgba::from(hsva))
}
}
impl From<Color32> for Hsva {
fn from(srgba: Color32) -> Hsva {
Hsva::from(Rgba::from(srgba))
}
}
/// All ranges in 0-1, rgb is linear.
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
#![allow(clippy::many_single_char_names)]
let min = r.min(g.min(b));
let max = r.max(g.max(b)); // value
let range = max - min;
let h = if max == min {
0.0 // hue is undefined
} else if max == r {
(g - b) / (6.0 * range)
} else if max == g {
(b - r) / (6.0 * range) + 1.0 / 3.0
} else {
// max == b
(r - g) / (6.0 * range) + 2.0 / 3.0
};
let h = (h + 1.0).fract(); // wrap
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
(h, s, max)
}
/// All ranges in 0-1, rgb is linear.
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
#![allow(clippy::many_single_char_names)]
let h = (h.fract() + 1.0).fract(); // wrap
let s = s.clamp(0.0, 1.0);
let f = h * 6.0 - (h * 6.0).floor();
let p = v * (1.0 - s);
let q = v * (1.0 - f * s);
let t = v * (1.0 - (1.0 - f) * s);
match (h * 6.0).floor() as i32 % 6 {
0 => [v, t, p],
1 => [q, v, p],
2 => [p, v, t],
3 => [p, q, v],
4 => [t, p, v],
5 => [v, p, q],
_ => unreachable!(),
}
}
#[test]
#[ignore] // a bit expensive
fn test_hsv_roundtrip() {
for r in 0..=255 {
for g in 0..=255 {
for b in 0..=255 {
let srgba = Color32::from_rgb(r, g, b);
let hsva = Hsva::from(srgba);
assert_eq!(srgba, Color32::from(hsva));
}
}
}
}

View file

@ -0,0 +1,66 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct HsvaGamma {
/// hue 0-1
pub h: f32,
/// saturation 0-1
pub s: f32,
/// value 0-1, in gamma-space (~perceptually even)
pub v: f32,
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32,
}
impl From<HsvaGamma> for Rgba {
fn from(hsvag: HsvaGamma) -> Rgba {
Hsva::from(hsvag).into()
}
}
impl From<HsvaGamma> for Color32 {
fn from(hsvag: HsvaGamma) -> Color32 {
Rgba::from(hsvag).into()
}
}
impl From<HsvaGamma> for Hsva {
fn from(hsvag: HsvaGamma) -> Hsva {
let HsvaGamma { h, s, v, a } = hsvag;
Hsva {
h,
s,
v: linear_from_gamma(v),
a,
}
}
}
impl From<Rgba> for HsvaGamma {
fn from(rgba: Rgba) -> HsvaGamma {
Hsva::from(rgba).into()
}
}
impl From<Color32> for HsvaGamma {
fn from(srgba: Color32) -> HsvaGamma {
Hsva::from(srgba).into()
}
}
impl From<Hsva> for HsvaGamma {
fn from(hsva: Hsva) -> HsvaGamma {
let Hsva { h, s, v, a } = hsva;
HsvaGamma {
h,
s,
v: gamma_from_linear(v),
a,
}
}
}

173
crates/ecolor/src/lib.rs Normal file
View file

@ -0,0 +1,173 @@
//! Color conversions and types.
//!
//! If you want a compact color representation, use [`Color32`].
//! If you want to manipulate RGBA colors use [`Rgba`].
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::wrong_self_convention)]
#[cfg(feature = "cint")]
mod cint_impl;
#[cfg(feature = "cint")]
pub use cint_impl::*;
mod color32;
pub use color32::*;
mod hsva_gamma;
pub use hsva_gamma::*;
mod hsva;
pub use hsva::*;
#[cfg(feature = "color-hex")]
mod hex_color_macro;
mod rgba;
pub use rgba::*;
// ----------------------------------------------------------------------------
// Color conversion:
impl From<Color32> for Rgba {
fn from(srgba: Color32) -> Rgba {
Rgba([
linear_f32_from_gamma_u8(srgba.0[0]),
linear_f32_from_gamma_u8(srgba.0[1]),
linear_f32_from_gamma_u8(srgba.0[2]),
linear_f32_from_linear_u8(srgba.0[3]),
])
}
}
impl From<Rgba> for Color32 {
fn from(rgba: Rgba) -> Color32 {
Color32([
gamma_u8_from_linear_f32(rgba.0[0]),
gamma_u8_from_linear_f32(rgba.0[1]),
gamma_u8_from_linear_f32(rgba.0[2]),
linear_u8_from_linear_f32(rgba.0[3]),
])
}
}
/// gamma [0, 255] -> linear [0, 1].
pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
if s <= 10 {
s as f32 / 3294.6
} else {
((s as f32 + 14.025) / 269.025).powf(2.4)
}
}
/// linear [0, 255] -> linear [0, 1].
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
a as f32 / 255.0
}
/// linear [0, 1] -> gamma [0, 255] (clamped).
/// Values outside this range will be clamped to the range.
pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
if l <= 0.0 {
0
} else if l <= 0.0031308 {
fast_round(3294.6 * l)
} else if l <= 1.0 {
fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
} else {
255
}
}
/// linear [0, 1] -> linear [0, 255] (clamped).
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
fast_round(a * 255.0)
}
fn fast_round(r: f32) -> u8 {
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
}
#[test]
pub fn test_srgba_conversion() {
for b in 0..=255 {
let l = linear_f32_from_gamma_u8(b);
assert!(0.0 <= l && l <= 1.0);
assert_eq!(gamma_u8_from_linear_f32(l), b);
}
}
/// gamma [0, 1] -> linear [0, 1] (not clamped).
/// Works for numbers outside this range (e.g. negative numbers).
pub fn linear_from_gamma(gamma: f32) -> f32 {
if gamma < 0.0 {
-linear_from_gamma(-gamma)
} else if gamma <= 0.04045 {
gamma / 12.92
} else {
((gamma + 0.055) / 1.055).powf(2.4)
}
}
/// linear [0, 1] -> gamma [0, 1] (not clamped).
/// Works for numbers outside this range (e.g. negative numbers).
pub fn gamma_from_linear(linear: f32) -> f32 {
if linear < 0.0 {
-gamma_from_linear(-linear)
} else if linear <= 0.0031308 {
12.92 * linear
} else {
1.055 * linear.powf(1.0 / 2.4) - 0.055
}
}
// ----------------------------------------------------------------------------
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
/// or with the `extra_debug_asserts` feature in debug builds.
#[macro_export]
macro_rules! ecolor_assert {
($($arg: tt)*) => {
if cfg!(any(
feature = "extra_asserts",
all(feature = "extra_debug_asserts", debug_assertions),
)) {
assert!($($arg)*);
}
}
}
// ----------------------------------------------------------------------------
/// Cheap and ugly.
/// Made for graying out disabled `Ui`s.
pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
let [mut r, mut g, mut b, mut a] = color.to_array();
if a == 0 {
r /= 2;
g /= 2;
b /= 2;
} else if a < 170 {
// Cheapish and looks ok.
// Works for e.g. grid stripes.
let div = (2 * 255 / a as i32) as u8;
r = r / 2 + target.r() / div;
g = g / 2 + target.g() / div;
b = b / 2 + target.b() / div;
a /= 2;
} else {
r = r / 2 + target.r() / 2;
g = g / 2 + target.g() / 2;
b = b / 2 + target.b() / 2;
}
Color32::from_rgba_premultiplied(r, g, b, a)
}

266
crates/ecolor/src/rgba.rs Normal file
View file

@ -0,0 +1,266 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32,
};
/// 0-1 linear space `RGBA` color with premultiplied alpha.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Rgba(pub(crate) [f32; 4]);
impl std::ops::Index<usize> for Rgba {
type Output = f32;
#[inline(always)]
fn index(&self, index: usize) -> &f32 {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> for Rgba {
#[inline(always)]
fn index_mut(&mut self, index: usize) -> &mut f32 {
&mut self.0[index]
}
}
#[inline(always)]
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
if f == 0.0 {
state.write_u8(0);
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash;
f.to_bits().hash(state);
}
}
#[allow(clippy::derive_hash_xor_eq)]
impl std::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
crate::f32_hash(state, self.0[0]);
crate::f32_hash(state, self.0[1]);
crate::f32_hash(state, self.0[2]);
crate::f32_hash(state, self.0[3]);
}
}
impl Rgba {
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
#[inline(always)]
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
Self([r, g, b, a])
}
#[inline(always)]
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
Self([r * a, g * a, b * a, a])
}
#[inline(always)]
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r, g, b, a)
}
#[inline(always)]
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
}
#[inline(always)]
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
Self([r, g, b, 1.0])
}
#[inline(always)]
pub const fn from_gray(l: f32) -> Self {
Self([l, l, l, 1.0])
}
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
crate::ecolor_assert!(0.0 <= l && l <= 1.0);
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
Self([l * a, l * a, l * a, a])
}
/// Transparent black
#[inline(always)]
pub fn from_black_alpha(a: f32) -> Self {
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
Self([0.0, 0.0, 0.0, a])
}
/// Transparent white
#[inline(always)]
pub fn from_white_alpha(a: f32) -> Self {
crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
Self([a, a, a, a])
}
/// Return an additive version of this color (alpha = 0)
#[inline(always)]
pub fn additive(self) -> Self {
let [r, g, b, _] = self.0;
Self([r, g, b, 0.0])
}
/// Multiply with e.g. 0.5 to make us half transparent
#[inline(always)]
pub fn multiply(self, alpha: f32) -> Self {
Self([
alpha * self[0],
alpha * self[1],
alpha * self[2],
alpha * self[3],
])
}
#[inline(always)]
pub fn r(&self) -> f32 {
self.0[0]
}
#[inline(always)]
pub fn g(&self) -> f32 {
self.0[1]
}
#[inline(always)]
pub fn b(&self) -> f32 {
self.0[2]
}
#[inline(always)]
pub fn a(&self) -> f32 {
self.0[3]
}
/// How perceptually intense (bright) is the color?
#[inline]
pub fn intensity(&self) -> f32 {
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
}
/// Returns an opaque version of self
pub fn to_opaque(&self) -> Self {
if self.a() == 0.0 {
// Additive or fully transparent black.
Self::from_rgb(self.r(), self.g(), self.b())
} else {
// un-multiply alpha:
Self::from_rgb(
self.r() / self.a(),
self.g() / self.a(),
self.b() / self.a(),
)
}
}
/// Premultiplied RGBA
#[inline(always)]
pub fn to_array(&self) -> [f32; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
/// Premultiplied RGBA
#[inline(always)]
pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
(self.r(), self.g(), self.b(), self.a())
}
/// unmultiply the alpha
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
let a = self.a();
if a == 0.0 {
// Additive, let's assume we are black
self.0
} else {
[self.r() / a, self.g() / a, self.b() / a, a]
}
}
/// unmultiply the alpha
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a.abs()),
]
}
}
impl std::ops::Add for Rgba {
type Output = Rgba;
#[inline(always)]
fn add(self, rhs: Rgba) -> Rgba {
Rgba([
self[0] + rhs[0],
self[1] + rhs[1],
self[2] + rhs[2],
self[3] + rhs[3],
])
}
}
impl std::ops::Mul<Rgba> for Rgba {
type Output = Rgba;
#[inline(always)]
fn mul(self, other: Rgba) -> Rgba {
Rgba([
self[0] * other[0],
self[1] * other[1],
self[2] * other[2],
self[3] * other[3],
])
}
}
impl std::ops::Mul<f32> for Rgba {
type Output = Rgba;
#[inline(always)]
fn mul(self, factor: f32) -> Rgba {
Rgba([
self[0] * factor,
self[1] * factor,
self[2] * factor,
self[3] * factor,
])
}
}
impl std::ops::Mul<Rgba> for f32 {
type Output = Rgba;
#[inline(always)]
fn mul(self, rgba: Rgba) -> Rgba {
Rgba([
self * rgba[0],
self * rgba[1],
self * rgba[2],
self * rgba[3],
])
}
}

View file

@ -1,14 +1,75 @@
# Changelog for eframe # Changelog for eframe
All notable changes to the `eframe` crate. All notable changes to the `eframe` crate.
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), and [`egui_glow`](../egui_glow/CHANGELOG.md) have their own changelogs! NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
## Unreleased ## 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.
#### Desktop/Native:
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
#### Web:
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* Added `wgpu` rendering backed ([#1564](https://github.com/emilk/egui/pull/1564)): * Added `wgpu` rendering backed ([#1564](https://github.com/emilk/egui/pull/1564)):
* Added features "wgpu" and "glow" * Added features `wgpu` and `glow`.
* Added `NativeOptions::renderer` to switch between the rendering backends * Added `NativeOptions::renderer` to switch between the rendering backends.
* `egui_glow`: remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)). * `egui_glow`: remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)).
* Added `App::post_rendering` for e.g. reading the framebuffer ([#1591](https://github.com/emilk/egui/pull/1591)). * Added `App::post_rendering` for e.g. reading the framebuffer ([#1591](https://github.com/emilk/egui/pull/1591)).
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)). * Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
@ -21,15 +82,16 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
* Added ability to read window position and size with `frame.info().window_info` ([#1617](https://github.com/emilk/egui/pull/1617)). * Added ability to read window position and size with `frame.info().window_info` ([#1617](https://github.com/emilk/egui/pull/1617)).
* Allow running on native without hardware accelerated rendering. Change with `NativeOptions::hardware_acceleration` ([#1681](https://github.com/emilk/egui/pull/1681), [#1693](https://github.com/emilk/egui/pull/1693)). * Allow running on native without hardware accelerated rendering. Change with `NativeOptions::hardware_acceleration` ([#1681](https://github.com/emilk/egui/pull/1681), [#1693](https://github.com/emilk/egui/pull/1693)).
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)). * Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
* `dark-light` (dark mode detection) is now enabled by default on Mac and Windows ([#1726](https://github.com/emilk/egui/pull/1726)).
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)). * Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
* Added `Frame::set_visible` ([#1808](https://github.com/emilk/egui/pull/1808)). * Added `Frame::set_visible` ([#1808](https://github.com/emilk/egui/pull/1808)).
* Added fullscreen support ([#1866](https://github.com/emilk/egui/pull/1866)). * Added fullscreen support ([#1866](https://github.com/emilk/egui/pull/1866)).
* You can now continue execution after closing the native desktop window ([#1889](https://github.com/emilk/egui/pull/1889)).
* `Frame::quit` has been renamed to `Frame::close` and `App::on_exit_event` is now `App::on_close_event` ([#1943](https://github.com/emilk/egui/pull/1943)).
#### Web: #### Web:
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)). * Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
* Added `WebOptions::follow_system_theme` and `WebOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
## 0.18.0 - 2022-04-30 ## 0.18.0 - 2022-04-30
@ -92,7 +154,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
## 0.15.0 - 2021-10-24 ## 0.15.0 - 2021-10-24
* `Frame` now provides `set_window_title` to set window title dynamically * `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
* `Frame` now provides `set_decorations` to set whether to show window decorations. * `Frame` now provides `set_decorations` to set whether to show window decorations.
* Remove "http" feature (use https://github.com/emilk/ehttp instead!). * Remove "http" feature (use https://github.com/emilk/ehttp instead!).
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted. * Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.

View file

@ -1,26 +1,30 @@
[package] [package]
name = "eframe" name = "eframe"
version = "0.18.0" version = "0.21.3"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively" description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021" edition = "2021"
rust-version = "1.61" rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/eframe" homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/eframe" repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
categories = ["gui", "game-development"] categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"] keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[lib] [lib]
[features] [features]
default = ["dark-light", "default_fonts", "glow"] default = ["accesskit", "default_fonts", "glow"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light). ## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
## ##
@ -31,8 +35,8 @@ dark-light = ["dep:dark-light"]
## If you plan on specifying your own fonts you may disable this feature. ## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"] default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow). ## 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", "egui_glow"] glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
## Enable saving app state to disk. ## Enable saving app state to disk.
persistence = [ persistence = [
@ -47,59 +51,76 @@ persistence = [
## ##
## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers. ## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers.
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you ## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
puffin = ["dep:puffin", "egui_glow?/puffin"] puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
## Enable screen reader support (requires `ctx.options().screen_reader = true;`) ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
screen_reader = [ ##
"egui-winit/screen_reader", ## For other platforms, use the "accesskit" feature instead.
"tts", web_screen_reader = ["tts"]
]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui_wgpu`](https://github.com/emilk/egui/tree/master/egui_wgpu)). ## 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.
__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. ## This overrides the `glow` feature.
wgpu = ["dep:wgpu", "egui-wgpu"] wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
[dependencies] [dependencies]
egui = { version = "0.18.0", path = "../egui", default-features = false, features = [ egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"bytemuck", "bytemuck",
"tracing", "tracing",
] } ] }
thiserror = "1.0.37"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
#! ### Optional dependencies #! ### Optional dependencies
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
egui_glow = { version = "0.18.0", path = "../egui_glow", optional = true, default-features = false } egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
egui-wgpu = { version = "0.18.0", path = "../egui-wgpu", optional = true, features = ["winit"] } glow = { version = "0.12", optional = true }
glow = { version = "0.11", optional = true }
ron = { version = "0.8", optional = true, features = ["integer128"] } ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
wgpu = { version = "0.13", optional = true }
# ------------------------------------------- # -------------------------------------------
# native: # native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dark-light = { version = "0.2.1", optional = true } egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] } "clipboard",
glutin = { version = "0.29.0" } "links",
winit = "0.27.2" ] }
raw-window-handle = { version = "0.5.0" }
winit = "0.28.1"
# optional native: # optional native:
puffin = { version = "0.13", optional = true } dark-light = { version = "1.0", optional = true }
directories-next = { version = "2", optional = true } directories-next = { version = "2", optional = true }
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", 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.15.0", optional = true }
# ------------------------------------------- # -------------------------------------------
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck = "1.7" bytemuck = "1.7"
getrandom = { version = "0.2", features = ["js"] } # used by ahash
js-sys = "0.3" js-sys = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
wasm-bindgen = "0.2" wasm-bindgen = "=0.2.84"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.52", features = [ web-sys = { version = "0.3.58", features = [
"BinaryType", "BinaryType",
"Blob", "Blob",
"Clipboard", "Clipboard",
@ -142,6 +163,7 @@ web-sys = { version = "0.3.52", features = [
"Window", "Window",
] } ] }
# optional # optional web:
# feature screen_reader egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
tts = { version = "0.20", optional = true } # Can't use 0.22 due to compilation problems on linux: https://github.com/emilk/egui/runs/7170127089?check_suite_focus=true#step:5:713 tts = { version = "0.25", optional = true, default-features = false }
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }

View file

@ -17,23 +17,23 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
--- ---
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit). `eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
To use on Linux, first run: 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. You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`. You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
## Alternatives ## Alternatives
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others. `eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/egui_glow/examples/pure_glow.rs>. You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
## Problems with running egui on the web ## Problems with running egui on the web
@ -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. * 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). * 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. * 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). 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

@ -6,6 +6,25 @@
#![warn(missing_docs)] // Let's keep `epi` well-documented. #![warn(missing_docs)] // Let's keep `epi` well-documented.
#[cfg(target_arch = "wasm32")]
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
///
/// 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. /// This is how your app is created.
/// ///
/// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc. /// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc.
@ -43,7 +62,7 @@ pub struct CreationContext<'s> {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/eframe). /// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
pub trait App { pub trait App {
/// Called each time the UI needs repainting, which may be many times per second. /// Called each time the UI needs repainting, which may be many times per second.
/// ///
@ -54,6 +73,26 @@ pub trait App {
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame); fn update(&mut self, ctx: &egui::Context, frame: &mut Frame);
/// Get a handle to the 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 [`crate::web::backend::AppRunner`].
///
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
///
/// Just copy-paste this as your implementation:
/// ```ignore
/// #[cfg(target_arch = "wasm32")]
/// fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
/// Some(&mut *self)
/// }
/// ```
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
None
}
/// Called on shutdown, and perhaps at regular intervals. Allows you to save state. /// Called on shutdown, and perhaps at regular intervals. Allows you to save state.
/// ///
/// Only called when the "persistence" feature is enabled. /// Only called when the "persistence" feature is enabled.
@ -67,21 +106,25 @@ pub trait App {
/// where `APPNAME` is what is given to `eframe::run_native`. /// where `APPNAME` is what is given to `eframe::run_native`.
fn save(&mut self, _storage: &mut dyn Storage) {} fn save(&mut self, _storage: &mut dyn Storage) {}
/// Called before an exit that can be aborted. /// Called when the user attempts to close the desktop window and/or quit the application.
/// By returning `false` the exit will be aborted. To continue the exit return `true`. ///
/// By returning `false` the closing will be aborted. To continue the closing return `true`.
/// ///
/// A scenario where this method will be run is after pressing the close button on a native /// A scenario where this method will be run is after pressing the close button on a native
/// window, which allows you to ask the user whether they want to do something before exiting. /// window, which allows you to ask the user whether they want to do something before exiting.
/// See the example at <https://github.com/emilk/egui/blob/master/examples/confirm_exit/> for practical usage. /// See the example at <https://github.com/emilk/egui/blob/master/examples/confirm_exit/> for practical usage.
/// ///
/// It will _not_ be called on the web or when the window is forcefully closed. /// It will _not_ be called on the web or when the window is forcefully closed.
fn on_exit_event(&mut self) -> bool { #[cfg(not(target_arch = "wasm32"))]
#[doc(alias = "exit")]
#[doc(alias = "quit")]
fn on_close_event(&mut self) -> bool {
true true
} }
/// Called once on shutdown, after [`Self::save`]. /// Called once on shutdown, after [`Self::save`].
/// ///
/// If you need to abort an exit use [`Self::on_exit_event`]. /// If you need to abort an exit use [`Self::on_close_event`].
/// ///
/// To get a [`glow`] context you need to compile with the `glow` feature flag, /// To get a [`glow`] context you need to compile with the `glow` feature flag,
/// and run eframe with the glow backend. /// and run eframe with the glow backend.
@ -90,7 +133,7 @@ pub trait App {
/// Called once on shutdown, after [`Self::save`]. /// Called once on shutdown, after [`Self::save`].
/// ///
/// If you need to abort an exit use [`Self::on_exit_event`]. /// If you need to abort an exit use [`Self::on_close_event`].
#[cfg(not(feature = "glow"))] #[cfg(not(feature = "glow"))]
fn on_exit(&mut self) {} fn on_exit(&mut self) {}
@ -105,20 +148,25 @@ pub trait App {
/// The size limit of the web app canvas. /// The size limit of the web app canvas.
/// ///
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. /// 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 { fn max_size_points(&self) -> egui::Vec2 {
egui::Vec2::INFINITY 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. /// 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. // 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 // We use a bit of transparency so that if the user switches on the
// `transparent()` option they get immediate results. // `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 // _visuals.window_fill() would also be a natural choice
} }
@ -136,7 +184,7 @@ pub trait App {
} }
/// If `true` a warm-up call to [`Self::update`] will be issued where /// 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. /// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
/// ///
@ -173,9 +221,8 @@ pub enum HardwareAcceleration {
/// ///
/// Only a single native window is currently supported. /// Only a single native window is currently supported.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[derive(Clone)]
pub struct NativeOptions { pub struct NativeOptions {
/// Sets whether or not the window will always be on top of other windows. /// Sets whether or not the window will always be on top of other windows at initialization.
pub always_on_top: bool, pub always_on_top: bool,
/// Show window in maximized mode /// Show window in maximized mode
@ -190,6 +237,14 @@ pub struct NativeOptions {
/// Default: `false`. /// Default: `false`.
pub fullscreen: bool, pub fullscreen: bool,
/// On Mac: the window doesn't have a titlebar, but floating window buttons.
///
/// See [winit's documentation][with_fullsize_content_view] for information on Mac-specific options.
///
/// [with_fullsize_content_view]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/macos/trait.WindowBuilderExtMacOS.html#tymethod.with_fullsize_content_view
#[cfg(target_os = "macos")]
pub fullsize_content: bool,
/// On Windows: enable drag and drop support. Drag and drop can /// On Windows: enable drag and drop support. Drag and drop can
/// not be disabled on other platforms. /// not be disabled on other platforms.
/// ///
@ -211,10 +266,10 @@ pub struct NativeOptions {
/// The initial inner size of the native window in points (logical pixels). /// The initial inner size of the native window in points (logical pixels).
pub initial_window_size: Option<egui::Vec2>, 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>, 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>, pub max_window_size: Option<egui::Vec2>,
/// Should the app window be resizable? /// Should the app window be resizable?
@ -225,6 +280,10 @@ pub struct NativeOptions {
/// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent. /// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent.
pub transparent: bool, pub transparent: bool,
/// On desktop: mouse clicks pass through the window, used for non-interactable overlays
/// Generally you would use this in conjunction with always_on_top
pub mouse_passthrough: bool,
/// Turn on vertical syncing, limiting the FPS to the display refresh rate. /// Turn on vertical syncing, limiting the FPS to the display refresh rate.
/// ///
/// The default is `true`. /// The default is `true`.
@ -244,6 +303,10 @@ pub struct NativeOptions {
/// Sets the number of bits in the depth buffer. /// Sets the number of bits in the depth buffer.
/// ///
/// `egui` doesn't need the depth buffer, so the default value is 0. /// `egui` doesn't need the depth buffer, so the default value is 0.
///
/// On `wgpu` backends, due to limited depth texture format options, this
/// will be interpreted as a boolean (non-zero = true) for whether or not
/// specifically a `Depth32Float` buffer is used.
pub depth_buffer: u8, pub depth_buffer: u8,
/// Sets the number of bits in the stencil buffer. /// Sets the number of bits in the stencil buffer.
@ -251,15 +314,16 @@ pub struct NativeOptions {
/// `egui` doesn't need the stencil buffer, so the default value is 0. /// `egui` doesn't need the stencil buffer, so the default value is 0.
pub stencil_buffer: u8, pub stencil_buffer: u8,
/// Specify wether or not hardware acceleration is preferred, required, or not. /// Specify whether or not hardware acceleration is preferred, required, or not.
/// ///
/// Default: [`HardwareAcceleration::Preferred`]. /// Default: [`HardwareAcceleration::Preferred`].
pub hardware_acceleration: HardwareAcceleration, pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use. /// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub renderer: Renderer, pub renderer: Renderer,
/// If the `dark-light` feature is enabled: /// Only used if the `dark-light` feature is enabled:
/// ///
/// Try to detect and follow the system preferred setting for dark vs light mode. /// Try to detect and follow the system preferred setting for dark vs light mode.
/// ///
@ -272,7 +336,7 @@ pub struct NativeOptions {
/// Which theme to use in case [`Self::follow_system_theme`] is `false` /// Which theme to use in case [`Self::follow_system_theme`] is `false`
/// or the `dark-light` feature is disabled. /// or the `dark-light` feature is disabled.
/// ///
/// Default: `Theme::Dark`. /// Default: [`Theme::Dark`].
pub default_theme: Theme, pub default_theme: Theme,
/// This controls what happens when you close the main eframe window. /// This controls what happens when you close the main eframe window.
@ -288,6 +352,50 @@ pub struct NativeOptions {
/// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used. /// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used.
/// When `false`, [`winit::event_loop::EventLoop::run`] is used. /// When `false`, [`winit::event_loop::EventLoop::run`] is used.
pub run_and_return: bool, pub run_and_return: bool,
/// Hook into the building of an event loop before it is run.
///
/// Specify a callback here in case you need to make platform specific changes to the
/// 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")]
/// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture.
/// See <https://github.com/emilk/egui/pull/1993>.
///
/// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader").
pub shader_version: Option<egui_glow::ShaderVersion>,
/// On desktop: make the window position to be centered at initialization.
///
/// Platform specific:
///
/// Wayland desktop currently not supported.
pub centered: bool,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
}
#[cfg(not(target_arch = "wasm32"))]
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
}
}
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -298,6 +406,10 @@ impl Default for NativeOptions {
maximized: false, maximized: false,
decorated: true, decorated: true,
fullscreen: false, fullscreen: false,
#[cfg(target_os = "macos")]
fullsize_content: false,
drag_and_drop_support: true, drag_and_drop_support: true,
icon_data: None, icon_data: None,
initial_window_pos: None, initial_window_pos: None,
@ -306,15 +418,30 @@ impl Default for NativeOptions {
max_window_size: None, max_window_size: None,
resizable: true, resizable: true,
transparent: false, transparent: false,
mouse_passthrough: false,
vsync: true, vsync: true,
multisampling: 0, multisampling: 0,
depth_buffer: 0, depth_buffer: 0,
stencil_buffer: 0, stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred, hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu"))]
renderer: Renderer::default(), renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"), follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark, default_theme: Theme::Dark,
run_and_return: true, 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(),
} }
} }
} }
@ -329,6 +456,7 @@ impl NativeOptions {
match dark_light::detect() { match dark_light::detect() {
dark_light::Mode::Dark => Some(Theme::Dark), dark_light::Mode::Dark => Some(Theme::Dark),
dark_light::Mode::Light => Some(Theme::Light), dark_light::Mode::Light => Some(Theme::Light),
dark_light::Mode::Default => None,
} }
} else { } else {
None None
@ -363,7 +491,12 @@ pub struct WebOptions {
/// Which version of WebGl context to select /// Which version of WebGl context to select
/// ///
/// Default: [`WebGlContextOption::BestFirst`]. /// Default: [`WebGlContextOption::BestFirst`].
#[cfg(feature = "glow")]
pub webgl_context_option: WebGlContextOption, pub webgl_context_option: WebGlContextOption,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -372,7 +505,26 @@ impl Default for WebOptions {
Self { Self {
follow_system_theme: true, follow_system_theme: true,
default_theme: Theme::Dark, default_theme: Theme::Dark,
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst, webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration {
// WebGPU is not stable enough yet, use WebGL emulation
backends: wgpu::Backends::GL,
device_descriptor: wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
features: wgpu::Features::default(),
limits: wgpu::Limits {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..wgpu::Limits::downlevel_webgl2_defaults()
},
},
..Default::default()
},
} }
} }
} }
@ -426,6 +578,7 @@ pub enum WebGlContextOption {
/// What rendering backend to use. /// What rendering backend to use.
/// ///
/// You need to enable the "glow" and "wgpu" features to have a choice. /// 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@ -439,6 +592,7 @@ pub enum Renderer {
Wgpu, Wgpu,
} }
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl Default for Renderer { impl Default for Renderer {
fn default() -> Self { fn default() -> Self {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@ -454,6 +608,7 @@ impl Default for Renderer {
} }
} }
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::fmt::Display for Renderer { impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -466,6 +621,7 @@ impl std::fmt::Display for Renderer {
} }
} }
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::str::FromStr for Renderer { impl std::str::FromStr for Renderer {
type Err = String; type Err = String;
@ -571,17 +727,44 @@ impl Frame {
self.wgpu_render_state.as_ref() self.wgpu_render_state.as_ref()
} }
/// Signal the app to stop/exit/quit the app (only works for native apps, not web apps). /// Tell `eframe` to close the desktop window.
/// The framework will not quit immediately, but at the end of the this frame. ///
/// The window will not close immediately, but at the end of the this frame.
///
/// Calling this will likely result in the app quitting, unless
/// you have more code after the call to [`crate::run_native`].
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[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`"]
pub fn quit(&mut self) { pub fn quit(&mut self) {
self.output.quit = true; self.close();
} }
/// Set the desired inner size of the window (in egui points). /// Set the desired inner size of the window (in egui points).
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn set_window_size(&mut self, size: egui::Vec2) { pub fn set_window_size(&mut self, size: egui::Vec2) {
self.output.window_size = Some(size); 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. /// Set the desired title of the window.
@ -602,12 +785,14 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn set_fullscreen(&mut self, fullscreen: bool) { pub fn set_fullscreen(&mut self, fullscreen: bool) {
self.output.fullscreen = Some(fullscreen); 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. /// set the position of the outer window.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn set_window_pos(&mut self, pos: egui::Pos2) { pub fn set_window_pos(&mut self, pos: egui::Pos2) {
self.output.window_pos = Some(pos); 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 /// When called, the native window will follow the
@ -625,7 +810,31 @@ impl Frame {
self.output.visible = Some(visible); self.output.visible = Some(visible);
} }
/// On desktop: Set the window always on top.
///
/// (Wayland desktop currently not supported)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_always_on_top(&mut self, always_on_top: bool) {
self.output.always_on_top = Some(always_on_top);
}
/// On desktop: Set the window to be centered.
///
/// (Wayland desktop currently not supported)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_centered(&mut self) {
if let Some(monitor_size) = self.info.window_info.monitor_size {
let inner_size = self.info.window_info.size;
if monitor_size.x > 1.0 && monitor_size.y > 1.0 {
let x = (monitor_size.x - inner_size.x) / 2.0;
let y = (monitor_size.y - inner_size.y) / 2.0;
self.set_window_pos(egui::Pos2 { x, y });
}
}
}
/// for integrations only: call once per frame /// for integrations only: call once per frame
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput { pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
std::mem::take(&mut self.output) std::mem::take(&mut self.output)
} }
@ -635,6 +844,9 @@ impl Frame {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub struct WebInfo { pub struct WebInfo {
/// The browser user agent.
pub user_agent: String,
/// Information about the URL. /// Information about the URL.
pub location: Location, pub location: Location,
} }
@ -653,8 +865,17 @@ pub struct WindowInfo {
/// Are we in fullscreen mode? /// Are we in fullscreen mode?
pub fullscreen: bool, pub fullscreen: bool,
/// Are we minimized?
pub minimized: bool,
/// Are we maximized?
pub maximized: bool,
/// Window inner size in egui points (logical pixels). /// Window inner size in egui points (logical pixels).
pub size: egui::Vec2, pub size: egui::Vec2,
/// Current monitor size in egui points (logical pixels)
pub monitor_size: Option<egui::Vec2>,
} }
/// Information about the URL. /// Information about the URL.
@ -797,10 +1018,9 @@ pub(crate) mod backend {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[must_use] #[must_use]
pub struct AppOutput { pub struct AppOutput {
/// Set to `true` to stop the app. /// Set to `true` to close the native window (which often quits the app).
/// This does nothing for web apps.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub quit: bool, pub close: bool,
/// Set to some size to resize the outer window (e.g. glium window) to this size. /// Set to some size to resize the outer window (e.g. glium window) to this size.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -829,5 +1049,17 @@ pub(crate) mod backend {
/// Set to some bool to change window visibility. /// Set to some bool to change window visibility.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub visible: Option<bool>, pub visible: Option<bool>,
/// 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

@ -48,8 +48,9 @@
//! /// Call this once from the HTML. //! /// Call this once from the HTML.
//! #[cfg(target_arch = "wasm32")] //! #[cfg(target_arch = "wasm32")]
//! #[wasm_bindgen] //! #[wasm_bindgen]
//! pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> { //! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
//! eframe::start_web(canvas_id, Box::new(|cc| Box::new(MyApp::new(cc)))) //! let web_options = eframe::WebOptions::default();
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
//! } //! }
//! ``` //! ```
//! //!
@ -102,18 +103,32 @@ pub use web_sys;
/// /// You can add more callbacks like this if you want to call in to your code. /// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")] /// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen] /// #[wasm_bindgen]
/// pub 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(); /// let web_options = eframe::WebOptions::default();
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))) /// eframe::start_web(
/// canvas_id,
/// web_options,
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
/// )
/// .await
/// .map(|handle| WebHandle { handle })
/// } /// }
/// ``` /// ```
///
/// # Errors
/// Failing to initialize WebGL graphics.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn start_web( pub async fn start_web(
canvas_id: &str, canvas_id: &str,
web_options: WebOptions, web_options: WebOptions,
app_creator: AppCreator, 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)?; let handle = web::start(canvas_id, web_options, app_creator).await?;
Ok(handle) Ok(handle)
} }
@ -122,6 +137,7 @@ pub fn start_web(
// When compiling natively // When compiling natively
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod native; mod native;
/// This is how you start a native (desktop) app. /// This is how you start a native (desktop) app.
@ -159,46 +175,88 @@ mod native;
/// } /// }
/// } /// }
/// ``` /// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)] #[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; let renderer = native_options.renderer;
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
match renderer { match renderer {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
Renderer::Glow => { Renderer::Glow => {
tracing::debug!("Using the glow renderer"); 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")] #[cfg(feature = "wgpu")]
Renderer::Wgpu => { Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer"); 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"))] #[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function { macro_rules! profile_function {
($($arg: tt)*) => { ($($arg: tt)*) => {
#[cfg(feature = "puffin")] #[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*); puffin::profile_function!($($arg)*);
}; };
} }
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_function; pub(crate) use profile_function;
/// Profiling macro for feature "puffin" /// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
macro_rules! profile_scope { macro_rules! profile_scope {
($($arg: tt)*) => { ($($arg: tt)*) => {
#[cfg(feature = "puffin")] #[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*); puffin::profile_scope!($($arg)*);
}; };
} }
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_scope; pub(crate) use profile_scope;
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) use profiling_scopes::*;

View file

@ -1,7 +1,25 @@
use crate::{epi, Theme, WindowInfo};
use egui_winit::{native_pixels_per_point, WindowSettings};
use winit::event_loop::EventLoopWindowTarget; use winit::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
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> { pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize { winit::dpi::LogicalSize {
width: points.x as f64, width: points.x as f64,
@ -9,36 +27,56 @@ 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 let position = window
.outer_position() .outer_position()
.ok() .ok()
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into())) .map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y }); .map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
let monitor = window.current_monitor().is_some();
let monitor_size = if monitor {
let size = window.current_monitor().unwrap().size();
Some(egui::vec2(size.width as _, size.height as _))
} else {
None
};
let size = window let size = window
.inner_size() .inner_size()
.to_logical::<f32>(pixels_per_point.into()); .to_logical::<f32>(pixels_per_point.into());
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
WindowInfo { WindowInfo {
position, position,
fullscreen: window.fullscreen().is_some(), fullscreen: window.fullscreen().is_some(),
minimized: window_state.minimized,
maximized: window_state.maximized,
size: egui::Vec2 { size: egui::Vec2 {
x: size.width, x: size.width,
y: size.height, y: size.height,
}, },
monitor_size,
} }
} }
pub fn window_builder( pub fn window_builder<E>(
event_loop: &EventLoopWindowTarget<E>,
title: &str,
native_options: &epi::NativeOptions, native_options: &epi::NativeOptions,
window_settings: &Option<WindowSettings>, window_settings: Option<WindowSettings>,
) -> winit::window::WindowBuilder { ) -> winit::window::WindowBuilder {
let epi::NativeOptions { let epi::NativeOptions {
always_on_top,
maximized, maximized,
decorated, decorated,
fullscreen, fullscreen,
#[cfg(target_os = "macos")]
fullsize_content,
drag_and_drop_support, drag_and_drop_support,
icon_data, icon_data,
initial_window_pos, initial_window_pos,
@ -47,19 +85,31 @@ pub fn window_builder(
max_window_size, max_window_size,
resizable, resizable,
transparent, transparent,
centered,
.. ..
} = native_options; } = native_options;
let window_icon = icon_data.clone().and_then(load_icon); let window_icon = icon_data.clone().and_then(load_icon);
let mut window_builder = winit::window::WindowBuilder::new() let mut window_builder = winit::window::WindowBuilder::new()
.with_always_on_top(*always_on_top) .with_title(title)
.with_decorations(*decorated) .with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))) .with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
.with_maximized(*maximized) .with_maximized(*maximized)
.with_resizable(*resizable) .with_resizable(*resizable)
.with_transparent(*transparent) .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 {
window_builder = window_builder
.with_title_hidden(true)
.with_titlebar_transparent(true)
.with_fullsize_content_view(true);
}
if let Some(min_size) = *min_window_size { if let Some(min_size) = *min_window_size {
window_builder = window_builder.with_min_inner_size(points_to_size(min_size)); window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
@ -70,21 +120,73 @@ pub fn window_builder(
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support); 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_builder = window_settings.initialize_window(window_builder);
window_settings.inner_size_points()
} else { } else {
if let Some(pos) = *initial_window_pos { 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, x: pos.x as f64,
y: pos.y as f64, y: pos.y as f64,
}); });
} }
if let Some(initial_window_size) = *initial_window_size { 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)); 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
} }
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);
}
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> { fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
@ -113,16 +215,20 @@ pub fn handle_app_output(
window: &winit::window::Window, window: &winit::window::Window,
current_pixels_per_point: f32, current_pixels_per_point: f32,
app_output: epi::backend::AppOutput, app_output: epi::backend::AppOutput,
window_state: &mut WindowState,
) { ) {
let epi::backend::AppOutput { let epi::backend::AppOutput {
quit: _, close: _,
window_size, window_size,
window_title, window_title,
decorated, decorated,
fullscreen, fullscreen,
drag_window, drag_window,
window_pos, window_pos,
visible, visible: _, // handled in post_present
always_on_top,
minimized,
maximized,
} = app_output; } = app_output;
if let Some(decorated) = decorated { if let Some(decorated) = decorated {
@ -140,7 +246,7 @@ pub fn handle_app_output(
} }
if let Some(fullscreen) = fullscreen { if let Some(fullscreen) = fullscreen {
window.set_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))); window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
} }
if let Some(window_title) = window_title { if let Some(window_title) = window_title {
@ -158,8 +264,23 @@ pub fn handle_app_output(
let _ = window.drag_window(); let _ = window.drag_window();
} }
if let Some(visible) = visible { if let Some(always_on_top) = always_on_top {
window.set_visible(visible); 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;
} }
} }
@ -183,9 +304,10 @@ pub struct EpiIntegration {
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput, pending_full_output: egui::FullOutput,
egui_winit: egui_winit::State, egui_winit: egui_winit::State,
/// When set, it is time to quit /// When set, it is time to close the native window.
quit: bool, close: bool,
can_drag_window: bool, can_drag_window: bool,
window_state: WindowState,
} }
impl EpiIntegration { impl EpiIntegration {
@ -200,16 +322,27 @@ impl EpiIntegration {
) -> Self { ) -> Self {
let egui_ctx = egui::Context::default(); 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 { let frame = epi::Frame {
info: epi::IntegrationInfo { info: epi::IntegrationInfo {
system_theme, system_theme,
cpu_usage: None, cpu_usage: None,
native_pixels_per_point: Some(native_pixels_per_point(window)), 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),
..Default::default()
}, },
output: Default::default(),
storage, storage,
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl, gl,
@ -219,8 +352,7 @@ impl EpiIntegration {
let mut egui_winit = egui_winit::State::new(event_loop); let mut egui_winit = egui_winit::State::new(event_loop);
egui_winit.set_max_texture_side(max_texture_side); egui_winit.set_max_texture_side(max_texture_side);
let pixels_per_point = window.scale_factor() as f32; egui_winit.set_pixels_per_point(native_pixels_per_point);
egui_winit.set_pixels_per_point(pixels_per_point);
Self { Self {
frame, frame,
@ -228,41 +360,81 @@ impl EpiIntegration {
egui_ctx, egui_ctx,
egui_winit, egui_winit,
pending_full_output: Default::default(), pending_full_output: Default::default(),
quit: false, close: false,
can_drag_window: false, can_drag_window: false,
window_state,
} }
} }
#[cfg(feature = "accesskit")]
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
) {
let egui_ctx = self.egui_ctx.clone();
self.egui_winit
.init_accesskit(window, event_loop_proxy, move || {
// This function is called when an accessibility client
// (e.g. screen reader) makes its first request. If we got here,
// we know that an accessibility tree is actually wanted.
egui_ctx.enable_accesskit();
// Enqueue a repaint so we'll receive a full tree update soon.
egui_ctx.request_repaint();
egui_ctx.accesskit_placeholder_tree_update()
});
}
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!(); crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
self.egui_ctx.memory().set_everything_is_visible(true); self.egui_ctx
.memory_mut(|mem| mem.set_everything_is_visible(true));
let full_output = self.update(app, window); let full_output = self.update(app, window);
self.pending_full_output.append(full_output); // Handle it next frame 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(); self.egui_ctx.clear_animations();
} }
/// If `true`, it is time to shut down. /// If `true`, it is time to close the native window.
pub fn should_quit(&self) -> bool { pub fn should_close(&self) -> bool {
self.quit self.close
} }
pub fn on_event(&mut self, app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>) { pub fn on_event(
&mut self,
app: &mut dyn epi::App,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event::{ElementState, MouseButton, WindowEvent};
match event { match event {
WindowEvent::CloseRequested => self.quit = app.on_exit_event(), WindowEvent::CloseRequested => {
WindowEvent::Destroyed => self.quit = true, 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 { WindowEvent::MouseInput {
button: MouseButton::Left, button: MouseButton::Left,
state: ElementState::Pressed, state: ElementState::Pressed,
.. ..
} => self.can_drag_window = true, } => self.can_drag_window = true,
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.frame.info.native_pixels_per_point = Some(*scale_factor as _);
}
_ => {} _ => {}
} }
self.egui_winit.on_event(&self.egui_ctx, event); self.egui_winit.on_event(&self.egui_ctx, event)
}
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_winit.on_accesskit_action_request(request);
} }
pub fn update( pub fn update(
@ -272,12 +444,16 @@ impl EpiIntegration {
) -> egui::FullOutput { ) -> egui::FullOutput {
let frame_start = std::time::Instant::now(); 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); let raw_input = self.egui_winit.take_egui_input(window);
// Run user code:
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
crate::profile_scope!("App::update"); crate::profile_scope!("App::update");
app.update(egui_ctx, &mut self.frame); app.update(egui_ctx, &mut self.frame);
}); });
self.pending_full_output.append(full_output); self.pending_full_output.append(full_output);
let full_output = std::mem::take(&mut self.pending_full_output); let full_output = std::mem::take(&mut self.pending_full_output);
@ -285,13 +461,20 @@ impl EpiIntegration {
let mut app_output = self.frame.take_app_output(); let mut app_output = self.frame.take_app_output();
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
self.can_drag_window = false; self.can_drag_window = false;
if app_output.quit { if app_output.close {
self.quit = app.on_exit_event(); self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
} }
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); 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,
&mut self.window_state,
);
} }
let frame_time = (std::time::Instant::now() - frame_start).as_secs_f64() as f32; let frame_time = frame_start.elapsed().as_secs_f64() as f32;
self.frame.info.cpu_usage = Some(frame_time); self.frame.info.cpu_usage = Some(frame_time);
full_output full_output
@ -304,6 +487,12 @@ impl EpiIntegration {
app.post_rendering(window_size_px, &self.frame); app.post_rendering(window_size_px, &self.frame);
} }
pub fn post_present(&mut self, window: &winit::window::Window) {
if let Some(visible) = self.frame.output.visible.take() {
window.set_visible(visible);
}
}
pub fn handle_platform_output( pub fn handle_platform_output(
&mut self, &mut self,
window: &winit::window::Window, window: &winit::window::Window,
@ -339,7 +528,8 @@ impl EpiIntegration {
} }
if _app.persist_egui_memory() { if _app.persist_egui_memory() {
crate::profile_scope!("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"); crate::profile_scope!("App::save");

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,11 @@
use super::{glow_wrapping::WrappedGlowPainter, *};
use crate::epi;
use egui::{ use egui::{
mutex::{Mutex, MutexGuard}, mutex::{Mutex, MutexGuard},
TexturesDelta, TexturesDelta,
}; };
pub use egui::{pos2, Color32};
use crate::{epi, App};
use super::{web_painter::WebPainter, *};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -88,6 +87,10 @@ impl IsDestroyed {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
fn user_agent() -> Option<String> {
web_sys::window()?.navigator().user_agent().ok()
}
fn web_location() -> epi::Location { fn web_location() -> epi::Location {
let location = web_sys::window().unwrap().location(); let location = web_sys::window().unwrap().location();
@ -102,7 +105,7 @@ fn web_location() -> epi::Location {
let query_map = parse_query_map(&query) let query_map = parse_query_map(&query)
.iter() .iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string())) .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect(); .collect();
epi::Location { epi::Location {
@ -162,7 +165,7 @@ fn test_parse_query() {
pub struct AppRunner { pub struct AppRunner {
pub(crate) frame: epi::Frame, pub(crate) frame: epi::Frame,
egui_ctx: egui::Context, egui_ctx: egui::Context,
painter: WrappedGlowPainter, painter: ActiveWebPainter,
pub(crate) input: WebInput, pub(crate) input: WebInput,
app: Box<dyn epi::App>, app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>, pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
@ -182,13 +185,14 @@ impl Drop for AppRunner {
} }
impl AppRunner { impl AppRunner {
pub fn new( /// # Errors
/// Failure to initialize WebGL renderer.
pub async fn new(
canvas_id: &str, canvas_id: &str,
web_options: crate::WebOptions, web_options: crate::WebOptions,
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
) -> Result<Self, JsValue> { ) -> Result<Self, String> {
let painter = WrappedGlowPainter::new(canvas_id, web_options.webgl_context_option) let painter = ActiveWebPainter::new(canvas_id, &web_options).await?;
.map_err(JsValue::from)?; // fail early
let system_theme = if web_options.follow_system_theme { let system_theme = if web_options.follow_system_theme {
super::system_theme() super::system_theme()
@ -198,6 +202,7 @@ impl AppRunner {
let info = epi::IntegrationInfo { let info = epi::IntegrationInfo {
web_info: epi::WebInfo { web_info: epi::WebInfo {
user_agent: user_agent().unwrap_or_default(),
location: web_location(), location: web_location(),
}, },
system_theme, system_theme,
@ -207,6 +212,9 @@ impl AppRunner {
let storage = LocalStorage::default(); let storage = LocalStorage::default();
let egui_ctx = egui::Context::default(); let egui_ctx = egui::Context::default();
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
&user_agent().unwrap_or_default(),
));
load_memory(&egui_ctx); load_memory(&egui_ctx);
let theme = system_theme.unwrap_or(web_options.default_theme); let theme = system_theme.unwrap_or(web_options.default_theme);
@ -216,9 +224,13 @@ impl AppRunner {
egui_ctx: egui_ctx.clone(), egui_ctx: egui_ctx.clone(),
integration_info: info.clone(), integration_info: info.clone(),
storage: Some(&storage), storage: Some(&storage),
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: Some(painter.painter.gl().clone()), gl: Some(painter.gl().clone()),
#[cfg(feature = "wgpu")]
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None, wgpu_render_state: None,
}); });
@ -226,9 +238,13 @@ impl AppRunner {
info, info,
output: Default::default(), output: Default::default(),
storage: Some(Box::new(storage)), storage: Some(Box::new(storage)),
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: Some(painter.gl().clone()), gl: Some(painter.gl().clone()),
#[cfg(feature = "wgpu")]
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None, wgpu_render_state: None,
}; };
@ -265,6 +281,17 @@ impl AppRunner {
&self.egui_ctx &self.egui_ctx
} }
/// 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 + App>(&mut self) -> &mut ConreteApp {
self.app
.as_any_mut()
.expect("Your app must implement `as_any_mut`, but it doesn't")
.downcast_mut::<ConreteApp>()
.unwrap()
}
pub fn auto_save(&mut self) { pub fn auto_save(&mut self) {
let now = now_sec(); let now = now_sec();
let time_since_last_save = now - self.last_save_time; let time_since_last_save = now - self.last_save_time;
@ -286,10 +313,11 @@ impl AppRunner {
pub fn warm_up(&mut self) -> Result<(), JsValue> { pub fn warm_up(&mut self) -> Result<(), JsValue> {
if self.app.warm_up_enabled() { if self.app.warm_up_enabled() {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
self.egui_ctx.memory().set_everything_is_visible(true); self.egui_ctx
.memory_mut(|m| m.set_everything_is_visible(true));
self.logic()?; 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(); self.egui_ctx.clear_animations();
} }
Ok(()) Ok(())
@ -346,16 +374,12 @@ impl AppRunner {
Ok((repaint_after, clipped_primitives)) Ok((repaint_after, clipped_primitives))
} }
pub fn clear_color_buffer(&self) {
self.painter
.clear(self.app.clear_color(&self.egui_ctx.style().visuals));
}
/// Paint the results of the last call to [`Self::logic`]. /// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
let textures_delta = std::mem::take(&mut self.textures_delta); let textures_delta = std::mem::take(&mut self.textures_delta);
self.painter.paint_and_update_textures( self.painter.paint_and_update_textures(
self.app.clear_color(&self.egui_ctx.style().visuals),
clipped_primitives, clipped_primitives,
self.egui_ctx.pixels_per_point(), self.egui_ctx.pixels_per_point(),
&textures_delta, &textures_delta,
@ -365,7 +389,7 @@ impl AppRunner {
} }
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { 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 self.screen_reader
.speak(&platform_output.events_description()); .speak(&platform_output.events_description());
} }
@ -377,6 +401,8 @@ impl AppRunner {
events: _, // already handled events: _, // already handled
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update: _, // not currently implemented
} = platform_output; } = platform_output;
set_cursor_icon(cursor_icon); set_cursor_icon(cursor_icon);
@ -424,8 +450,6 @@ pub enum EventToUnsubscribe {
impl EventToUnsubscribe { impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> { pub fn unsubscribe(self) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
match self { match self {
EventToUnsubscribe::TargetEvent(handle) => { EventToUnsubscribe::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback( handle.target.remove_event_listener_with_callback(
@ -442,6 +466,7 @@ impl EventToUnsubscribe {
} }
} }
} }
pub struct AppRunnerContainer { pub struct AppRunnerContainer {
pub runner: AppRunnerRef, pub runner: AppRunnerRef,
@ -454,17 +479,12 @@ pub struct AppRunnerContainer {
impl AppRunnerContainer { impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers /// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way /// are dealt with in the same way
///
#[must_use]
pub fn add_event_listener<E: wasm_bindgen::JsCast>( pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&mut self, &mut self,
target: &EventTarget, target: &EventTarget,
event_name: &'static str, event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static, mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
// Create a JS closure based on the FnMut provided // Create a JS closure based on the FnMut provided
let closure = Closure::wrap({ let closure = Closure::wrap({
// Clone atomics // Clone atomics
@ -487,7 +507,7 @@ impl AppRunnerContainer {
let handle = TargetEvent { let handle = TargetEvent {
target: target.clone(), target: target.clone(),
event_name: event_name.to_string(), event_name: event_name.to_owned(),
closure, closure,
}; };
@ -501,12 +521,17 @@ impl AppRunnerContainer {
/// Install event listeners to register different input events /// Install event listeners to register different input events
/// and start running the given app. /// and start running the given app.
pub fn start( pub async fn start(
canvas_id: &str, canvas_id: &str,
web_options: crate::WebOptions, web_options: crate::WebOptions,
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
) -> Result<AppRunnerRef, JsValue> { ) -> Result<AppRunnerRef, JsValue> {
let mut runner = AppRunner::new(canvas_id, web_options, app_creator)?; #[cfg(not(web_sys_unstable_apis))]
tracing::warn!(
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
);
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
runner.warm_up()?; runner.warm_up()?;
start_runner(runner) start_runner(runner)
} }

View file

@ -1,6 +1,9 @@
use super::*;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
struct IsDestroyed(pub bool); struct IsDestroyed(pub bool);
pub fn paint_and_schedule( pub fn paint_and_schedule(
@ -13,7 +16,6 @@ pub fn paint_and_schedule(
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() { if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
runner_lock.needs_repaint.clear(); runner_lock.needs_repaint.clear();
runner_lock.clear_color_buffer();
let (repaint_after, clipped_primitives) = runner_lock.logic()?; let (repaint_after, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?; runner_lock.paint(&clipped_primitives)?;
runner_lock runner_lock
@ -29,7 +31,6 @@ pub fn paint_and_schedule(
runner_ref: AppRunnerRef, runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>, panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked)); let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
window.request_animation_frame(closure.as_ref().unchecked_ref())?; window.request_animation_frame(closure.as_ref().unchecked_ref())?;
@ -65,11 +66,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.modifiers = modifiers; runner_lock.input.raw.modifiers = modifiers;
let key = event.key(); 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 { runner_lock.input.raw.events.push(egui::Event::Key {
key, key,
pressed: true, pressed: true,
repeat: false, // egui will fill this in for us!
modifiers, modifiers,
}); });
} }
@ -85,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 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. // Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field. // egui wants to use tab to move to the next text field.
true 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 { } else if egui_wants_keyboard {
matches!( matches!(
event.key().as_str(), event.key().as_str(),
@ -112,6 +123,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
if prevent_default { if prevent_default {
event.prevent_default(); event.prevent_default();
// event.stop_propagation();
} }
}, },
)?; )?;
@ -126,6 +138,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.events.push(egui::Event::Key { runner_lock.input.raw.events.push(egui::Event::Key {
key, key,
pressed: false, pressed: false,
repeat: false,
modifiers, modifiers,
}); });
} }
@ -197,15 +210,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{ let prevent_default_events = [
// By default, right-clicks open a context menu. // By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui): // 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 = let closure =
move |event: web_sys::MouseEvent, move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<AppRunner>| { mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
event.prevent_default(); event.prevent_default();
// event.stop_propagation();
// tracing::debug!("Preventing event {:?}", event_name);
}; };
runner_container.add_event_listener(&canvas, event_name, closure)?; runner_container.add_event_listener(&canvas, event_name, closure)?;
@ -214,7 +233,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
runner_container.add_event_listener( runner_container.add_event_listener(
&canvas, &canvas,
"mousedown", "mousedown",
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<AppRunner>| { |event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
if let Some(button) = button_from_mouse_event(&event) { if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers; let modifiers = runner_lock.input.raw.modifiers;
@ -309,7 +328,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
modifiers, modifiers,
}); });
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event); push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.repaint_asap(); runner_lock.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -331,7 +350,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
.events .events
.push(egui::Event::PointerMoved(pos)); .push(egui::Event::PointerMoved(pos));
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event); push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.repaint_asap(); runner_lock.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -358,7 +377,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
// Then remove hover effect: // Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone); runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event); push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.repaint_asap(); runner_lock.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -389,7 +408,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
} }
web_sys::WheelEvent::DOM_DELTA_LINE => { web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)] #[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 points_per_scroll_line
} }
_ => 1.0, // DOM_DELTA_PIXEL _ => 1.0, // DOM_DELTA_PIXEL

View file

@ -54,8 +54,8 @@ pub fn pos_from_touch_event(
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 { egui::Pos2 {
x: touch.page_x() as f32 - canvas_origin.x as f32, x: touch.page_x() as f32 - canvas_origin.x,
y: touch.page_y() as f32 - canvas_origin.y as f32, y: touch.page_y() as f32 - canvas_origin.y,
} }
} }
@ -113,83 +113,88 @@ pub fn should_ignore_key(key: &str) -> bool {
/// Web sends all all keys as strings, so it is up to us to figure out if it is /// Web sends all all keys as strings, so it is up to us to figure out if it is
/// a real text input or the name of a key. /// a real text input or the name of a key.
pub fn translate_key(key: &str) -> Option<egui::Key> { pub fn translate_key(key: &str) -> Option<egui::Key> {
use egui::Key;
match key { match key {
"ArrowDown" => Some(egui::Key::ArrowDown), "ArrowDown" => Some(Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft), "ArrowLeft" => Some(Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight), "ArrowRight" => Some(Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp), "ArrowUp" => Some(Key::ArrowUp),
"Esc" | "Escape" => Some(egui::Key::Escape), "Esc" | "Escape" => Some(Key::Escape),
"Tab" => Some(egui::Key::Tab), "Tab" => Some(Key::Tab),
"Backspace" => Some(egui::Key::Backspace), "Backspace" => Some(Key::Backspace),
"Enter" => Some(egui::Key::Enter), "Enter" => Some(Key::Enter),
"Space" | " " => Some(egui::Key::Space), "Space" | " " => Some(Key::Space),
"Help" | "Insert" => Some(egui::Key::Insert), "Help" | "Insert" => Some(Key::Insert),
"Delete" => Some(egui::Key::Delete), "Delete" => Some(Key::Delete),
"Home" => Some(egui::Key::Home), "Home" => Some(Key::Home),
"End" => Some(egui::Key::End), "End" => Some(Key::End),
"PageUp" => Some(egui::Key::PageUp), "PageUp" => Some(Key::PageUp),
"PageDown" => Some(egui::Key::PageDown), "PageDown" => Some(Key::PageDown),
"0" => Some(egui::Key::Num0), "-" => Some(Key::Minus),
"1" => Some(egui::Key::Num1), "+" | "=" => Some(Key::PlusEquals),
"2" => Some(egui::Key::Num2),
"3" => Some(egui::Key::Num3),
"4" => Some(egui::Key::Num4),
"5" => Some(egui::Key::Num5),
"6" => Some(egui::Key::Num6),
"7" => Some(egui::Key::Num7),
"8" => Some(egui::Key::Num8),
"9" => Some(egui::Key::Num9),
"a" | "A" => Some(egui::Key::A), "0" => Some(Key::Num0),
"b" | "B" => Some(egui::Key::B), "1" => Some(Key::Num1),
"c" | "C" => Some(egui::Key::C), "2" => Some(Key::Num2),
"d" | "D" => Some(egui::Key::D), "3" => Some(Key::Num3),
"e" | "E" => Some(egui::Key::E), "4" => Some(Key::Num4),
"f" | "F" => Some(egui::Key::F), "5" => Some(Key::Num5),
"g" | "G" => Some(egui::Key::G), "6" => Some(Key::Num6),
"h" | "H" => Some(egui::Key::H), "7" => Some(Key::Num7),
"i" | "I" => Some(egui::Key::I), "8" => Some(Key::Num8),
"j" | "J" => Some(egui::Key::J), "9" => Some(Key::Num9),
"k" | "K" => Some(egui::Key::K),
"l" | "L" => Some(egui::Key::L),
"m" | "M" => Some(egui::Key::M),
"n" | "N" => Some(egui::Key::N),
"o" | "O" => Some(egui::Key::O),
"p" | "P" => Some(egui::Key::P),
"q" | "Q" => Some(egui::Key::Q),
"r" | "R" => Some(egui::Key::R),
"s" | "S" => Some(egui::Key::S),
"t" | "T" => Some(egui::Key::T),
"u" | "U" => Some(egui::Key::U),
"v" | "V" => Some(egui::Key::V),
"w" | "W" => Some(egui::Key::W),
"x" | "X" => Some(egui::Key::X),
"y" | "Y" => Some(egui::Key::Y),
"z" | "Z" => Some(egui::Key::Z),
"F1" => Some(egui::Key::F1), "a" | "A" => Some(Key::A),
"F2" => Some(egui::Key::F2), "b" | "B" => Some(Key::B),
"F3" => Some(egui::Key::F3), "c" | "C" => Some(Key::C),
"F4" => Some(egui::Key::F4), "d" | "D" => Some(Key::D),
"F5" => Some(egui::Key::F5), "e" | "E" => Some(Key::E),
"F6" => Some(egui::Key::F6), "f" | "F" => Some(Key::F),
"F7" => Some(egui::Key::F7), "g" | "G" => Some(Key::G),
"F8" => Some(egui::Key::F8), "h" | "H" => Some(Key::H),
"F9" => Some(egui::Key::F9), "i" | "I" => Some(Key::I),
"F10" => Some(egui::Key::F10), "j" | "J" => Some(Key::J),
"F11" => Some(egui::Key::F11), "k" | "K" => Some(Key::K),
"F12" => Some(egui::Key::F12), "l" | "L" => Some(Key::L),
"F13" => Some(egui::Key::F13), "m" | "M" => Some(Key::M),
"F14" => Some(egui::Key::F14), "n" | "N" => Some(Key::N),
"F15" => Some(egui::Key::F15), "o" | "O" => Some(Key::O),
"F16" => Some(egui::Key::F16), "p" | "P" => Some(Key::P),
"F17" => Some(egui::Key::F17), "q" | "Q" => Some(Key::Q),
"F18" => Some(egui::Key::F18), "r" | "R" => Some(Key::R),
"F19" => Some(egui::Key::F19), "s" | "S" => Some(Key::S),
"F20" => Some(egui::Key::F20), "t" | "T" => Some(Key::T),
"u" | "U" => Some(Key::U),
"v" | "V" => Some(Key::V),
"w" | "W" => Some(Key::W),
"x" | "X" => Some(Key::X),
"y" | "Y" => Some(Key::Y),
"z" | "Z" => Some(Key::Z),
"F1" => Some(Key::F1),
"F2" => Some(Key::F2),
"F3" => Some(Key::F3),
"F4" => Some(Key::F4),
"F5" => Some(Key::F5),
"F6" => Some(Key::F6),
"F7" => Some(Key::F7),
"F8" => Some(Key::F8),
"F9" => Some(Key::F9),
"F10" => Some(Key::F10),
"F11" => Some(Key::F11),
"F12" => Some(Key::F12),
"F13" => Some(Key::F13),
"F14" => Some(Key::F14),
"F15" => Some(Key::F15),
"F16" => Some(Key::F16),
"F17" => Some(Key::F17),
"F18" => Some(Key::F18),
"F19" => Some(Key::F19),
"F20" => Some(Key::F20),
_ => None, _ => None,
} }

View file

@ -4,12 +4,26 @@
pub mod backend; pub mod backend;
mod events; mod events;
mod glow_wrapping;
mod input; mod input;
pub mod screen_reader; pub mod screen_reader;
pub mod storage; pub mod storage;
mod text_agent; mod text_agent;
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
mod web_painter;
#[cfg(feature = "glow")]
mod web_painter_glow;
#[cfg(feature = "glow")]
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
#[cfg(feature = "wgpu")]
mod web_painter_wgpu;
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*; pub use backend::*;
pub use events::*; pub use events::*;
pub use storage::*; pub use storage::*;
@ -69,7 +83,6 @@ pub fn system_theme() -> Option<Theme> {
} }
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> { pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
use wasm_bindgen::JsCast;
let document = web_sys::window()?.document()?; let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?; let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok() canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
@ -77,7 +90,7 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
canvas_element(canvas_id) canvas_element(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id)) .unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
} }
fn canvas_origin(canvas_id: &str) -> egui::Pos2 { fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
@ -108,6 +121,10 @@ pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2
y: height as f32, y: height as f32,
}; };
if width <= 0 || height <= 0 {
tracing::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height);
}
let pixels_per_point = native_pixels_per_point(); let pixels_per_point = native_pixels_per_point();
let max_size_pixels = pixels_per_point * max_size_points; let max_size_pixels = pixels_per_point * max_size_points;
@ -239,47 +256,3 @@ pub fn percent_decode(s: &str) -> String {
.decode_utf8_lossy() .decode_utf8_lossy()
.to_string() .to_string()
} }
// ----------------------------------------------------------------------------
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794
// detect WebKitGTK
// WebKitGTK use WebKit default unmasked vendor and renderer
// but safari use same vendor and renderer
// so exclude "Mac OS X" user-agent.
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
}
/// detecting Safari and `webkitGTK`.
///
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
///
/// If we detect safari or `webkitGTKs` returns true.
///
/// This function used to avoid displaying linear color with `sRGB` supported systems.
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
// TODO(emilk): do something smart based on user agent?
if gl
.get_extension("WEBGL_debug_renderer_info")
.unwrap()
.is_some()
{
if let Ok(renderer) =
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
{
if let Some(renderer) = renderer.as_string() {
if renderer.contains("Apple") {
return true;
}
}
}
}
false
}

View file

@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
if let Some(memory_string) = local_storage_get("egui_memory_ron") { if let Some(memory_string) = local_storage_get("egui_memory_ron") {
match ron::from_str(&memory_string) { match ron::from_str(&memory_string) {
Ok(memory) => { Ok(memory) => {
*ctx.memory() = memory; ctx.memory_mut(|m| *m = memory);
} }
Err(err) => { Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err); tracing::error!("Failed to parse memory RON: {}", err);
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) { pub fn save_memory(ctx: &egui::Context) {
match ron::to_string(&*ctx.memory()) { match ctx.memory(|mem| ron::to_string(mem)) {
Ok(ron) => { Ok(ron) => {
local_storage_set("egui_memory_ron", &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"; static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement { pub fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window() web_sys::window()
.unwrap() .unwrap()
.document() .document()
@ -23,7 +22,6 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
/// Text event handler, /// Text event handler,
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let document = window.document().unwrap(); let document = window.document().unwrap();
let body = document.body().expect("document should have a body"); 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. /// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> { pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement; use web_sys::HtmlInputElement;
let window = web_sys::window()?; let window = web_sys::window()?;
let document = window.document()?; let document = window.document()?;

View file

@ -0,0 +1,29 @@
use wasm_bindgen::JsValue;
/// Renderer for a browser canvas.
/// As of writing we're not allowing to decide on the painter at runtime,
/// therefore this trait is merely there for specifying and documenting the interface.
pub(crate) trait WebPainter {
// Create a new web painter targeting a given canvas.
// fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String>
// where
// Self: Sized;
/// Id of the canvas in use.
fn canvas_id(&self) -> &str;
/// Maximum size of a texture in one direction.
fn max_texture_side(&self) -> usize;
/// Update all internal textures and paint gui.
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue>;
/// Destroy all resources.
fn destroy(&mut self);
}

View file

@ -1,27 +1,32 @@
use crate::WebGlContextOption;
use egui::{ClippedPrimitive, Rgba};
use egui_glow::glow;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement; use web_sys::HtmlCanvasElement;
#[cfg(not(target_arch = "wasm32"))]
use web_sys::{WebGl2RenderingContext, WebGlRenderingContext};
pub(crate) struct WrappedGlowPainter { use egui_glow::glow;
pub(crate) canvas: HtmlCanvasElement,
pub(crate) canvas_id: String, use crate::{WebGlContextOption, WebOptions};
pub(crate) painter: egui_glow::Painter,
use super::web_painter::WebPainter;
pub(crate) struct WebPainterGlow {
canvas: HtmlCanvasElement,
canvas_id: String,
painter: egui_glow::Painter,
} }
impl WrappedGlowPainter { impl WebPainterGlow {
pub fn new(canvas_id: &str, options: WebGlContextOption) -> Result<Self, String> { pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
self.painter.gl()
}
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
let canvas = super::canvas_element_or_die(canvas_id); let canvas = super::canvas_element_or_die(canvas_id);
let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options)?; let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
let gl = std::sync::Arc::new(gl); let gl = std::sync::Arc::new(gl);
let dimension = [canvas.width() as i32, canvas.height() as i32]; let painter = egui_glow::Painter::new(gl, shader_prefix, None)
let painter = egui_glow::Painter::new(gl, Some(dimension), shader_prefix)
.map_err(|error| format!("Error starting glow painter: {}", error))?; .map_err(|error| format!("Error starting glow painter: {}", error))?;
Ok(Self { Ok(Self {
@ -32,64 +37,41 @@ impl WrappedGlowPainter {
} }
} }
impl WrappedGlowPainter { impl WebPainter for WebPainterGlow {
pub fn gl(&self) -> &std::sync::Arc<glow::Context> { fn max_texture_side(&self) -> usize {
self.painter.gl()
}
pub fn max_texture_side(&self) -> usize {
self.painter.max_texture_side() self.painter.max_texture_side()
} }
pub fn canvas_id(&self) -> &str { fn canvas_id(&self) -> &str {
&self.canvas_id &self.canvas_id
} }
pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { fn paint_and_update_textures(
self.painter.set_texture(tex_id, delta);
}
pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.painter.free_texture(tex_id);
}
pub fn clear(&self, clear_color: Rgba) {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
}
pub fn paint_primitives(
&mut self,
clipped_primitives: &[ClippedPrimitive],
pixels_per_point: f32,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
Ok(())
}
pub fn paint_and_update_textures(
&mut self, &mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive], clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32, pixels_per_point: f32,
textures_delta: &egui::TexturesDelta, textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
for (id, image_delta) in &textures_delta.set { for (id, image_delta) in &textures_delta.set {
self.set_texture(*id, image_delta); self.painter.set_texture(*id, image_delta);
} }
self.paint_primitives(clipped_primitives, pixels_per_point)?; egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
for &id in &textures_delta.free { for &id in &textures_delta.free {
self.free_texture(id); self.painter.free_texture(id);
} }
Ok(()) Ok(())
} }
pub fn destroy(&mut self) { fn destroy(&mut self) {
self.painter.destroy() self.painter.destroy();
} }
} }
@ -130,7 +112,7 @@ fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st
.dyn_into::<web_sys::WebGlRenderingContext>() .dyn_into::<web_sys::WebGlRenderingContext>()
.unwrap(); .unwrap();
let shader_prefix = if super::webgl1_requires_brightening(&gl1_ctx) { let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) {
tracing::debug!("Enabling webkitGTK brightening workaround."); tracing::debug!("Enabling webkitGTK brightening workaround.");
"#define APPLY_BRIGHTENING_GAMMA" "#define APPLY_BRIGHTENING_GAMMA"
} else { } else {
@ -159,8 +141,44 @@ fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st
Some((gl, shader_prefix)) Some((gl, shader_prefix))
} }
trait DummyWebGLConstructor { fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
fn from_webgl1_context(context: web_sys::WebGlRenderingContext) -> Self; // See https://github.com/emilk/egui/issues/794
fn from_webgl2_context(context: web_sys::WebGl2RenderingContext) -> Self; // detect WebKitGTK
// WebKitGTK use WebKit default unmasked vendor and renderer
// but safari use same vendor and renderer
// so exclude "Mac OS X" user-agent.
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
}
/// detecting Safari and `webkitGTK`.
///
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
///
/// If we detect safari or `webkitGTKs` returns true.
///
/// This function used to avoid displaying linear color with `sRGB` supported systems.
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
// TODO(emilk): do something smart based on user agent?
if gl
.get_extension("WEBGL_debug_renderer_info")
.unwrap()
.is_some()
{
if let Ok(renderer) =
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
{
if let Some(renderer) = renderer.as_string() {
if renderer.contains("Apple") {
return true;
}
}
}
}
false
} }

View file

@ -0,0 +1,282 @@
use std::sync::Arc;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui::mutex::RwLock;
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
use crate::WebOptions;
use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
canvas_id: String,
surface: wgpu::Surface,
surface_configuration: wgpu::SurfaceConfiguration,
limits: wgpu::Limits,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
pub fn generate_depth_texture_view(
&self,
render_state: &RenderState,
width_in_pixels: u32,
height_in_pixels: u32,
) -> Option<wgpu::TextureView> {
let device = &render_state.device;
self.depth_format.map(|depth_format| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
width: width_in_pixels,
height: height_in_pixels,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
tracing::debug!("Creating wgpu painter");
let canvas = super::canvas_element_or_die(canvas_id);
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 {
power_preference: options.wgpu_options.power_preference,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?;
let (device, queue) = adapter
.request_device(
&options.wgpu_options.device_descriptor,
None, // Capture doesn't work in the browser environment.
)
.await
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
let target_format =
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);
let render_state = RenderState {
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
renderer: Arc::new(RwLock::new(renderer)),
};
let surface_configuration = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: target_format,
width: 0,
height: 0,
present_mode: options.wgpu_options.present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![target_format],
};
tracing::debug!("wgpu painter initialized.");
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
render_state: Some(render_state),
surface,
surface_configuration,
depth_format,
depth_texture_view: None,
limits: options.wgpu_options.device_descriptor.limits.clone(),
on_surface_error: options.wgpu_options.on_surface_error.clone(),
})
}
}
impl WebPainter for WebPainterWgpu {
fn canvas_id(&self) -> &str {
&self.canvas_id
}
fn max_texture_side(&self) -> usize {
self.limits.max_texture_dimension_2d as _
}
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> {
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
let render_state = if let Some(render_state) = &self.render_state {
render_state
} else {
return Err(JsValue::from_str(
"Can't paint, wgpu renderer was already disposed",
));
};
let mut encoder =
render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("egui_webpainter_paint_and_update_textures"),
});
// Upload all resources for the GPU.
let screen_descriptor = ScreenDescriptor {
size_in_pixels,
pixels_per_point,
};
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
renderer.update_buffers(
&render_state.device,
&render_state.queue,
&mut encoder,
clipped_primitives,
&screen_descriptor,
)
};
// Resize surface if needed
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
let frame = if is_zero_sized_surface {
None
} else {
if size_in_pixels[0] != self.surface_configuration.width
|| size_in_pixels[1] != self.surface_configuration.height
{
self.surface_configuration.width = size_in_pixels[0];
self.surface_configuration.height = size_in_pixels[1];
self.surface
.configure(&render_state.device, &self.surface_configuration);
self.depth_texture_view = self.generate_depth_texture_view(
render_state,
size_in_pixels[0],
size_in_pixels[1],
);
}
let frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
#[allow(clippy::single_match_else)]
Err(e) => match (*self.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
return Ok(());
}
SurfaceErrorAction::SkipFrame => {
return Ok(());
}
},
};
{
let renderer = render_state.renderer.read();
let frame_view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
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,
},
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: false,
}),
stencil_ops: None,
}
}),
label: Some("egui_render"),
});
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
}
Some(frame)
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state.queue.submit(
user_cmd_bufs
.into_iter()
.chain(std::iter::once(encoder.finish())),
);
if let Some(frame) = frame {
frame.present();
}
Ok(())
}
fn destroy(&mut self) {
self.render_state = None;
}
}

View file

@ -0,0 +1,37 @@
# Changelog for egui-wgpu
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`.
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
* Reexported `Renderer`.
* You can now use `egui-wgpu` on web, using WebGL ([#2107](https://github.com/emilk/egui/pull/2107)).
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
* Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)).
* `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198))
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
## 0.19.0 - 2022-08-20
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
* Make `RenderPass` `Send` and `Sync` ([#1883](https://github.com/emilk/egui/pull/1883)).
## 0.18.0 - 2022-05-15
First published version since moving the code into the `egui` repository from <https://github.com/LU15W1R7H/eww>.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui-wgpu" name = "egui-wgpu"
version = "0.18.0" version = "0.21.0"
description = "Bindings for using egui natively using the wgpu library" description = "Bindings for using egui natively using the wgpu library"
authors = [ authors = [
"Nils Hasenbanck <nils@hasenbanck.de>", "Nils Hasenbanck <nils@hasenbanck.de>",
@ -8,11 +8,11 @@ authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
] ]
edition = "2021" edition = "2021"
rust-version = "1.61" rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/egui-wgpu" homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/egui-wgpu" repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
categories = ["gui", "game-development"] categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"] keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [ include = [
@ -28,23 +28,29 @@ all-features = true
[features] [features]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Enable [`winit`](https://docs.rs/winit) integration. ## Enable [`winit`](https://docs.rs/winit) integration.
winit = ["dep:pollster", "dep:winit"] winit = ["dep:winit"]
[dependencies] [dependencies]
egui = { version = "0.18.1", path = "../egui", default-features = false, features = [ epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
"bytemuck", "bytemuck",
] } ] }
bytemuck = "1.7" bytemuck = "1.7"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
type-map = "0.5.0" type-map = "0.5.0"
wgpu = "0.13" wgpu = "0.15.0"
#! ### Optional dependencies #! ### Optional dependencies
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
pollster = { version = "0.2", optional = true } winit = { version = "0.28", optional = true }
winit = { version = "0.27.2", optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = { version = "0.14", optional = true }

View file

@ -0,0 +1,91 @@
// Vertex shader bindings
struct VertexOutput {
@location(0) tex_coord: vec2<f32>,
@location(1) color: vec4<f32>, // gamma 0-1
@builtin(position) position: vec4<f32>,
};
struct Locals {
screen_size: vec2<f32>,
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: vec2<u32>,
};
@group(0) @binding(0) var<uniform> r_locals: Locals;
// 0-1 linear from 0-1 sRGB gamma
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
let lower = srgb / vec3<f32>(12.92);
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
return select(higher, lower, cutoff);
}
// 0-1 sRGB gamma from 0-1 linear
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
let cutoff = rgb < vec3<f32>(0.0031308);
let lower = rgb * vec3<f32>(12.92);
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
return select(higher, lower, cutoff);
}
// 0-1 sRGBA gamma from 0-1 linear
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
}
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
fn unpack_color(color: u32) -> vec4<f32> {
return vec4<f32>(
f32(color & 255u),
f32((color >> 8u) & 255u),
f32((color >> 16u) & 255u),
f32((color >> 24u) & 255u),
) / 255.0;
}
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
return vec4<f32>(
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
0.0,
1.0,
);
}
@vertex
fn vs_main(
@location(0) a_pos: vec2<f32>,
@location(1) a_tex_coord: vec2<f32>,
@location(2) a_color: u32,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coord = a_tex_coord;
out.color = unpack_color(a_color);
out.position = position_from_screen(a_pos);
return out;
}
// Fragment shader bindings
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return vec4<f32>(linear_from_gamma_rgb(out_color_gamma.rgb), out_color_gamma.a);
}
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return out_color_gamma;
}

155
crates/egui-wgpu/src/lib.rs Normal file
View file

@ -0,0 +1,155 @@
//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(unsafe_code)]
pub use 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`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
use std::sync::Arc;
use epaint::mutex::RwLock;
/// Access to the render state for egui.
#[derive(Clone)]
pub struct RenderState {
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: wgpu::TextureFormat,
pub renderer: Arc<RwLock<Renderer>>,
}
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
/// Instructs egui to recreate the surface, then skip the current frame.
RecreateSurface,
}
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
#[derive(Clone)]
pub struct WgpuConfiguration {
/// Configuration passed on device request.
pub device_descriptor: wgpu::DeviceDescriptor<'static>,
/// Backends that should be supported (wgpu will pick one of these)
pub backends: wgpu::Backends,
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Power preference for the adapter.
pub power_preference: wgpu::PowerPreference,
/// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
pub depth_format: Option<wgpu::TextureFormat>,
}
impl Default for WgpuConfiguration {
fn default() -> Self {
Self {
device_descriptor: wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
features: wgpu::Features::default(),
limits: wgpu::Limits::default(),
},
backends: wgpu::Backends::PRIMARY | wgpu::Backends::GL,
present_mode: wgpu::PresentMode::AutoVsync,
power_preference: wgpu::PowerPreference::HighPerformance,
depth_format: None,
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
} else {
tracing::warn!("Dropped frame with error: {err}");
}
SurfaceErrorAction::SkipFrame
}),
}
}
}
/// Find the framebuffer format that egui prefers
pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
for &format in formats {
if matches!(
format,
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
) {
return format;
}
}
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"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;

View file

@ -1,38 +1,47 @@
#![allow(unsafe_code)] #![allow(unsafe_code)]
use std::num::NonZeroU64;
use std::ops::Range;
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32}; use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
use egui::{epaint::Primitive, NumExt, PaintCallbackInfo};
use type_map::concurrent::TypeMap; use type_map::concurrent::TypeMap;
use wgpu; use wgpu;
use wgpu::util::DeviceExt as _; 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. /// rendering.
/// ///
/// The callback is composed of two functions: `prepare` and `paint`. /// The callback is composed of two functions: `prepare` and `paint`:
/// /// - `prepare` is called every frame before `paint`, and can use the passed-in
/// `prepare` is called every frame before `paint`, and can use the passed-in [`wgpu::Device`] and /// [`wgpu::Device`] and [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers.
/// [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers. /// - `paint` is called after `prepare` and is given access to the [`wgpu::RenderPass`] so
/// /// that it can issue draw commands into the same [`wgpu::RenderPass`] that is used for
/// `paint` is called after `prepare` and is given access to the the [`wgpu::RenderPass`] so that it /// all other egui elements.
/// can issue draw commands.
/// ///
/// The final argument of both the `prepare` and `paint` callbacks is a the /// The final argument of both the `prepare` and `paint` callbacks is a the
/// [`paint_callback_resources`][crate::renderer::RenderPass::paint_callback_resources]. /// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources].
/// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to /// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to
/// store buffers, pipelines, and other information that needs to be accessed during the render /// store buffers, pipelines, and other information that needs to be accessed during the render
/// pass. /// pass.
/// ///
/// # Example /// # Example
/// ///
/// See the [`custom3d_glow`](https://github.com/emilk/egui/blob/master/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example. /// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub struct CallbackFn { pub struct CallbackFn {
prepare: Box<PrepareCallback>, prepare: Box<PrepareCallback>,
paint: Box<PaintCallback>, paint: Box<PaintCallback>,
} }
type PrepareCallback = dyn Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send; type PrepareCallback = dyn Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send;
type PaintCallback = type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send; dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
@ -40,7 +49,7 @@ type PaintCallback =
impl Default for CallbackFn { impl Default for CallbackFn {
fn default() -> Self { fn default() -> Self {
CallbackFn { CallbackFn {
prepare: Box::new(|_, _, _| ()), prepare: Box::new(|_, _, _, _| Vec::new()),
paint: Box::new(|_, _, _| ()), paint: Box::new(|_, _, _| ()),
} }
} }
@ -51,10 +60,32 @@ impl CallbackFn {
Self::default() Self::default()
} }
/// Set the prepare callback /// Set the prepare callback.
///
/// The passed-in `CommandEncoder` is egui's and can be used directly to register
/// wgpu commands for simple use cases.
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
/// rendering itself.
///
/// For more complicated use cases, one can also return a list of arbitrary
/// `CommandBuffer`s and have complete control over how they get created and fed.
/// In particular, this gives an opportunity to parallelize command registration and
/// prevents a faulty callback from poisoning the main wgpu pipeline.
///
/// When using eframe, the main egui command buffer, as well as all user-defined
/// command buffers returned by this function, are guaranteed to all be submitted
/// at once in a single call.
pub fn prepare<F>(mut self, prepare: F) -> Self pub fn prepare<F>(mut self, prepare: F) -> Self
where where
F: Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send + 'static, F: Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send
+ 'static,
{ {
self.prepare = Box::new(prepare) as _; self.prepare = Box::new(prepare) as _;
self self
@ -73,14 +104,6 @@ impl CallbackFn {
} }
} }
/// Enum for selecting the right buffer type.
#[derive(Debug)]
enum BufferType {
Uniform,
Index,
Vertex,
}
/// Information about the screen used for rendering. /// Information about the screen used for rendering.
pub struct ScreenDescriptor { pub struct ScreenDescriptor {
/// Size of the window in physical pixels. /// Size of the window in physical pixels.
@ -110,43 +133,50 @@ struct UniformBuffer {
_padding: [u32; 2], _padding: [u32; 2],
} }
/// Wraps the buffers and includes additional information. struct SlicedBuffer {
#[derive(Debug)]
struct SizedBuffer {
buffer: wgpu::Buffer, buffer: wgpu::Buffer,
/// number of bytes slices: Vec<Range<wgpu::BufferAddress>>,
size: usize, capacity: wgpu::BufferAddress,
} }
/// Render pass to render a egui based GUI. /// Renderer for a egui based GUI.
pub struct RenderPass { pub struct Renderer {
render_pipeline: wgpu::RenderPipeline, pipeline: wgpu::RenderPipeline,
index_buffers: Vec<SizedBuffer>,
vertex_buffers: Vec<SizedBuffer>, index_buffer: SlicedBuffer,
uniform_buffer: SizedBuffer, vertex_buffer: SlicedBuffer,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup, uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout, texture_bind_group_layout: wgpu::BindGroupLayout,
/// Map of egui texture IDs to textures and their associated bindgroups (texture view + /// 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). The texture may be None if the TextureId is just a handle to a user-provided
/// sampler. /// sampler.
textures: HashMap<egui::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>, textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
next_user_texture_id: u64, next_user_texture_id: u64,
/// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
/// pipelines that must have the lifetime of the renderpass. /// pipelines that must have the lifetime of the renderpass.
pub paint_callback_resources: TypeMap, pub paint_callback_resources: TypeMap,
} }
impl RenderPass { impl Renderer {
/// Creates a new render pass to render a egui UI. /// Creates a renderer for a egui UI.
/// ///
/// If the format passed is not a *Srgb format, the shader will automatically convert to `sRGB` colors in the shader. /// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
pub fn new( pub fn new(
device: &wgpu::Device, device: &wgpu::Device,
output_format: wgpu::TextureFormat, output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32, msaa_samples: u32,
) -> Self { ) -> Self {
crate::profile_function!();
let shader = wgpu::ShaderModuleDescriptor { let shader = wgpu::ShaderModuleDescriptor {
label: Some("egui_shader"), label: Some("egui"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
}; };
let module = device.create_shader_module(shader); let module = device.create_shader_module(shader);
@ -159,10 +189,6 @@ impl RenderPass {
}]), }]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
}); });
let uniform_buffer = SizedBuffer {
buffer: uniform_buffer,
size: std::mem::size_of::<UniformBuffer>(),
};
let uniform_bind_group_layout = let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -172,7 +198,7 @@ impl RenderPass {
visibility: wgpu::ShaderStages::VERTEX, visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false, has_dynamic_offset: false,
min_binding_size: None, min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
}, },
count: None, count: None,
@ -185,7 +211,7 @@ impl RenderPass {
entries: &[wgpu::BindGroupEntry { entries: &[wgpu::BindGroupEntry {
binding: 0, binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &uniform_buffer.buffer, buffer: &uniform_buffer,
offset: 0, offset: 0,
size: None, size: None,
}), }),
@ -221,15 +247,19 @@ impl RenderPass {
push_constant_ranges: &[], push_constant_ranges: &[],
}); });
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("egui_pipeline"), label: Some("egui_pipeline"),
layout: Some(&pipeline_layout), layout: Some(&pipeline_layout),
vertex: wgpu::VertexState { vertex: wgpu::VertexState {
entry_point: if output_format.describe().srgb { entry_point: "vs_main",
"vs_main"
} else {
"vs_conv_main"
},
module: &module, module: &module,
buffers: &[wgpu::VertexBufferLayout { buffers: &[wgpu::VertexBufferLayout {
array_stride: 5 * 4, array_stride: 5 * 4,
@ -249,7 +279,7 @@ impl RenderPass {
polygon_mode: wgpu::PolygonMode::default(), polygon_mode: wgpu::PolygonMode::default(),
strip_index_format: None, strip_index_format: None,
}, },
depth_stencil: None, depth_stencil,
multisample: wgpu::MultisampleState { multisample: wgpu::MultisampleState {
alpha_to_coverage_enabled: false, alpha_to_coverage_enabled: false,
count: msaa_samples, count: msaa_samples,
@ -258,9 +288,14 @@ impl RenderPass {
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
module: &module, module: &module,
entry_point: "fs_main", entry_point: if output_color_format.describe().srgb {
tracing::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
"fs_main_linear_framebuffer"
} else {
"fs_main_gamma_framebuffer" // this is what we prefer
},
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: output_format, format: output_color_format,
blend: Some(wgpu::BlendState { blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent { color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One, src_factor: wgpu::BlendFactor::One,
@ -279,77 +314,59 @@ impl RenderPass {
multiview: None, multiview: None,
}); });
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<Vertex>() * 1024) as _;
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<u32>() * 1024 * 3) as _;
Self { Self {
render_pipeline, pipeline,
vertex_buffers: Vec::with_capacity(64), vertex_buffer: SlicedBuffer {
index_buffers: Vec::with_capacity(64), buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
slices: Vec::with_capacity(64),
capacity: VERTEX_BUFFER_START_CAPACITY,
},
index_buffer: SlicedBuffer {
buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
slices: Vec::with_capacity(64),
capacity: INDEX_BUFFER_START_CAPACITY,
},
uniform_buffer, uniform_buffer,
uniform_bind_group, uniform_bind_group,
texture_bind_group_layout, texture_bind_group_layout,
textures: HashMap::new(), textures: HashMap::new(),
next_user_texture_id: 0, next_user_texture_id: 0,
samplers: HashMap::new(),
paint_callback_resources: TypeMap::default(), paint_callback_resources: TypeMap::default(),
} }
} }
/// Executes the egui render pass. /// Executes the egui renderer onto an existing wgpu renderpass.
pub fn execute( pub fn render<'rp>(
&self, &'rp self,
encoder: &mut wgpu::CommandEncoder, render_pass: &mut wgpu::RenderPass<'rp>,
color_attachment: &wgpu::TextureView, paint_jobs: &[epaint::ClippedPrimitive],
paint_jobs: &[egui::epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
clear_color: Option<wgpu::Color>,
) {
let load_operation = if let Some(color) = clear_color {
wgpu::LoadOp::Clear(color)
} else {
wgpu::LoadOp::Load
};
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_attachment,
resolve_target: None,
ops: wgpu::Operations {
load: load_operation,
store: true,
},
})],
depth_stencil_attachment: None,
label: Some("egui main render pass"),
});
rpass.push_debug_group("egui_pass");
self.execute_with_renderpass(&mut rpass, paint_jobs, screen_descriptor);
rpass.pop_debug_group();
}
/// Executes the egui render pass onto an existing wgpu renderpass.
pub fn execute_with_renderpass<'rpass>(
&'rpass self,
rpass: &mut wgpu::RenderPass<'rpass>,
paint_jobs: &[egui::epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor, screen_descriptor: &ScreenDescriptor,
) { ) {
crate::profile_function!();
let pixels_per_point = screen_descriptor.pixels_per_point; let pixels_per_point = screen_descriptor.pixels_per_point;
let size_in_pixels = screen_descriptor.size_in_pixels; let size_in_pixels = screen_descriptor.size_in_pixels;
// Whether or not we need to reset the renderpass state because a paint callback has just // Whether or not we need to reset the render pass because a paint callback has just
// run. // run.
let mut needs_reset = true; let mut needs_reset = true;
let mut index_buffers = self.index_buffers.iter(); let mut index_buffer_slices = self.index_buffer.slices.iter();
let mut vertex_buffers = self.vertex_buffers.iter(); let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
for egui::ClippedPrimitive { for epaint::ClippedPrimitive {
clip_rect, clip_rect,
primitive, primitive,
} in paint_jobs } in paint_jobs
{ {
if needs_reset { if needs_reset {
rpass.set_viewport( render_pass.set_viewport(
0.0, 0.0,
0.0, 0.0,
size_in_pixels[0] as f32, size_in_pixels[0] as f32,
@ -357,8 +374,8 @@ impl RenderPass {
0.0, 0.0,
1.0, 1.0,
); );
rpass.set_pipeline(&self.render_pipeline); render_pass.set_pipeline(&self.pipeline);
rpass.set_bind_group(0, &self.uniform_bind_group, &[]); render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
needs_reset = false; needs_reset = false;
} }
@ -366,31 +383,34 @@ impl RenderPass {
let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels); let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
if rect.width == 0 || rect.height == 0 { if rect.width == 0 || rect.height == 0 {
// Skip rendering with zero-sized clip areas. // Skip rendering zero-sized clip areas.
if let Primitive::Mesh(_) = primitive { if let Primitive::Mesh(_) = primitive {
// If this is a mesh, we need to advance the index and vertex buffer iterators // If this is a mesh, we need to advance the index and vertex buffer iterators:
index_buffers.next().unwrap(); index_buffer_slices.next().unwrap();
vertex_buffers.next().unwrap(); vertex_buffer_slices.next().unwrap();
} }
continue; continue;
} }
rpass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height); render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
} }
match primitive { match primitive {
Primitive::Mesh(mesh) => { Primitive::Mesh(mesh) => {
let index_buffer = index_buffers.next().unwrap(); let index_buffer_slice = index_buffer_slices.next().unwrap();
let vertex_buffer = vertex_buffers.next().unwrap(); let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) {
rpass.set_bind_group(1, bind_group, &[]); render_pass.set_bind_group(1, bind_group, &[]);
rpass.set_index_buffer( render_pass.set_index_buffer(
index_buffer.buffer.slice(..), self.index_buffer.buffer.slice(index_buffer_slice.clone()),
wgpu::IndexFormat::Uint32, wgpu::IndexFormat::Uint32,
); );
rpass.set_vertex_buffer(0, vertex_buffer.buffer.slice(..)); render_pass.set_vertex_buffer(
rpass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1); 0,
self.vertex_buffer.buffer.slice(vertex_buffer_slice.clone()),
);
render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
} else { } else {
tracing::warn!("Missing texture: {:?}", mesh.texture_id); tracing::warn!("Missing texture: {:?}", mesh.texture_id);
} }
@ -404,26 +424,28 @@ impl RenderPass {
}; };
if callback.rect.is_positive() { if callback.rect.is_positive() {
crate::profile_scope!("callback");
needs_reset = true; needs_reset = true;
{ {
// Set the viewport rect // We're setting a default viewport for the render pass as a
// Transform callback rect to physical pixels: // courtesy for the user, so that they don't have to think about
let rect_min_x = pixels_per_point * callback.rect.min.x; // it in the simple case where they just want to fill the whole
let rect_min_y = pixels_per_point * callback.rect.min.y; // paint area.
let rect_max_x = pixels_per_point * callback.rect.max.x; //
let rect_max_y = pixels_per_point * callback.rect.max.y; // The user still has the possibility of setting their own custom
// viewport during the paint callback, effectively overriding this
// one.
let rect_min_x = rect_min_x.round(); let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let rect_min_y = rect_min_y.round(); let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
let rect_max_x = rect_max_x.round();
let rect_max_y = rect_max_y.round();
rpass.set_viewport( render_pass.set_viewport(
rect_min_x, min.x,
rect_min_y, min.y,
rect_max_x - rect_min_x, max.x - min.x,
rect_max_y - rect_min_y, max.y - min.y,
0.0, 0.0,
1.0, 1.0,
); );
@ -436,7 +458,7 @@ impl RenderPass {
pixels_per_point, pixels_per_point,
screen_size_px: size_in_pixels, screen_size_px: size_in_pixels,
}, },
rpass, render_pass,
&self.paint_callback_resources, &self.paint_callback_resources,
); );
} }
@ -444,17 +466,19 @@ impl RenderPass {
} }
} }
rpass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
} }
/// Should be called before `execute()`. /// Should be called before `render()`.
pub fn update_texture( pub fn update_texture(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
id: egui::TextureId, id: epaint::TextureId,
image_delta: &egui::epaint::ImageDelta, image_delta: &epaint::ImageDelta,
) { ) {
crate::profile_function!();
let width = image_delta.image.width() as u32; let width = image_delta.image.width() as u32;
let height = image_delta.image.height() as u32; let height = image_delta.image.height() as u32;
@ -465,7 +489,7 @@ impl RenderPass {
}; };
let data_color32 = match &image_delta.image { let data_color32 = match &image_delta.image {
egui::ImageData::Color(image) => { epaint::ImageData::Color(image) => {
assert_eq!( assert_eq!(
width as usize * height as usize, width as usize * height as usize,
image.pixels.len(), image.pixels.len(),
@ -473,13 +497,13 @@ impl RenderPass {
); );
Cow::Borrowed(&image.pixels) Cow::Borrowed(&image.pixels)
} }
egui::ImageData::Font(image) => { epaint::ImageData::Font(image) => {
assert_eq!( assert_eq!(
width as usize * height as usize, width as usize * height as usize,
image.pixels.len(), image.pixels.len(),
"Mismatch between texture size and texel count" "Mismatch between texture size and texel count"
); );
Cow::Owned(image.srgba_pixels(1.0).collect::<Vec<_>>()) Cow::Owned(image.srgba_pixels(None).collect::<Vec<_>>())
} }
}; };
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@ -519,27 +543,25 @@ impl RenderPass {
); );
} else { } else {
// allocate a new texture // allocate a new texture
// Use same label for all resources associated with this texture id (no point in retyping the type)
let label_str = format!("egui_texid_{:?}", id);
let label = Some(label_str.as_str());
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label: None, label,
size, size,
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, 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, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
}); });
let filter = match image_delta.filter { let sampler = self
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest, .samplers
egui::TextureFilter::Linear => wgpu::FilterMode::Linear, .entry(image_delta.options)
}; .or_insert_with(|| create_sampler(image_delta.options, device));
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: None,
mag_filter: filter,
min_filter: filter,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None, label,
layout: &self.texture_bind_group_layout, layout: &self.texture_bind_group_layout,
entries: &[ entries: &[
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
@ -550,7 +572,7 @@ impl RenderPass {
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 1, binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler), resource: wgpu::BindingResource::Sampler(sampler),
}, },
], ],
}); });
@ -560,7 +582,7 @@ impl RenderPass {
}; };
} }
pub fn free_texture(&mut self, id: &egui::TextureId) { pub fn free_texture(&mut self, id: &epaint::TextureId) {
self.textures.remove(id); self.textures.remove(id);
} }
@ -568,15 +590,15 @@ impl RenderPass {
/// ///
/// This could be used by custom paint hooks to render images that have been added through with /// 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) /// [`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( pub fn texture(
&self, &self,
id: &egui::TextureId, id: &epaint::TextureId,
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> { ) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
self.textures.get(id) 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 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 /// This effectively enables off-screen rendering inside the egui UI. Texture must have
@ -587,18 +609,12 @@ impl RenderPass {
device: &wgpu::Device, device: &wgpu::Device,
texture: &wgpu::TextureView, texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode, texture_filter: wgpu::FilterMode,
) -> egui::TextureId { ) -> epaint::TextureId {
self.register_native_texture_with_sampler_options( self.register_native_texture_with_sampler_options(
device, device,
texture, texture,
wgpu::SamplerDescriptor { wgpu::SamplerDescriptor {
label: Some( label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
format!(
"egui_user_image_{}_texture_sampler",
self.next_user_texture_id
)
.as_str(),
),
mag_filter: texture_filter, mag_filter: texture_filter,
min_filter: texture_filter, min_filter: texture_filter,
..Default::default() ..Default::default()
@ -606,7 +622,30 @@ impl RenderPass {
) )
} }
/// Registers a `wgpu::Texture` with a `egui::TextureId` while also accepting custom /// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
///
/// This enables applications to reuse `TextureId`s.
pub fn update_egui_texture_from_wgpu_texture(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
id: epaint::TextureId,
) {
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
device,
texture,
wgpu::SamplerDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
mag_filter: texture_filter,
min_filter: texture_filter,
..Default::default()
},
id,
);
}
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options. /// `wgpu::SamplerDescriptor` options.
/// ///
/// This allows applications to specify individual minification/magnification filters as well as /// This allows applications to specify individual minification/magnification filters as well as
@ -621,20 +660,16 @@ impl RenderPass {
device: &wgpu::Device, device: &wgpu::Device,
texture: &wgpu::TextureView, texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>, sampler_descriptor: wgpu::SamplerDescriptor<'_>,
) -> egui::TextureId { ) -> epaint::TextureId {
crate::profile_function!();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None, compare: None,
..sampler_descriptor ..sampler_descriptor
}); });
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some( label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
format!(
"egui_user_image_{}_texture_bind_group",
self.next_user_texture_id
)
.as_str(),
),
layout: &self.texture_bind_group_layout, layout: &self.texture_bind_group_layout,
entries: &[ entries: &[
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
@ -648,124 +683,208 @@ impl RenderPass {
], ],
}); });
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.textures.insert(id, (None, bind_group));
self.next_user_texture_id += 1; self.next_user_texture_id += 1;
id id
} }
/// Uploads the uniform, vertex and index data used by the render pass. /// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
/// Should be called before `execute()`. /// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to reuse `TextureId`s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
id: epaint::TextureId,
) {
crate::profile_function!();
let (_user_texture, user_texture_binding) = self
.textures
.get_mut(&id)
.expect("Tried to update a texture that has not been allocated yet.");
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
*user_texture_binding = bind_group;
}
/// Uploads the uniform, vertex and index data used by the renderer.
/// Should be called before `render()`.
///
/// Returns all user-defined command buffers gathered from prepare callbacks.
pub fn update_buffers( pub fn update_buffers(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
paint_jobs: &[egui::epaint::ClippedPrimitive], encoder: &mut wgpu::CommandEncoder,
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor, screen_descriptor: &ScreenDescriptor,
) { ) -> Vec<wgpu::CommandBuffer> {
crate::profile_function!();
let screen_size_in_points = screen_descriptor.screen_size_in_points(); let screen_size_in_points = screen_descriptor.screen_size_in_points();
self.update_buffer( {
device, crate::profile_scope!("uniforms");
queue, // Update uniform buffer
&BufferType::Uniform, queue.write_buffer(
&self.uniform_buffer,
0, 0,
bytemuck::cast_slice(&[UniformBuffer { bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points, screen_size_in_points,
_padding: Default::default(), _padding: Default::default(),
}]), }]),
); );
}
let mut mesh_idx = 0; // Determine how many vertices & indices need to be rendered.
for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() { let (vertex_count, index_count) = {
crate::profile_scope!("count_vertices_indices");
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
match &clipped_primitive.primitive {
Primitive::Mesh(mesh) => {
(acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
}
Primitive::Callback(_) => acc,
}
})
};
{
// Resize index buffer if needed:
self.index_buffer.slices.clear();
let required_size = (std::mem::size_of::<u32>() * index_count) as u64;
if self.index_buffer.capacity < required_size {
self.index_buffer.capacity =
(self.index_buffer.capacity * 2).at_least(required_size);
self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
}
}
{
// Resize vertex buffer if needed:
self.vertex_buffer.slices.clear();
let required_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
if self.vertex_buffer.capacity < required_size {
self.vertex_buffer.capacity =
(self.vertex_buffer.capacity * 2).at_least(required_size);
self.vertex_buffer.buffer =
create_vertex_buffer(device, self.vertex_buffer.capacity);
}
}
// Upload index & vertex data and call user callbacks
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
crate::profile_scope!("primitives");
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
match primitive { match primitive {
Primitive::Mesh(mesh) => { Primitive::Mesh(mesh) => {
let data: &[u8] = bytemuck::cast_slice(&mesh.indices); {
if mesh_idx < self.index_buffers.len() { let index_offset = self.index_buffer.slices.last().unwrap_or(&(0..0)).end;
self.update_buffer(device, queue, &BufferType::Index, mesh_idx, data); let data = bytemuck::cast_slice(&mesh.indices);
} else { queue.write_buffer(&self.index_buffer.buffer, index_offset, data);
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { self.index_buffer
label: Some("egui_index_buffer"), .slices
contents: data, .push(index_offset..(data.len() as wgpu::BufferAddress + index_offset));
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
self.index_buffers.push(SizedBuffer {
buffer,
size: data.len(),
});
} }
{
let data: &[u8] = bytemuck::cast_slice(&mesh.vertices); let vertex_offset = self.vertex_buffer.slices.last().unwrap_or(&(0..0)).end;
if mesh_idx < self.vertex_buffers.len() { let data = bytemuck::cast_slice(&mesh.vertices);
self.update_buffer(device, queue, &BufferType::Vertex, mesh_idx, data); queue.write_buffer(&self.vertex_buffer.buffer, vertex_offset, data);
} else { self.vertex_buffer.slices.push(
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { vertex_offset..(data.len() as wgpu::BufferAddress + vertex_offset),
label: Some("egui_vertex_buffer"), );
contents: data,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
self.vertex_buffers.push(SizedBuffer {
buffer,
size: data.len(),
});
} }
mesh_idx += 1;
} }
Primitive::Callback(callback) => { Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() { let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c c
} else { } else {
tracing::warn!("Unknown paint callback: expected `egui_gpu::CallbackFn`"); tracing::warn!("Unknown paint callback: expected `egui_wgpu::CallbackFn`");
continue; continue;
}; };
(cbfn.prepare)(device, queue, &mut self.paint_callback_resources); crate::profile_scope!("callback");
} user_cmd_bufs.extend((cbfn.prepare)(
device,
queue,
encoder,
&mut self.paint_callback_resources,
));
} }
} }
} }
/// Updates the buffers used by egui. Will properly re-size the buffers if needed. user_cmd_bufs
fn update_buffer( }
&mut self, }
fn create_sampler(
options: epaint::textures::TextureOptions,
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, ) -> wgpu::Sampler {
buffer_type: &BufferType, let mag_filter = match options.magnification {
index: usize, epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
data: &[u8], epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
) {
let (buffer, storage, label) = match buffer_type {
BufferType::Index => (
&mut self.index_buffers[index],
wgpu::BufferUsages::INDEX,
"egui_index_buffer",
),
BufferType::Vertex => (
&mut self.vertex_buffers[index],
wgpu::BufferUsages::VERTEX,
"egui_vertex_buffer",
),
BufferType::Uniform => (
&mut self.uniform_buffer,
wgpu::BufferUsages::UNIFORM,
"egui_uniform_buffer",
),
}; };
let min_filter = match options.minification {
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!(
"egui sampler (mag: {:?}, min {:?})",
mag_filter, min_filter
)),
mag_filter,
min_filter,
..Default::default()
})
}
if data.len() > buffer.size { fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
buffer.size = data.len(); crate::profile_function!();
buffer.buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label), label: Some("egui_vertex_buffer"),
contents: bytemuck::cast_slice(data), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
usage: storage | wgpu::BufferUsages::COPY_DST, size,
}); mapped_at_creation: false,
} else { })
queue.write_buffer(&buffer.buffer, 0, data);
}
} }
fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
crate::profile_function!();
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_index_buffer"),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
size,
mapped_at_creation: false,
})
} }
/// A Rect in physical pixel space, used for setting cliipping rectangles. /// A Rect in physical pixel space, used for setting cliipping rectangles.
@ -777,7 +896,7 @@ struct ScissorRect {
} }
impl 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: // Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_min_y = pixels_per_point * clip_rect.min.y;
@ -796,20 +915,17 @@ impl ScissorRect {
let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]); let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]); let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
let width = (clip_max_x - clip_min_x).at_least(1); Self {
let height = (clip_max_y - clip_min_y).at_least(1);
ScissorRect {
x: clip_min_x, x: clip_min_x,
y: clip_min_y, y: clip_min_y,
width, width: clip_max_x - clip_min_x,
height, height: clip_max_y - clip_min_y,
} }
} }
} }
#[test] #[test]
fn render_pass_impl_send_sync() { fn renderer_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {} fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RenderPass>(); assert_send_sync::<Renderer>();
} }

View file

@ -0,0 +1,418 @@
use std::sync::Arc;
use epaint::mutex::RwLock;
use tracing::error;
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
struct SurfaceState {
surface: wgpu::Surface,
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
///
/// Alternatively you can use [`crate::renderer`] directly.
pub struct Painter {
configuration: WgpuConfiguration,
msaa_samples: u32,
support_transparent_backbuffer: bool,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
instance: wgpu::Instance,
adapter: Option<wgpu::Adapter>,
render_state: Option<RenderState>,
surface_state: Option<SurfaceState>,
}
impl Painter {
/// Manages [`wgpu`] state, including surface state, required to render egui.
///
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
/// of render + surface state is deferred until the painter is given its first window target
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
/// native window is chosen)
///
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
/// [`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,
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,
instance,
adapter: None,
render_state: None,
surface_state: None,
}
}
/// Get the [`RenderState`].
///
/// Will return [`None`] if the render state has not been initialized yet.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
async fn init_render_state(
&self,
adapter: &wgpu::Adapter,
target_format: wgpu::TextureFormat,
) -> 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
// so we can take its format into account.
//
// 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.
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,
})
.await;
}
if self.render_state.is_none() {
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(
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: 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`]
///
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
/// state if needed) that is used for egui rendering.
///
/// This must be called before trying to render via
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
///
/// # Portability
///
/// _In particular it's important to note that on Android a it's only possible to create
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
/// attempts to query the raw window handle while paused._
///
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
/// valid [`winit::window::Window`].
///
/// # Safety
///
/// 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`).
///
/// # 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)?;
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();
self.surface_state = Some(SurfaceState {
surface,
width: size.width,
height: size.height,
alpha_mode,
});
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
///
/// This API will only return a known dimension after `set_window()` has been called
/// at least once, since the underlying device and render state are initialized lazily
/// once we have a window (that may determine the choice of adapter/device).
pub fn max_texture_side(&self) -> Option<usize> {
self.render_state
.as_ref()
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
}
fn resize_and_generate_depth_texture_view(
&mut self,
width_in_pixels: u32,
height_in_pixels: u32,
) {
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| {
render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
width: width_in_pixels,
height: height_in_pixels,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
});
}
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
if self.surface_state.is_some() {
self.resize_and_generate_depth_texture_view(width_in_pixels, height_in_pixels);
} else {
error!("Ignoring window resize notification with no surface created via Painter::set_window()");
}
}
pub fn paint_and_update_textures(
&mut self,
pixels_per_point: f32,
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
) {
crate::profile_function!();
let render_state = match self.render_state.as_mut() {
Some(rs) => rs,
None => return,
};
let surface_state = match self.surface_state.as_ref() {
Some(rs) => rs,
None => return,
};
let output_frame = {
crate::profile_scope!("get_current_texture");
// This is what vsync-waiting happens, at least on Mac.
surface_state.surface.get_current_texture()
};
let output_frame = match output_frame {
Ok(frame) => frame,
#[allow(clippy::single_match_else)]
Err(e) => match (*self.configuration.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(
surface_state,
render_state,
self.configuration.present_mode,
);
return;
}
SurfaceErrorAction::SkipFrame => {
return;
}
},
};
let mut encoder =
render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
// Upload all resources for the GPU.
let screen_descriptor = renderer::ScreenDescriptor {
size_in_pixels: [surface_state.width, surface_state.height],
pixels_per_point,
};
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
renderer.update_buffers(
&render_state.device,
&render_state.queue,
&mut encoder,
clipped_primitives,
&screen_descriptor,
)
};
{
let renderer = render_state.renderer.read();
let frame_view = output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
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,
},
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: true,
}),
stencil_ops: None,
}
}),
label: Some("egui_render"),
});
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
}
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
let encoded = {
crate::profile_scope!("CommandEncoder::finish");
encoder.finish()
};
// Submit the commands: both the main buffer and user-defined ones.
{
crate::profile_scope!("Queue::submit");
render_state
.queue
.submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded)));
};
// Redraw egui
{
crate::profile_scope!("present");
output_frame.present();
}
}
#[allow(clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}
}

View file

@ -3,6 +3,30 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased ## 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)).
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)). * Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
* Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)). * Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).

View file

@ -1,14 +1,14 @@
[package] [package]
name = "egui-winit" name = "egui-winit"
version = "0.18.0" version = "0.21.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit" description = "Bindings for using egui with winit"
edition = "2021" edition = "2021"
rust-version = "1.61" rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/egui-winit" homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/egui-winit" repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
categories = ["gui", "game-development"] categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"] keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
@ -18,7 +18,10 @@ all-features = true
[features] [features]
default = ["clipboard", "links"] default = ["clipboard", "links", "wayland", "winit/default"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`. ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"] bytemuck = ["egui/bytemuck"]
@ -33,36 +36,41 @@ links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"] puffin = ["dep:puffin"]
## Experimental support for a screen reader.
screen_reader = ["tts"]
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde). ## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde"] serde = ["egui/serde", "dep:serde"]
## Enables Wayland support.
wayland = ["winit/wayland"]
[dependencies] [dependencies]
egui = { version = "0.18.0", path = "../egui", default-features = false, features = [ egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"tracing", "tracing",
] } ] }
instant = { version = "0.1", features = ["wasm-bindgen"] } # We use instant so we can (maybe) compile for web 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"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
winit = "0.27.2" winit = { version = "0.28", default-features = false }
#! ### Optional dependencies #! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.10.0", optional = true }
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
puffin = { version = "0.13", optional = true } puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] } serde = { version = "1.0", optional = true, features = ["derive"] }
# feature screen_reader webbrowser = { version = "0.8.3", optional = true }
tts = { version = "0.20", optional = true } # Can't use 0.22 due to compilation problems on linux: https://github.com/emilk/egui/runs/7170127089?check_suite_focus=true#step:5:713
webbrowser = { version = "0.7", optional = true }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies] [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 } smithay-clipboard = { version = "0.6.3", optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "2.1", optional = true, default-features = false } 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 { Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))] #[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(), arboard: init_arboard(),
#[cfg(all( #[cfg(all(
any( any(
target_os = "linux", target_os = "linux",
@ -41,6 +42,7 @@ impl Clipboard {
feature = "smithay-clipboard" feature = "smithay-clipboard"
))] ))]
smithay: init_smithay_clipboard(wayland_display), smithay: init_smithay_clipboard(wayland_display),
clipboard: Default::default(), clipboard: Default::default(),
} }
} }
@ -60,7 +62,7 @@ impl Clipboard {
return match clipboard.load() { return match clipboard.load() {
Ok(text) => Some(text), Ok(text) => Some(text),
Err(err) => { Err(err) => {
tracing::error!("Paste error: {}", err); tracing::error!("smithay paste error: {err}");
None None
} }
}; };
@ -71,7 +73,7 @@ impl Clipboard {
return match clipboard.get_text() { return match clipboard.get_text() {
Ok(text) => Some(text), Ok(text) => Some(text),
Err(err) => { Err(err) => {
tracing::error!("Paste error: {}", err); tracing::error!("arboard paste error: {err}");
None None
} }
}; };
@ -99,7 +101,7 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))] #[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard { if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) { if let Err(err) = clipboard.set_text(text) {
tracing::error!("Copy/Cut error: {}", err); tracing::error!("arboard copy/cut error: {err}");
} }
return; return;
} }
@ -110,10 +112,11 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))] #[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> { fn init_arboard() -> Option<arboard::Clipboard> {
tracing::debug!("Initializing arboard clipboard…");
match arboard::Clipboard::new() { match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard), Ok(clipboard) => Some(clipboard),
Err(err) => { Err(err) => {
tracing::error!("Failed to initialize clipboard: {}", err); tracing::warn!("Failed to initialize arboard clipboard: {err}");
None None
} }
} }
@ -133,10 +136,11 @@ fn init_smithay_clipboard(
wayland_display: Option<*mut c_void>, wayland_display: Option<*mut c_void>,
) -> Option<smithay_clipboard::Clipboard> { ) -> Option<smithay_clipboard::Clipboard> {
if let Some(display) = wayland_display { if let Some(display) = wayland_display {
tracing::debug!("Initializing smithay clipboard…");
#[allow(unsafe_code)] #[allow(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display) }) Some(unsafe { smithay_clipboard::Clipboard::new(display) })
} else { } else {
tracing::error!("Cannot initialize smithay clipboard without a display handle!"); tracing::debug!("Cannot initialize smithay clipboard without a display handle");
None None
} }
} }

View file

@ -11,24 +11,19 @@
use std::os::raw::c_void; use std::os::raw::c_void;
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui; pub use egui;
#[cfg(feature = "accesskit")]
use egui::accesskit;
pub use winit; pub use winit;
pub mod clipboard; pub mod clipboard;
pub mod screen_reader;
mod window_settings; mod window_settings;
pub use window_settings::WindowSettings; pub use window_settings::WindowSettings;
use winit::event_loop::EventLoopWindowTarget; use winit::event_loop::EventLoopWindowTarget;
#[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 { pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
window.scale_factor() as f32 window.scale_factor() as f32
@ -39,18 +34,37 @@ pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 {
egui::vec2(size.width as f32, size.height as f32) egui::vec2(size.width as f32, size.height as f32)
} }
// ----------------------------------------------------------------------------
#[must_use]
pub struct EventResponse {
/// If true, egui consumed this event, i.e. wants exclusive use of this event
/// (e.g. a mouse click on an egui window, or entering text into a text field).
///
/// For instance, if you use egui for a game, you should only
/// pass on the events to your game when [`Self::consumed`] is `false.
///
/// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs.
pub consumed: bool,
/// Do we need an egui refresh because of this event?
pub repaint: bool,
}
// ----------------------------------------------------------------------------
/// Handles the integration between egui and winit. /// Handles the integration between egui and winit.
pub struct State { pub struct State {
start_time: instant::Instant, start_time: instant::Instant,
egui_input: egui::RawInput, egui_input: egui::RawInput,
pointer_pos_in_points: Option<egui::Pos2>, pointer_pos_in_points: Option<egui::Pos2>,
any_pointer_button_down: bool, any_pointer_button_down: bool,
current_cursor_icon: egui::CursorIcon, current_cursor_icon: Option<egui::CursorIcon>,
/// What egui uses. /// What egui uses.
current_pixels_per_point: f32, current_pixels_per_point: f32,
clipboard: clipboard::Clipboard, clipboard: clipboard::Clipboard,
screen_reader: screen_reader::ScreenReader,
/// If `true`, mouse inputs will be treated as touches. /// If `true`, mouse inputs will be treated as touches.
/// Useful for debugging touch support in egui. /// Useful for debugging touch support in egui.
@ -62,6 +76,12 @@ pub struct State {
/// ///
/// Only one touch will be interpreted as pointer at any time. /// Only one touch will be interpreted as pointer at any time.
pointer_touch_id: Option<u64>, pointer_touch_id: Option<u64>,
/// track ime state
input_method_editor_started: bool,
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
} }
impl State { impl State {
@ -80,17 +100,35 @@ impl State {
egui_input, egui_input,
pointer_pos_in_points: None, pointer_pos_in_points: None,
any_pointer_button_down: false, any_pointer_button_down: false,
current_cursor_icon: egui::CursorIcon::Default, current_cursor_icon: None,
current_pixels_per_point: 1.0, current_pixels_per_point: 1.0,
clipboard: clipboard::Clipboard::new(wayland_display), clipboard: clipboard::Clipboard::new(wayland_display),
screen_reader: screen_reader::ScreenReader::default(),
simulate_touch_screen: false, simulate_touch_screen: false,
pointer_touch_id: None, pointer_touch_id: None,
input_method_editor_started: false,
#[cfg(feature = "accesskit")]
accesskit: None,
} }
} }
#[cfg(feature = "accesskit")]
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
) {
self.accesskit = Some(accesskit_winit::Adapter::new(
window,
initial_tree_update_factory,
event_loop_proxy,
));
}
/// Call this once a graphics context has been created to update the maximum texture dimensions /// Call this once a graphics context has been created to update the maximum texture dimensions
/// that egui will use. /// that egui will use.
pub fn set_max_texture_side(&mut self, max_texture_side: usize) { pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
@ -152,51 +190,63 @@ impl State {
/// Call this when there is a new event. /// Call this when there is a new event.
/// ///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
///
/// Returns `true` if egui wants exclusive use of this event
/// (e.g. a mouse click on an egui window, or entering text into a text field).
/// For instance, if you use egui for a game, you want to first call this
/// and only when this returns `false` pass on the events to your game.
///
/// Note that egui uses `tab` to move focus between elements, so this will always return `true` for tabs.
pub fn on_event( pub fn on_event(
&mut self, &mut self,
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
event: &winit::event::WindowEvent<'_>, event: &winit::event::WindowEvent<'_>,
) -> bool { ) -> EventResponse {
use winit::event::WindowEvent; use winit::event::WindowEvent;
match event { match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => { WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let pixels_per_point = *scale_factor as f32; let pixels_per_point = *scale_factor as f32;
self.egui_input.pixels_per_point = Some(pixels_per_point); self.egui_input.pixels_per_point = Some(pixels_per_point);
self.current_pixels_per_point = pixels_per_point; self.current_pixels_per_point = pixels_per_point;
false EventResponse {
repaint: true,
consumed: false,
}
} }
WindowEvent::MouseInput { state, button, .. } => { WindowEvent::MouseInput { state, button, .. } => {
self.on_mouse_button_input(*state, *button); self.on_mouse_button_input(*state, *button);
egui_ctx.wants_pointer_input() EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
} }
WindowEvent::MouseWheel { delta, .. } => { WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(*delta); self.on_mouse_wheel(*delta);
egui_ctx.wants_pointer_input() EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
} }
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
self.on_cursor_moved(*position); self.on_cursor_moved(*position);
egui_ctx.is_using_pointer() EventResponse {
repaint: true,
consumed: egui_ctx.is_using_pointer(),
}
} }
WindowEvent::CursorLeft { .. } => { WindowEvent::CursorLeft { .. } => {
self.pointer_pos_in_points = None; self.pointer_pos_in_points = None;
self.egui_input.events.push(egui::Event::PointerGone); self.egui_input.events.push(egui::Event::PointerGone);
false EventResponse {
repaint: true,
consumed: false,
}
} }
// WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO
WindowEvent::Touch(touch) => { WindowEvent::Touch(touch) => {
self.on_touch(touch); self.on_touch(touch);
match touch.phase { let consumed = match touch.phase {
winit::event::TouchPhase::Started winit::event::TouchPhase::Started
| winit::event::TouchPhase::Ended | winit::event::TouchPhase::Ended
| winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(), | winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(),
winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(), winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(),
};
EventResponse {
repaint: true,
consumed,
} }
} }
WindowEvent::ReceivedCharacter(ch) => { WindowEvent::ReceivedCharacter(ch) => {
@ -205,37 +255,92 @@ impl State {
let is_mac_cmd = cfg!(target_os = "macos") let is_mac_cmd = cfg!(target_os = "macos")
&& (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd); && (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd);
if is_printable_char(*ch) && !is_mac_cmd { let consumed = if is_printable_char(*ch) && !is_mac_cmd {
self.egui_input self.egui_input
.events .events
.push(egui::Event::Text(ch.to_string())); .push(egui::Event::Text(ch.to_string()));
egui_ctx.wants_keyboard_input() egui_ctx.wants_keyboard_input()
} else { } else {
false false
};
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::Ime(ime) => {
// on Mac even Cmd-C is preessed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
// How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS
// and Windows.
//
// - On Windows, before and after each Commit will produce an Enable/Disabled
// event.
// - On MacOS, only when user explicit enable/disable ime. No Disabled
// after Commit.
//
// We use input_method_editor_started to mannualy insert CompositionStart
// between Commits.
match ime {
winit::event::Ime::Enabled | winit::event::Ime::Disabled => (),
winit::event::Ime::Commit(text) => {
self.input_method_editor_started = false;
self.egui_input
.events
.push(egui::Event::CompositionEnd(text.clone()));
}
winit::event::Ime::Preedit(text, ..) => {
if !self.input_method_editor_started {
self.input_method_editor_started = true;
self.egui_input.events.push(egui::Event::CompositionStart);
}
self.egui_input
.events
.push(egui::Event::CompositionUpdate(text.clone()));
}
};
EventResponse {
repaint: true,
consumed: egui_ctx.wants_keyboard_input(),
} }
} }
WindowEvent::KeyboardInput { input, .. } => { WindowEvent::KeyboardInput { input, .. } => {
self.on_keyboard_input(input); self.on_keyboard_input(input);
egui_ctx.wants_keyboard_input() let consumed = egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab) || input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
EventResponse {
repaint: true,
consumed,
}
} }
WindowEvent::Focused(has_focus) => { WindowEvent::Focused(has_focus) => {
self.egui_input.has_focus = *has_focus; self.egui_input.has_focus = *has_focus;
// We will not be given a KeyboardInput event when the modifiers are released while // We will not be given a KeyboardInput event when the modifiers are released while
// the window does not have focus. Unset all modifier state to be safe. // the window does not have focus. Unset all modifier state to be safe.
self.egui_input.modifiers = egui::Modifiers::default(); self.egui_input.modifiers = egui::Modifiers::default();
false EventResponse {
repaint: true,
consumed: false,
}
} }
WindowEvent::HoveredFile(path) => { WindowEvent::HoveredFile(path) => {
self.egui_input.hovered_files.push(egui::HoveredFile { self.egui_input.hovered_files.push(egui::HoveredFile {
path: Some(path.clone()), path: Some(path.clone()),
..Default::default() ..Default::default()
}); });
false EventResponse {
repaint: true,
consumed: false,
}
} }
WindowEvent::HoveredFileCancelled => { WindowEvent::HoveredFileCancelled => {
self.egui_input.hovered_files.clear(); self.egui_input.hovered_files.clear();
false EventResponse {
repaint: true,
consumed: false,
}
} }
WindowEvent::DroppedFile(path) => { WindowEvent::DroppedFile(path) => {
self.egui_input.hovered_files.clear(); self.egui_input.hovered_files.clear();
@ -243,7 +348,10 @@ impl State {
path: Some(path.clone()), path: Some(path.clone()),
..Default::default() ..Default::default()
}); });
false EventResponse {
repaint: true,
consumed: false,
}
} }
WindowEvent::ModifiersChanged(state) => { WindowEvent::ModifiersChanged(state) => {
self.egui_input.modifiers.alt = state.alt(); self.egui_input.modifiers.alt = state.alt();
@ -255,13 +363,54 @@ impl State {
} else { } else {
state.ctrl() state.ctrl()
}; };
false EventResponse {
} repaint: true,
_ => { consumed: false,
// dbg!(event);
false
} }
} }
// Things that may require repaint:
WindowEvent::CloseRequested
| WindowEvent::CursorEntered { .. }
| WindowEvent::Destroyed
| WindowEvent::Occluded(_)
| WindowEvent::Resized(_)
| WindowEvent::ThemeChanged(_)
| WindowEvent::TouchpadPressure { .. } => EventResponse {
repaint: true,
consumed: false,
},
// 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(),
}
}
}
}
/// Call this when there is a new [`accesskit::ActionRequest`].
///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_input
.events
.push(egui::Event::AccessKitActionRequest(request));
} }
fn on_mouse_button_input( fn on_mouse_button_input(
@ -450,6 +599,7 @@ impl State {
self.egui_input.events.push(egui::Event::Key { self.egui_input.events.push(egui::Event::Key {
key, key,
pressed, pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers, modifiers: self.egui_input.modifiers,
}); });
} }
@ -470,11 +620,6 @@ impl State {
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput, platform_output: egui::PlatformOutput,
) { ) {
if egui_ctx.options().screen_reader {
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::PlatformOutput { let egui::PlatformOutput {
cursor_icon, cursor_icon,
open_url, open_url,
@ -482,6 +627,8 @@ impl State {
events: _, // handled above events: _, // handled above
mutable_text_under_cursor: _, // only used in eframe web mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos, text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = platform_output; } = platform_output;
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
@ -498,26 +645,36 @@ impl State {
if let Some(egui::Pos2 { x, y }) = text_cursor_pos { if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y }); window.set_ime_position(winit::dpi::LogicalPosition { x, y });
} }
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_ref() {
if let Some(update) = accesskit_update {
accesskit.update_if_active(|| update);
}
}
} }
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) { 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 if self.current_cursor_icon == Some(cursor_icon) {
#[cfg(windows)] // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
if self.current_cursor_icon == cursor_icon { // On other platforms: just early-out to save CPU.
return; 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(); let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window { 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 { } else {
window.set_cursor_visible(false); window.set_cursor_visible(false);
} }
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
}
} }
} }
@ -600,6 +757,11 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
VirtualKeyCode::PageUp => Key::PageUp, VirtualKeyCode::PageUp => Key::PageUp,
VirtualKeyCode::PageDown => Key::PageDown, VirtualKeyCode::PageDown => Key::PageDown,
VirtualKeyCode::Minus => Key::Minus,
// Using Mac the key with the Plus sign on it is reported as the Equals key
// (with both English and Swedish keyboard).
VirtualKeyCode::Equals => Key::PlusEquals,
VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0, VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0,
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1, VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1,
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2, VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2,
@ -711,6 +873,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
/// Returns a Wayland display handle if the target is running Wayland /// Returns a Wayland display handle if the target is running Wayland
fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> { fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> {
#[cfg(feature = "wayland")]
#[cfg(any( #[cfg(any(
target_os = "linux", target_os = "linux",
target_os = "dragonfly", target_os = "dragonfly",
@ -719,6 +882,7 @@ fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_v
target_os = "openbsd" target_os = "openbsd"
))] ))]
{ {
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
return _event_loop.wayland_display(); return _event_loop.wayland_display();
} }

View file

@ -0,0 +1,144 @@
/// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WindowSettings {
/// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform.
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>,
fullscreen: bool,
/// Inner size of window in logical pixels
inner_size_points: Option<egui::Vec2>,
}
impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows.
window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else {
// Other platforms use the outer position.
window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};
Self {
position,
fullscreen: window.fullscreen().is_some(),
inner_size_points: Some(egui::vec2(
inner_size_points.width,
inner_size_points.height,
)),
}
}
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 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 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
.with_inner_size(winit::dpi::LogicalSize {
width: inner_size_points.x as f64,
height: inner_size_points.y as f64,
})
.with_fullscreen(
self.fullscreen
.then_some(winit::window::Fullscreen::Borderless(None)),
)
} else {
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,13 +1,13 @@
[package] [package]
name = "egui" name = "egui"
version = "0.18.1" version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native" description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021" edition = "2021"
rust-version = "1.61" rust-version = "1.65"
homepage = "https://github.com/emilk/egui" homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "../README.md" readme = "../../README.md"
repository = "https://github.com/emilk/egui" repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"] categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
@ -52,15 +52,26 @@ mint = ["epaint/mint"]
persistence = ["serde", "epaint/serde", "ron"] persistence = ["serde", "epaint/serde", "ron"]
## Allow serialization using [`serde`](https://docs.rs/serde). ## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde"] serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
[dependencies] [dependencies]
epaint = { version = "0.18.1", path = "../epaint", default-features = false } epaint = { version = "0.21.0", path = "../epaint", default-features = false }
ahash = "0.8" 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
"std",
] }
nohash-hasher = "0.2" nohash-hasher = "0.2"
#! ### Optional dependencies #! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.9.0", optional = true }
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
@ -68,4 +79,6 @@ ron = { version = "0.8", optional = true }
serde = { version = "1", optional = true, features = ["derive", "rc"] } serde = { version = "1", optional = true, features = ["derive", "rc"] }
# egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing). # egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing).
tracing = { version = "0.1", optional = true, default-features = false, features = ["std"] } tracing = { version = "0.1", optional = true, default-features = false, features = [
"std",
] }

View file

@ -2,6 +2,6 @@ There are no stand-alone egui examples, because egui is not stand-alone!
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead. See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/egui_demo_lib>. There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!

View file

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

View file

@ -2,17 +2,17 @@
//! It has no frame or own size. It is potentially movable. //! It has no frame or own size. It is potentially movable.
//! It is the foundation for windows and popups. //! It is the foundation for windows and popups.
use std::{fmt::Debug, hash::Hash};
use crate::*; use crate::*;
/// State that is persisted between frames. /// 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)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State { pub(crate) struct State {
/// Last known pos /// Last known pos of the pivot
pub pos: Pos2, pub pivot_pos: Pos2,
pub pivot: Align2,
/// Last know size. Used for catching clicks. /// Last know size. Used for catching clicks.
pub size: Vec2, pub size: Vec2,
@ -23,8 +23,22 @@ pub(crate) struct State {
} }
impl 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 { pub fn rect(&self) -> Rect {
Rect::from_min_size(self.pos, self.size) Rect::from_min_size(self.left_top_pos(), self.size)
} }
} }
@ -48,23 +62,27 @@ pub struct Area {
movable: bool, movable: bool,
interactable: bool, interactable: bool,
enabled: bool, enabled: bool,
constrain: bool,
order: Order, order: Order,
default_pos: Option<Pos2>, default_pos: Option<Pos2>,
pivot: Align2,
anchor: Option<(Align2, Vec2)>, anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>, new_pos: Option<Pos2>,
drag_bounds: Option<Rect>, drag_bounds: Option<Rect>,
} }
impl Area { impl Area {
pub fn new(id_source: impl Hash) -> Self { pub fn new(id: impl Into<Id>) -> Self {
Self { Self {
id: Id::new(id_source), id: id.into(),
movable: true, movable: true,
interactable: true, interactable: true,
constrain: false,
enabled: true, enabled: true,
order: Order::Middle, order: Order::Middle,
default_pos: None, default_pos: None,
new_pos: None, new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None, anchor: None,
drag_bounds: None, drag_bounds: None,
} }
@ -129,6 +147,24 @@ impl Area {
self self
} }
/// Constrains this area to the screen bounds.
pub fn constrain(mut self, constrain: bool) -> Self {
self.constrain = constrain;
self
}
/// Where the "root" of the area is.
///
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
/// then [`Self::fixed_pos`] will set the position of the right-top
/// corner of the area.
///
/// Default: [`Align2::LEFT_TOP`].
pub fn pivot(mut self, pivot: Align2) -> Self {
self.pivot = pivot;
self
}
/// Positions the window but you can still move it. /// Positions the window but you can still move it.
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self { pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
self.new_pos = Some(current_pos.into()); self.new_pos = Some(current_pos.into());
@ -169,14 +205,15 @@ impl Area {
pub(crate) struct Prepared { pub(crate) struct Prepared {
layer_id: LayerId, layer_id: LayerId,
state: State, state: State,
pub(crate) movable: bool, move_response: Response,
enabled: bool, enabled: bool,
drag_bounds: Option<Rect>, drag_bounds: Option<Rect>,
/// Set the first frame of new windows with anchors.
/// We always make windows invisible the first frame to hide "first-frame-jitters".
/// ///
/// This is so that we use the first frame to calculate the window size, /// This is so that we use the first frame to calculate the window size,
/// and then can correctly position the window the next frame, /// and then can correctly position the window and its contents the next frame,
/// without having one frame where the window is positioned in the wrong place. /// without having one frame where the window is wrongly positioned or sized.
temporarily_invisible: bool, temporarily_invisible: bool,
} }
@ -202,44 +239,95 @@ impl Area {
enabled, enabled,
default_pos, default_pos,
new_pos, new_pos,
pivot,
anchor, anchor,
drag_bounds, drag_bounds,
constrain,
} = self; } = self;
let layer_id = LayerId::new(order, id); let layer_id = LayerId::new(order, id);
let state = ctx.memory().areas.get(id).cloned(); let state = ctx.memory(|mem| mem.areas.get(id).copied());
let is_new = state.is_none(); let is_new = state.is_none();
if is_new { if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place 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 { 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, size: Vec2::ZERO,
interactable, interactable,
}); });
state.pos = new_pos.unwrap_or(state.pos); state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.interactable = interactable; state.interactable = interactable;
let mut temporarily_invisible = false;
if let Some((anchor, offset)) = anchor { if let Some((anchor, offset)) = anchor {
if is_new {
temporarily_invisible = true;
} else {
let screen = ctx.available_rect(); 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,
);
} }
state.pos = ctx.round_pos_to_pixels(state.pos); // interact right away to prevent frame-delay
let move_response = {
let interact_id = layer_id.id.with("move");
let sense = if movable {
Sense::click_and_drag()
} else if interactable {
Sense::click() // allow clicks to bring to front
} else {
Sense::hover()
};
let move_response = ctx.interact(
Rect::EVERYTHING,
ctx.style().spacing.item_spacing,
layer_id,
interact_id,
state.rect(),
sense,
enabled,
);
// Important check - don't try to move e.g. a combobox popup!
if movable {
if move_response.dragged() {
state.pivot_pos += ctx.input(|i| i.pointer.delta());
}
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(|m| m.areas.visible_last_frame(&layer_id))
{
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
ctx.request_repaint();
}
move_response
};
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
if constrain {
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.left_top(),
);
}
Prepared { Prepared {
layer_id, layer_id,
state, state,
movable, move_response,
enabled, enabled,
drag_bounds, drag_bounds,
temporarily_invisible, temporarily_invisible: is_new,
} }
} }
@ -257,7 +345,7 @@ impl Area {
} }
let layer_id = LayerId::new(self.order, self.id); 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 { if let Some(area_rect) = area_rect {
let clip_rect = ctx.available_rect(); let clip_rect = ctx.available_rect();
let painter = Painter::new(ctx.clone(), layer_id, clip_rect); let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
@ -286,7 +374,7 @@ impl Prepared {
} }
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { 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 { let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds bounds.intersect(screen_rect) // protect against infinite bounds
@ -302,14 +390,16 @@ impl Prepared {
}; };
let max_rect = Rect::from_min_max( let max_rect = Rect::from_min_max(
self.state.pos, self.state.left_top_pos(),
bounds.max.at_least(self.state.pos + Vec2::splat(32.0)), bounds
.max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
); );
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky 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_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) .expand(clip_rect_margin)
.intersect(bounds); .intersect(bounds);
@ -330,50 +420,15 @@ impl Prepared {
let Prepared { let Prepared {
layer_id, layer_id,
mut state, mut state,
movable, move_response,
enabled, enabled: _,
drag_bounds, drag_bounds: _,
temporarily_invisible: _, temporarily_invisible: _,
} = self; } = self;
state.size = content_ui.min_rect().size(); state.size = content_ui.min_rect().size();
let interact_id = layer_id.id.with("move"); ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
let sense = if movable {
Sense::click_and_drag()
} else {
Sense::click() // allow clicks to bring to front
};
let move_response = ctx.interact(
Rect::EVERYTHING,
ctx.style().spacing.item_spacing,
layer_id,
interact_id,
state.rect(),
sense,
enabled,
);
if move_response.dragged() && movable {
state.pos += ctx.input().pointer.delta();
}
// Important check - don't try to move e.g. a combobox popup!
if movable {
state.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().areas.move_to_top(layer_id);
ctx.request_repaint();
}
ctx.memory().areas.set_state(layer_id, state);
move_response move_response
} }
@ -381,7 +436,7 @@ impl Prepared {
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(pointer_pos) = ctx.pointer_interact_pos() { 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) any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else { } else {
false false
@ -389,13 +444,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
} }
fn automatic_area_position(ctx: &Context) -> Pos2 { fn automatic_area_position(ctx: &Context) -> Pos2 {
let mut existing: Vec<Rect> = ctx let mut existing: Vec<Rect> = ctx.memory(|mem| {
.memory() mem.areas
.areas
.visible_windows() .visible_windows()
.into_iter() .into_iter()
.map(State::rect) .map(State::rect)
.collect(); .collect()
});
existing.sort_by_key(|r| r.left().round() as i32); existing.sort_by_key(|r| r.left().round() as i32);
let available_rect = ctx.available_rect(); let available_rect = ctx.available_rect();

View file

@ -26,13 +26,14 @@ pub struct CollapsingState {
impl CollapsingState { impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> { pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data() ctx.data_mut(|d| {
.get_persisted::<InnerState>(id) d.get_persisted::<InnerState>(id)
.map(|state| Self { id, state }) .map(|state| Self { id, state })
})
} }
pub fn store(&self, ctx: &Context) { 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 { pub fn id(&self) -> Id {
@ -64,7 +65,7 @@ impl CollapsingState {
/// 0 for closed, 1 for open, with tweening /// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 { pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory().everything_is_visible() { if ctx.memory(|mem| mem.everything_is_visible()) {
1.0 1.0
} else { } else {
ctx.animate_bool(self.id, self.state.open) ctx.animate_bool(self.id, self.state.open)
@ -111,10 +112,7 @@ impl CollapsingState {
response.rect.center().y, response.rect.center().y,
)); ));
let openness = self.openness(ui.ctx()); let openness = self.openness(ui.ctx());
let small_icon_response = Response { let small_icon_response = response.clone().with_new_rect(icon_rect);
rect: icon_rect,
..response.clone()
};
icon_fn(ui, openness, &small_icon_response); icon_fn(ui, openness, &small_icon_response);
response response
} }
@ -143,9 +141,10 @@ impl CollapsingState {
add_header: impl FnOnce(&mut Ui) -> HeaderRet, add_header: impl FnOnce(&mut Ui) -> HeaderRet,
) -> HeaderResponse<'_, HeaderRet> { ) -> HeaderResponse<'_, HeaderRet> {
let header_response = ui.horizontal(|ui| { 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 ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
let collapser = self.show_default_button_indented(ui); 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)) (collapser, add_header(ui))
}); });
HeaderResponse { HeaderResponse {
@ -311,7 +310,6 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
/// Paint the arrow icon that indicated if the region is open or not /// Paint the arrow icon that indicated if the region is open or not
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) { pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
let visuals = ui.style().interact(response); let visuals = ui.style().interact(response);
let stroke = visuals.fg_stroke;
let rect = response.rect; let rect = response.rect;
@ -325,7 +323,11 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
*p = rect.center() + rotation * (*p - rect.center()); *p = rect.center() + rotation * (*p - rect.center());
} }
ui.painter().add(Shape::closed_line(points, stroke)); ui.painter().add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
} }
/// A function that paints an icon indicating if the region is open or not /// A function that paints an icon indicating if the region is open or not
@ -552,7 +554,7 @@ impl CollapsingHeader {
ui.painter().add(epaint::RectShape { ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion), rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding, rounding: visuals.rounding,
fill: visuals.bg_fill, fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke, stroke: visuals.bg_stroke,
// stroke: Default::default(), // stroke: Default::default(),
}); });
@ -572,10 +574,7 @@ impl CollapsingHeader {
header_response.rect.left() + ui.spacing().indent / 2.0, header_response.rect.left() + ui.spacing().indent / 2.0,
header_response.rect.center().y, header_response.rect.center().y,
)); ));
let icon_response = Response { let icon_response = header_response.clone().with_new_rect(icon_rect);
rect: icon_rect,
..header_response.clone()
};
if let Some(icon) = icon { if let Some(icon) = icon {
icon(ui, openness, &icon_response); icon(ui, openness, &icon_response);
} else { } else {
@ -599,13 +598,23 @@ impl CollapsingHeader {
ui: &mut Ui, ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R, add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_body)) self.show_dyn(ui, Box::new(add_body), true)
}
#[inline]
pub fn show_unindented<R>(
self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_body), false)
} }
fn show_dyn<'c, R>( fn show_dyn<'c, R>(
self, self,
ui: &mut Ui, ui: &mut Ui,
add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
indented: bool,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
// Make sure body is bellow header, // Make sure body is bellow header,
// and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid). // and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid).
@ -618,7 +627,11 @@ impl CollapsingHeader {
openness, openness,
} = self.begin(ui); // show the header } = self.begin(ui); // show the header
let ret_response = state.show_body_indented(&header_response, ui, add_body); let ret_response = if indented {
state.show_body_indented(&header_response, ui, add_body)
} else {
state.show_body_unindented(ui, add_body)
};
if let Some(ret_response) = ret_response { if let Some(ret_response) = ret_response {
CollapsingResponse { CollapsingResponse {

View file

@ -1,8 +1,16 @@
use crate::{style::WidgetVisuals, *};
use epaint::Shape; use epaint::Shape;
use crate::{style::WidgetVisuals, *};
/// Indicate wether or not a popup will be shown above or below the box.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
/// A function that paints the [`ComboBox`] icon /// A function that paints the [`ComboBox`] icon
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>; pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
/// A drop-down selection menu with a descriptive label. /// A drop-down selection menu with a descriptive label.
/// ///
@ -28,6 +36,7 @@ pub struct ComboBox {
selected_text: WidgetText, selected_text: WidgetText,
width: Option<f32>, width: Option<f32>,
icon: Option<IconPainter>, icon: Option<IconPainter>,
wrap_enabled: bool,
} }
impl ComboBox { impl ComboBox {
@ -39,6 +48,7 @@ impl ComboBox {
selected_text: Default::default(), selected_text: Default::default(),
width: None, width: None,
icon: None, icon: None,
wrap_enabled: false,
} }
} }
@ -51,6 +61,7 @@ impl ComboBox {
selected_text: Default::default(), selected_text: Default::default(),
width: None, width: None,
icon: None, icon: None,
wrap_enabled: false,
} }
} }
@ -62,10 +73,11 @@ impl ComboBox {
selected_text: Default::default(), selected_text: Default::default(),
width: None, width: None,
icon: None, icon: None,
wrap_enabled: false,
} }
} }
/// Set the width of the button and menu /// Set the outer width of the button and menu.
pub fn width(mut self, width: f32) -> Self { pub fn width(mut self, width: f32) -> Self {
self.width = Some(width); self.width = Some(width);
self self
@ -89,6 +101,7 @@ impl ComboBox {
/// rect: egui::Rect, /// rect: egui::Rect,
/// visuals: &egui::style::WidgetVisuals, /// visuals: &egui::style::WidgetVisuals,
/// _is_open: bool, /// _is_open: bool,
/// _above_or_below: egui::AboveOrBelow,
/// ) { /// ) {
/// let rect = egui::Rect::from_center_size( /// let rect = egui::Rect::from_center_size(
/// rect.center(), /// rect.center(),
@ -107,11 +120,20 @@ impl ComboBox {
/// .show_ui(ui, |_ui| {}); /// .show_ui(ui, |_ui| {});
/// # }); /// # });
/// ``` /// ```
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self { pub fn icon(
mut self,
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
) -> Self {
self.icon = Some(Box::new(icon_fn)); self.icon = Some(Box::new(icon_fn));
self self
} }
/// Controls whether text wrap is used for the selected text
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap_enabled = wrap;
self
}
/// Show the combo box, with the given ui code for the menu contents. /// Show the combo box, with the given ui code for the menu contents.
/// ///
/// Returns `InnerResponse { inner: None }` if the combo box is closed. /// Returns `InnerResponse { inner: None }` if the combo box is closed.
@ -134,15 +156,21 @@ impl ComboBox {
selected_text, selected_text,
width, width,
icon, icon,
wrap_enabled,
} = self; } = self;
let button_id = ui.make_persistent_id(id_source); let button_id = ui.make_persistent_id(id_source);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if let Some(width) = width { let mut ir = combo_box_dyn(
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later. ui,
} button_id,
let mut ir = combo_box_dyn(ui, button_id, selected_text, menu_contents, icon); selected_text,
menu_contents,
icon,
wrap_enabled,
width,
);
if let Some(label) = label { if let Some(label) = label {
ir.response ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text())); .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
@ -209,18 +237,59 @@ fn combo_box_dyn<'c, R>(
selected_text: WidgetText, selected_text: WidgetText,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>, icon: Option<IconPainter>,
wrap_enabled: bool,
width: Option<f32>,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
let popup_id = button_id.with("popup"); 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.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().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
AboveOrBelow::Above
};
let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { 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 // We don't want to change width when user selects something new
let full_minimum_width = ui.spacing().slider_width; let full_minimum_width = if wrap_enabled {
// Currently selected value's text will be wrapped if needed, so occupy the available width.
ui.available_width()
} else {
// 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 icon_size = Vec2::splat(ui.spacing().icon_width);
let wrap_width = if wrap_enabled {
// 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(false), f32::INFINITY, TextStyle::Button); 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 width = width.at_least(full_minimum_width);
let height = galley.size().y.max(icon_size.y); let height = galley.size().y.max(icon_size.y);
@ -243,9 +312,15 @@ fn combo_box_dyn<'c, R>(
icon_rect.expand(visuals.expansion), icon_rect.expand(visuals.expansion),
visuals, visuals,
is_popup_open, is_popup_open,
above_or_below,
); );
} else { } else {
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals); paint_default_icon(
ui.painter(),
icon_rect.expand(visuals.expansion),
visuals,
above_or_below,
);
} }
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
@ -254,14 +329,20 @@ fn combo_box_dyn<'c, R>(
}); });
if button_response.clicked() { if button_response.clicked() {
ui.memory().toggle_popup(popup_id); ui.memory_mut(|mem| mem.toggle_popup(popup_id));
} }
let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| { let inner = crate::popup::popup_above_or_below_widget(
ui,
popup_id,
&button_response,
above_or_below,
|ui| {
ScrollArea::vertical() ScrollArea::vertical()
.max_height(ui.spacing().combo_height) .max_height(ui.spacing().combo_height)
.show(ui, menu_contents) .show(ui, menu_contents)
.inner .inner
}); },
);
InnerResponse { InnerResponse {
inner, inner,
@ -305,7 +386,7 @@ fn button_frame(
epaint::RectShape { epaint::RectShape {
rect: outer_rect.expand(visuals.expansion), rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding, rounding: visuals.rounding,
fill: visuals.bg_fill, fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke, stroke: visuals.bg_stroke,
}, },
); );
@ -316,13 +397,33 @@ fn button_frame(
response response
} }
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) { fn paint_default_icon(
painter: &Painter,
rect: Rect,
visuals: &WidgetVisuals,
above_or_below: AboveOrBelow,
) {
let rect = Rect::from_center_size( let rect = Rect::from_center_size(
rect.center(), rect.center(),
vec2(rect.width() * 0.7, rect.height() * 0.45), vec2(rect.width() * 0.7, rect.height() * 0.45),
); );
painter.add(Shape::closed_line(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()], match above_or_below {
visuals.fg_stroke, AboveOrBelow::Above => {
// Upward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
visuals.fg_stroke.color,
Stroke::NONE,
)); ));
} }
AboveOrBelow::Below => {
// Downward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
}
}

View file

@ -19,11 +19,16 @@ use epaint::*;
pub struct Frame { pub struct Frame {
/// Margin within the painted frame. /// Margin within the painted frame.
pub inner_margin: Margin, pub inner_margin: Margin,
/// Margin outside the painted frame. /// Margin outside the painted frame.
pub outer_margin: Margin, pub outer_margin: Margin,
pub rounding: Rounding, pub rounding: Rounding,
pub shadow: Shadow, pub shadow: Shadow,
pub fill: Color32, pub fill: Color32,
pub stroke: Stroke, pub stroke: Stroke,
} }
@ -42,22 +47,18 @@ impl Frame {
} }
} }
pub(crate) fn side_top_panel(style: &Style) -> Self { pub fn side_top_panel(style: &Style) -> Self {
Self { Self {
inner_margin: Margin::symmetric(8.0, 2.0), inner_margin: Margin::symmetric(8.0, 2.0),
rounding: Rounding::none(), fill: style.visuals.panel_fill,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default() ..Default::default()
} }
} }
pub(crate) fn central_panel(style: &Style) -> Self { pub fn central_panel(style: &Style) -> Self {
Self { Self {
inner_margin: Margin::same(8.0), inner_margin: Margin::same(8.0),
rounding: Rounding::none(), fill: style.visuals.panel_fill,
fill: style.visuals.window_fill(),
stroke: Default::default(),
..Default::default() ..Default::default()
} }
} }
@ -75,8 +76,8 @@ impl Frame {
pub fn menu(style: &Style) -> Self { pub fn menu(style: &Style) -> Self {
Self { Self {
inner_margin: Margin::same(1.0), inner_margin: style.spacing.menu_margin,
rounding: style.visuals.widgets.noninteractive.rounding, rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow, shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(), fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(), stroke: style.visuals.window_stroke(),
@ -86,8 +87,8 @@ impl Frame {
pub fn popup(style: &Style) -> Self { pub fn popup(style: &Style) -> Self {
Self { Self {
inner_margin: style.spacing.window_margin, inner_margin: style.spacing.menu_margin,
rounding: style.visuals.widgets.noninteractive.rounding, rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow, shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(), fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(), stroke: style.visuals.window_stroke(),
@ -119,38 +120,45 @@ impl Frame {
} }
impl Frame { impl Frame {
#[inline]
pub fn fill(mut self, fill: Color32) -> Self { pub fn fill(mut self, fill: Color32) -> Self {
self.fill = fill; self.fill = fill;
self self
} }
#[inline]
pub fn stroke(mut self, stroke: Stroke) -> Self { pub fn stroke(mut self, stroke: Stroke) -> Self {
self.stroke = stroke; self.stroke = stroke;
self self
} }
#[inline]
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self { pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into(); self.rounding = rounding.into();
self self
} }
/// Margin within the painted frame. /// Margin within the painted frame.
#[inline]
pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self { pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
self.inner_margin = inner_margin.into(); self.inner_margin = inner_margin.into();
self self
} }
/// Margin outside the painted frame. /// Margin outside the painted frame.
#[inline]
pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self { pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
self.outer_margin = outer_margin.into(); self.outer_margin = outer_margin.into();
self self
} }
#[deprecated = "Renamed inner_margin in egui 0.18"] #[deprecated = "Renamed inner_margin in egui 0.18"]
#[inline]
pub fn margin(self, margin: impl Into<Margin>) -> Self { pub fn margin(self, margin: impl Into<Margin>) -> Self {
self.inner_margin(margin) self.inner_margin(margin)
} }
#[inline]
pub fn shadow(mut self, shadow: Shadow) -> Self { pub fn shadow(mut self, shadow: Shadow) -> Self {
self.shadow = shadow; self.shadow = shadow;
self self
@ -164,6 +172,16 @@ impl Frame {
} }
} }
impl Frame {
/// inner margin plus outer margin.
#[inline]
pub fn total_margin(&self) -> Margin {
self.inner_margin + self.outer_margin
}
}
// ----------------------------------------------------------------------------
pub struct Prepared { pub struct Prepared {
pub frame: Frame, pub frame: Frame,
where_to_put_background: ShapeIdx, where_to_put_background: ShapeIdx,
@ -244,6 +262,13 @@ impl Prepared {
rect rect
} }
fn content_with_margin(&self) -> Rect {
let mut rect = self.content_ui.min_rect();
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
rect
}
pub fn end(self, ui: &mut Ui) -> Response { pub fn end(self, ui: &mut Ui) -> Response {
let paint_rect = self.paint_rect(); let paint_rect = self.paint_rect();
@ -258,6 +283,6 @@ impl Prepared {
ui.painter().set(where_to_put_background, shape); ui.painter().set(where_to_put_background, shape);
} }
ui.allocate_rect(paint_rect, Sense::hover()) ui.allocate_rect(self.content_with_margin(), Sense::hover())
} }
} }

View file

@ -9,7 +9,7 @@ pub(crate) mod frame;
pub mod panel; pub mod panel;
pub mod popup; pub mod popup;
pub(crate) mod resize; pub(crate) mod resize;
pub(crate) mod scroll_area; pub mod scroll_area;
pub(crate) mod window; pub(crate) mod window;
pub use { pub use {

File diff suppressed because it is too large Load diff

View file

@ -6,44 +6,42 @@ use crate::*;
/// Same state for all tooltips. /// Same state for all tooltips.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub(crate) struct MonoState { pub(crate) struct TooltipState {
last_id: Option<Id>, last_common_id: Option<Id>,
last_size: Vec<Vec2>, individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
} }
impl MonoState { impl TooltipState {
fn load(ctx: &Context) -> Option<Self> { 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) { fn store(self, ctx: &Context) {
ctx.data().insert_temp(Id::null(), self); ctx.data_mut(|d| d.insert_temp(Id::null(), self));
} }
fn tooltip_size(&self, id: Id, index: usize) -> Option<Vec2> { fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
if self.last_id == Some(id) { if self.last_common_id == Some(common_id) {
self.last_size.get(index).cloned() Some(self.individual_ids_and_sizes.get(&index)?.1)
} else { } else {
None None
} }
} }
fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) { fn set_individual_tooltip(
if self.last_id == Some(id) { &mut self,
if let Some(stored_size) = self.last_size.get_mut(index) { common_id: Id,
*stored_size = size; index: usize,
} else { individual_id: Id,
self.last_size size: Vec2,
.extend((0..index - self.last_size.len()).map(|_| Vec2::ZERO)); ) {
self.last_size.push(size); if self.last_common_id != Some(common_id) {
} self.last_common_id = Some(common_id);
return; self.individual_ids_and_sizes.clear();
} }
self.last_id = Some(id); self.individual_ids_and_sizes
self.last_size.clear(); .insert(index, (individual_id, size));
self.last_size.extend((0..index).map(|_| Vec2::ZERO));
self.last_size.push(size);
} }
} }
@ -97,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
let suggested_pos = ctx let suggested_pos = ctx
.input() .input(|i| i.pointer.hover_pos())
.pointer
.hover_pos()
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0)); .map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
show_tooltip_at(ctx, id, suggested_pos, add_contents) show_tooltip_at(ctx, id, suggested_pos, add_contents)
} }
@ -114,7 +110,7 @@ pub fn show_tooltip_for<R>(
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
let expanded_rect = rect.expand2(vec2(2.0, 4.0)); 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()) (true, expanded_rect.left_top())
} else { } else {
(false, expanded_rect.left_bottom()) (false, expanded_rect.left_bottom())
@ -151,74 +147,84 @@ pub fn show_tooltip_at<R>(
fn show_tooltip_at_avoid_dyn<'c, R>( fn show_tooltip_at_avoid_dyn<'c, R>(
ctx: &Context, ctx: &Context,
mut id: Id, individual_id: Id,
suggested_position: Option<Pos2>, suggested_position: Option<Pos2>,
above: bool, above: bool,
mut avoid_rect: Rect, mut avoid_rect: Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<R> { ) -> Option<R> {
let mut tooltip_rect = Rect::NOTHING; let spacing = 4.0;
let mut count = 0;
let stored = ctx.frame_state().tooltip_rect; // 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(|fs| fs.tooltip_state)
.unwrap_or(crate::frame_state::TooltipFrameState {
common_id: individual_id,
rect: Rect::NOTHING,
count: 0,
});
let mut position = if let Some(stored) = stored { let mut position = if frame_state.rect.is_positive() {
// if there are multiple tooltips open they should use the same id for the `tooltip_size` caching to work. avoid_rect = avoid_rect.union(frame_state.rect);
id = stored.id;
tooltip_rect = stored.rect;
count = stored.count;
avoid_rect = avoid_rect.union(tooltip_rect);
if above { if above {
tooltip_rect.left_top() frame_state.rect.left_top() - spacing * Vec2::Y
} else { } else {
tooltip_rect.left_bottom() frame_state.rect.left_bottom() + spacing * Vec2::Y
} }
} else if let Some(position) = suggested_position { } else if let Some(position) = suggested_position {
position position
} else if ctx.memory().everything_is_visible() { } else if ctx.memory(|mem| mem.everything_is_visible()) {
Pos2::ZERO Pos2::ZERO
} else { } else {
return None; // No good place for a tooltip :( return None; // No good place for a tooltip :(
}; };
let mut state = MonoState::load(ctx).unwrap_or_default(); let mut long_state = TooltipState::load(ctx).unwrap_or_default();
let expected_size = state.tooltip_size(id, count); let expected_size =
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0)); let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
if above { if above {
position.y -= expected_size.y; 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 // check if we intersect the avoid_rect
{ {
let new_rect = Rect::from_min_size(position, expected_size); let new_rect = Rect::from_min_size(position, expected_size);
// Note: We do not use Rect::intersects() since it returns true even if the rects only touch. // Note: We use shrink so that we don't get false positives when the rects just touch
if new_rect.shrink(1.0).intersects(avoid_rect) { if new_rect.shrink(1.0).intersects(avoid_rect) {
if above { if above {
// place below instead: // place below instead:
position = avoid_rect.left_bottom(); position = avoid_rect.left_bottom() + spacing * Vec2::Y;
} else { } else {
// place above instead: // place above instead:
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y); position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
} }
} }
} }
let position = position.at_least(ctx.input().screen_rect().min); let position = position.at_least(ctx.screen_rect().min);
let InnerResponse { inner, response } = show_tooltip_area_dyn(ctx, id, position, add_contents); let area_id = frame_state.common_id.with(frame_state.count);
state.set_tooltip_size(id, count, response.rect.size()); let InnerResponse { inner, response } =
state.store(ctx); show_tooltip_area_dyn(ctx, area_id, position, add_contents);
long_state.set_individual_tooltip(
frame_state.common_id,
frame_state.count,
individual_id,
response.rect.size(),
);
long_state.store(ctx);
frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
ctx.frame_state().tooltip_rect = Some(crate::frame_state::TooltipRect {
id,
rect: tooltip_rect.union(response.rect),
count: count + 1,
});
Some(inner) Some(inner)
} }
@ -246,16 +252,17 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) ->
/// Show a pop-over window. /// Show a pop-over window.
fn show_tooltip_area_dyn<'c, R>( fn show_tooltip_area_dyn<'c, R>(
ctx: &Context, ctx: &Context,
id: Id, area_id: Id,
window_pos: Pos2, window_pos: Pos2,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
use containers::*; use containers::*;
Area::new(id) Area::new(area_id)
.order(Order::Tooltip) .order(Order::Tooltip)
.fixed_pos(window_pos) .fixed_pos(window_pos)
.constrain(true)
.interactable(false) .interactable(false)
.drag_bounds(Rect::EVERYTHING) // disable clip rect .drag_bounds(ctx.screen_rect())
.show(ctx, |ui| { .show(ctx, |ui| {
Frame::popup(&ctx.style()) Frame::popup(&ctx.style())
.show(ui, |ui| { .show(ui, |ui| {
@ -266,10 +273,47 @@ fn show_tooltip_area_dyn<'c, R>(
}) })
} }
/// Shows a popup below another widget. /// Was this popup visible last frame?
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if let Some(state) = TooltipState::load(ctx) {
if let Some(common_id) = state.last_common_id {
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
if *individual_id == tooltip_id {
let area_id = common_id.with(count);
let layer_id = LayerId::new(Order::Tooltip, area_id);
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
return true;
}
}
}
}
}
false
}
/// Helper for [`popup_above_or_below_widget`].
pub fn popup_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
popup_above_or_below_widget(
ui,
popup_id,
widget_response,
AboveOrBelow::Below,
add_contents,
)
}
/// Shows a popup above or below another widget.
/// ///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. /// 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`]. /// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
/// ///
/// Returns `None` if the popup is not open. /// Returns `None` if the popup is not open.
@ -279,30 +323,39 @@ fn show_tooltip_area_dyn<'c, R>(
/// let response = ui.button("Open popup"); /// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id"); /// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() { /// if response.clicked() {
/// ui.memory().toggle_popup(popup_id); /// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// } /// }
/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| { /// let below = egui::AboveOrBelow::Below;
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
/// ui.set_min_width(200.0); // if you want to control the size /// ui.set_min_width(200.0); // if you want to control the size
/// ui.label("Some more info, or things you can select:"); /// ui.label("Some more info, or things you can select:");
/// ui.label("…"); /// ui.label("…");
/// }); /// });
/// # }); /// # });
/// ``` /// ```
pub fn popup_below_widget<R>( pub fn popup_above_or_below_widget<R>(
ui: &Ui, ui: &Ui,
popup_id: Id, popup_id: Id,
widget_response: &Response, widget_response: &Response,
above_or_below: AboveOrBelow,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<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),
};
let inner = Area::new(popup_id) let inner = Area::new(popup_id)
.order(Order::Foreground) .order(Order::Foreground)
.fixed_pos(widget_response.rect.left_bottom()) .constrain(true)
.fixed_pos(pos)
.pivot(pivot)
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
// Note: we use a separate clip-rect for this area, so the popup can be outside the parent. // Note: we use a separate clip-rect for this area, so the popup can be outside the parent.
// See https://github.com/emilk/egui/issues/825 // See https://github.com/emilk/egui/issues/825
let frame = Frame::popup(ui.style()); let frame = Frame::popup(ui.style());
let frame_margin = frame.inner_margin + frame.outer_margin; let frame_margin = frame.total_margin();
frame frame
.show(ui, |ui| { .show(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
@ -315,8 +368,8 @@ pub fn popup_below_widget<R>(
}) })
.inner; .inner;
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() { if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
ui.memory().close_popup(); ui.memory_mut(|mem| mem.close_popup());
} }
Some(inner) Some(inner)
} else { } else {

View file

@ -18,11 +18,11 @@ pub(crate) struct State {
impl State { impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> { 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) { 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_least(self.min_size)
.at_most(self.max_size) .at_most(self.max_size)
.at_most( .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 { State {
@ -305,7 +305,7 @@ impl Resize {
paint_resize_corner(ui, &corner_response); paint_resize_corner(ui, &corner_response);
if corner_response.hovered() || corner_response.dragged() { if corner_response.hovered() || corner_response.dragged() {
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe; ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
} }
} }

View file

@ -14,11 +14,15 @@ pub struct State {
/// Positive offset means scrolling down/right /// Positive offset means scrolling down/right
pub offset: Vec2, pub offset: Vec2,
/// Were the scroll bars visible last frame?
show_scroll: [bool; 2], show_scroll: [bool; 2],
/// The content were to large to fit large frame.
content_is_too_large: [bool; 2],
/// Momentum, used for kinetic scrolling /// Momentum, used for kinetic scrolling
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub vel: Vec2, vel: Vec2,
/// Mouse offset relative to the top of the handle when started moving the handle. /// Mouse offset relative to the top of the handle when started moving the handle.
scroll_start_offset_from_top_left: [Option<f32>; 2], scroll_start_offset_from_top_left: [Option<f32>; 2],
@ -34,6 +38,7 @@ impl Default for State {
Self { Self {
offset: Vec2::ZERO, offset: Vec2::ZERO,
show_scroll: [false; 2], show_scroll: [false; 2],
content_is_too_large: [false; 2],
vel: Vec2::ZERO, vel: Vec2::ZERO,
scroll_start_offset_from_top_left: [None; 2], scroll_start_offset_from_top_left: [None; 2],
scroll_stuck_to_end: [true; 2], scroll_stuck_to_end: [true; 2],
@ -43,11 +48,11 @@ impl Default for State {
impl State { impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> { 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) { pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self); ctx.data_mut(|d| d.insert_persisted(id, self));
} }
} }
@ -61,6 +66,10 @@ pub struct ScrollAreaOutput<R> {
/// The current state of the scroll area. /// The current state of the scroll area.
pub state: State, pub state: State,
/// The size of the content. If this is larger than [`Self::inner_rect`],
/// then there was need for scrolling.
pub content_size: Vec2,
/// Where on the screen the content is (excludes scroll bars). /// Where on the screen the content is (excludes scroll bars).
pub inner_rect: Rect, pub inner_rect: Rect,
} }
@ -88,8 +97,10 @@ pub struct ScrollArea {
id_source: Option<Id>, id_source: Option<Id>,
offset_x: Option<f32>, offset_x: Option<f32>,
offset_y: Option<f32>, offset_y: Option<f32>,
/// If false, we ignore scroll events. /// If false, we ignore scroll events.
scrolling_enabled: bool, scrolling_enabled: bool,
drag_to_scroll: bool,
/// If true for vertical or horizontal the scroll wheel will stick to the /// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true /// end position until user manually changes position. It will become true
@ -132,6 +143,7 @@ impl ScrollArea {
offset_x: None, offset_x: None,
offset_y: None, offset_y: None,
scrolling_enabled: true, scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2], stick_to_end: [false; 2],
} }
} }
@ -193,6 +205,8 @@ impl ScrollArea {
/// Set the horizontal and vertical scroll offset position. /// Set the horizontal and vertical scroll offset position.
/// ///
/// Positive offset means scrolling down/right.
///
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`], /// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
@ -204,6 +218,8 @@ impl ScrollArea {
/// Set the vertical scroll offset position. /// Set the vertical scroll offset position.
/// ///
/// Positive offset means scrolling down.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self { pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
@ -213,6 +229,8 @@ impl ScrollArea {
/// Set the horizontal scroll offset position. /// Set the horizontal scroll offset position.
/// ///
/// Positive offset means scrolling right.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self { pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
@ -238,12 +256,13 @@ impl ScrollArea {
self self
} }
/// Control the scrolling behavior /// Control the scrolling behavior.
/// If `true` (default), the scroll area will respond to user scrolling ///
/// If `false`, the scroll area will not respond to user scrolling /// * If `true` (default), the scroll area will respond to user scrolling.
/// * If `false`, the scroll area will not respond to user scrolling.
/// ///
/// This can be used, for example, to optionally freeze scrolling while the user /// This can be used, for example, to optionally freeze scrolling while the user
/// is inputing text in a [`TextEdit`] widget contained within the scroll area. /// is typing text in a [`TextEdit`] widget contained within the scroll area.
/// ///
/// This controls both scrolling directions. /// This controls both scrolling directions.
pub fn enable_scrolling(mut self, enable: bool) -> Self { pub fn enable_scrolling(mut self, enable: bool) -> Self {
@ -251,10 +270,22 @@ impl ScrollArea {
self 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? /// For each axis, should the containing area shrink if the content is small?
/// ///
/// If true, egui will add blank space outside the scroll area. /// * If `true`, egui will add blank space outside the scroll area.
/// If false, egui will add blank space inside the scroll area. /// * If `false`, egui will add blank space inside the scroll area.
/// ///
/// Default: `[true; 2]`. /// Default: `[true; 2]`.
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self { pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
@ -320,6 +351,7 @@ impl ScrollArea {
offset_x, offset_x,
offset_y, offset_y,
scrolling_enabled, scrolling_enabled,
drag_to_scroll,
stick_to_end, stick_to_end,
} = self; } = self;
@ -394,18 +426,75 @@ impl ScrollArea {
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size); 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_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 { 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]; 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); content_ui.set_clip_rect(content_clip_rect);
}
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
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.
let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag());
if content_response.dragged() {
for d in 0..2 {
if has_bar[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;
}
}
} else {
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input(|i| i.unstable_dt);
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
state.vel = Vec2::ZERO;
} else {
state.vel -= friction * state.vel.normalized();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset -= state.vel * dt;
ui.ctx().request_repaint();
}
}
}
Prepared { Prepared {
id, id,
state, state,
@ -460,9 +549,13 @@ impl ScrollArea {
self.show_viewport(ui, |ui, viewport| { self.show_viewport(ui, |ui, viewport| {
ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0)); ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
let min_row = (viewport.min.y / row_height_with_spacing).floor() as usize; let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
let max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1; let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
let max_row = max_row.at_most(total_rows); if max_row > total_rows {
let diff = max_row.saturating_sub(min_row);
max_row = total_rows;
min_row = total_rows.saturating_sub(diff);
}
let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing; let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing; let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
@ -479,7 +572,7 @@ impl ScrollArea {
/// This can be used to only paint the visible part of the contents. /// This can be used to only paint the visible part of the contents.
/// ///
/// `add_contents` is past the viewport, which is the relative view of the content. /// `add_contents` is given the viewport rectangle, which is the relative view of the content.
/// So if the passed rect has min = zero, then show the top left content (the user has not scrolled). /// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
pub fn show_viewport<R>( pub fn show_viewport<R>(
self, self,
@ -498,18 +591,20 @@ impl ScrollArea {
let id = prepared.id; let id = prepared.id;
let inner_rect = prepared.inner_rect; let inner_rect = prepared.inner_rect;
let inner = add_contents(&mut prepared.content_ui, prepared.viewport); let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
let state = prepared.end(ui); let (content_size, state) = prepared.end(ui);
ScrollAreaOutput { ScrollAreaOutput {
inner, inner,
id, id,
state, state,
content_size,
inner_rect, inner_rect,
} }
} }
} }
impl Prepared { impl Prepared {
fn end(self, ui: &mut Ui) -> State { /// Returns content size and state
fn end(self, ui: &mut Ui) -> (Vec2, State) {
let Prepared { let Prepared {
id, id,
mut state, mut state,
@ -529,7 +624,9 @@ impl Prepared {
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if has_bar[d] {
// We take the scroll target so only this ScrollArea will use it: // 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 { if let Some((scroll, align)) = scroll_target {
let min = content_ui.min_rect().min[d]; let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect(); let clip_rect = content_ui.clip_rect();
@ -580,23 +677,7 @@ impl Prepared {
}; };
} }
let mut inner_rect = Rect::from_min_size(inner_rect.min, inner_size); Rect::from_min_size(inner_rect.min, inner_size)
// The window that egui sits in can't be expanded by egui, so we need to respect it:
for d in 0..2 {
if !has_bar[d] {
// HACK for when we have a vertical-only scroll area in a top level panel,
// and that panel is not wide enough for the contents.
// This code ensures we still see the scroll bar!
let max = ui.input().screen_rect().max[d]
- current_bar_use[d]
- ui.spacing().item_spacing[d];
inner_rect.max[d] = inner_rect.max[d].at_most(max);
// TODO(emilk): maybe auto-enable horizontal/vertical scrolling if this limit is reached
}
}
inner_rect
}; };
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
@ -606,49 +687,11 @@ impl Prepared {
content_size.y > inner_rect.height(), content_size.y > inner_rect.height(),
]; ];
if content_is_too_large[0] || content_is_too_large[1] {
// Drag contents to scroll (for touch screens mostly):
let sense = if self.scrolling_enabled {
Sense::drag()
} else {
Sense::hover()
};
let content_response = ui.interact(inner_rect, id.with("area"), sense);
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];
state.scroll_stuck_to_end[d] = false;
} else {
state.vel[d] = 0.0;
}
}
} 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 friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
state.vel = Vec2::ZERO;
} else {
state.vel -= friction * state.vel.normalized();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset -= state.vel * dt;
ui.ctx().request_repaint();
}
}
}
let max_offset = content_size - inner_rect.size(); let max_offset = content_size - inner_rect.size();
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if has_bar[d] {
let mut frame_state = ui.ctx().frame_state(); let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scroll_delta = frame_state.scroll_delta;
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; 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; let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
@ -656,7 +699,7 @@ impl Prepared {
if scrolling_up || scrolling_down { if scrolling_up || scrolling_down {
state.offset[d] -= scroll_delta[d]; state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it. // 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; state.scroll_stuck_to_end[d] = false;
} }
} }
@ -685,13 +728,29 @@ impl Prepared {
continue; continue;
} }
// margin between contents and scroll bar // margin on either side of the scroll bar
let margin = animation_t * ui.spacing().item_spacing.x; let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin;
let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1) let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin;
let max_cross = outer_rect.max[1 - d]; // right of vertical scroll (d == 1) let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1)
let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1)
let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1) let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1)
let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1) let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1)
if ui.clip_rect().max[1 - d] < max_cross + outer_margin {
// Move the scrollbar so it is visible. This is needed in some cases.
// For instance:
// * When we have a vertical-only scroll area in a top level panel,
// and that panel is not wide enough for the contents.
// * When one ScrollArea is nested inside another, and the outer
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
// is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
let width = max_cross - min_cross;
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
min_cross = max_cross - width;
}
let outer_scroll_rect = if d == 0 { let outer_scroll_rect = if d == 0 {
Rect::from_min_max( Rect::from_min_max(
pos2(inner_rect.left(), min_cross), pos2(inner_rect.left(), min_cross),
@ -782,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 { if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size( handle_rect = Rect::from_center_size(
handle_rect.center(), handle_rect.center(),
@ -837,14 +896,17 @@ impl Prepared {
]; ];
state.show_scroll = show_scroll_this_frame; state.show_scroll = show_scroll_this_frame;
state.content_is_too_large = content_is_too_large;
state.store(ui.ctx(), id); state.store(ui.ctx(), id);
state (content_size, state)
} }
} }
/// Width of a vertical scrollbar, or height of a horizontal scroll bar /// Width of a vertical scrollbar, or height of a horizontal scroll bar
fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 { fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 {
ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width ui.spacing().scroll_bar_inner_margin
+ ui.spacing().scroll_bar_width
+ ui.spacing().scroll_bar_outer_margin
} }

View file

@ -10,7 +10,7 @@ use super::*;
/// ///
/// You can customize: /// You can customize:
/// * title /// * 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 has a scroll area (off by default)
/// * if the window can be collapsed (minimized) to just the title bar (yes, 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) /// * if there should be a close button (none by default)
@ -30,6 +30,7 @@ pub struct Window<'open> {
resize: Resize, resize: Resize,
scroll: ScrollArea, scroll: ScrollArea,
collapsible: bool, collapsible: bool,
default_open: bool,
with_title_bar: bool, with_title_bar: bool,
} }
@ -39,7 +40,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id. /// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self { pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading); let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(title.text()); let area = Area::new(Id::new(title.text()));
Self { Self {
title, title,
open: None, open: None,
@ -51,6 +52,7 @@ impl<'open> Window<'open> {
.default_size([340.0, 420.0]), // Default inner size of a window .default_size([340.0, 420.0]), // Default inner size of a window
scroll: ScrollArea::neither(), scroll: ScrollArea::neither(),
collapsible: true, collapsible: true,
default_open: true,
with_title_bar: true, with_title_bar: true,
} }
} }
@ -77,15 +79,27 @@ impl<'open> Window<'open> {
self 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))` /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
/// Not sure this is a good interface for this. // TODO(emilk): I'm not sure this is a good interface for this.
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self { pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
mutate(&mut self); mutate(&mut self);
self self
} }
/// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))` /// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
/// Not sure this is a good interface for this. // TODO(emilk): I'm not sure this is a good interface for this.
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self { pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
self.resize = mutate(self.resize); self.resize = mutate(self.resize);
self self
@ -122,6 +136,30 @@ impl<'open> Window<'open> {
self self
} }
/// Sets the window position and prevents it from being dragged around.
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
self.area = self.area.fixed_pos(pos);
self
}
/// Constrains this window to the screen bounds.
pub fn constrain(mut self, constrain: bool) -> Self {
self.area = self.area.constrain(constrain);
self
}
/// Where the "root" of the window is.
///
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
/// then [`Self::fixed_pos`] will set the position of the right-top
/// corner of the window.
///
/// Default: [`Align2::LEFT_TOP`].
pub fn pivot(mut self, pivot: Align2) -> Self {
self.area = self.area.pivot(pivot);
self
}
/// Set anchor and distance. /// Set anchor and distance.
/// ///
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
@ -138,6 +176,12 @@ impl<'open> Window<'open> {
self 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. /// Set initial size of the window.
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self { pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.default_size(default_size); self.resize = self.resize.default_size(default_size);
@ -156,23 +200,17 @@ impl<'open> Window<'open> {
self self
} }
/// Set initial position and size of the window.
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
/// Sets the window position and prevents it from being dragged around.
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
self.area = self.area.fixed_pos(pos);
self
}
/// Sets the window size and prevents it from being resized by dragging its edges. /// Sets the window size and prevents it from being resized by dragging its edges.
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self { pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size); self.resize = self.resize.fixed_size(size);
self self
} }
/// Set initial position and size of the window.
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
/// Sets the window pos and size and prevents it from being moved and resized by dragging its edges. /// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
pub fn fixed_rect(self, rect: Rect) -> Self { pub fn fixed_rect(self, rect: Rect) -> Self {
self.fixed_pos(rect.min).fixed_size(rect.size()) self.fixed_pos(rect.min).fixed_size(rect.size())
@ -257,12 +295,14 @@ impl<'open> Window<'open> {
resize, resize,
scroll, scroll,
collapsible, collapsible,
default_open,
with_title_bar, with_title_bar,
} = self; } = self;
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); 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); area.show_open_close_animation(ctx, &frame, is_open);
if !is_open { if !is_open {
@ -273,7 +313,7 @@ impl<'open> Window<'open> {
let area_layer_id = area.layer(); let area_layer_id = area.layer();
let resize_id = area_id.with("resize"); let resize_id = area_id.with("resize");
let mut collapsing = 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 is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed); let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
@ -300,7 +340,7 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect // Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar { let title_bar_height = if with_title_bar {
let style = ctx.style(); 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 { } else {
0.0 0.0
}; };
@ -386,7 +426,7 @@ impl<'open> Window<'open> {
ctx.style().visuals.widgets.active, ctx.style().visuals.widgets.active,
); );
} else if let Some(hover_interaction) = hover_interaction { } 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( paint_frame_interaction(
&mut area_content_ui, &mut area_content_ui,
outer_rect, outer_rect,
@ -398,9 +438,12 @@ impl<'open> Window<'open> {
content_inner content_inner
}; };
area.state_mut().pos = ctx {
let pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds()) .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); let full_response = area.end(ctx, area_content_ui);
@ -481,13 +524,13 @@ pub(crate) struct WindowInteraction {
impl WindowInteraction { impl WindowInteraction {
pub fn set_cursor(&self, ctx: &Context) { pub fn set_cursor(&self, ctx: &Context) {
if (self.left && self.top) || (self.right && self.bottom) { 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) { } 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 { } else if self.left || self.right {
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal; ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
} else if self.bottom || self.top { } else if self.bottom || self.top {
ctx.output().cursor_icon = CursorIcon::ResizeVertical; ctx.set_cursor_icon(CursorIcon::ResizeVertical);
} }
} }
@ -510,7 +553,7 @@ fn interact(
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds()); 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" // 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 window_interaction.is_resize() {
if let Some(mut state) = resize::State::load(ctx, resize_id) { if let Some(mut state) = resize::State::load(ctx, resize_id) {
@ -519,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) Some(window_interaction)
} }
@ -527,11 +570,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
window_interaction.set_cursor(ctx); window_interaction.set_cursor(ctx);
// Only move/resize windows with primary mouse button: // Only move/resize windows with primary mouse button:
if !ctx.input().pointer.primary_down() { if !ctx.input(|i| i.pointer.primary_down()) {
return None; 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 let mut rect = window_interaction.start_rect; // prevent drift
if window_interaction.is_resize() { if window_interaction.is_resize() {
@ -553,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 // 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, // 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. // but instead let other widgets to the steal. HACK.
if !ctx.input().pointer.any_pressed() { if !ctx.input(|i| i.pointer.any_pressed()) {
let press_origin = ctx.input().pointer.press_origin()?; let press_origin = ctx.input(|i| i.pointer.press_origin())?;
let delta = pointer_pos - press_origin; let delta = pointer_pos - press_origin;
rect = rect.translate(delta); rect = rect.translate(delta);
} }
@ -572,29 +615,31 @@ fn window_interaction(
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> 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) { if drag_id.is_some() && drag_id != Some(id) {
return None; 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 window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx); hover_window_interaction.set_cursor(ctx);
if ctx.input().pointer.any_pressed() && ctx.input().pointer.primary_down() { if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
ctx.memory().interaction.drag_id = Some(id); ctx.memory_mut(|mem| {
ctx.memory().interaction.drag_is_window = true; mem.interaction.drag_id = Some(id);
mem.interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction); window_interaction = Some(hover_window_interaction);
ctx.memory().window_interaction = window_interaction; mem.window_interaction = window_interaction;
});
} }
} }
} }
if let Some(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 { if is_active && window_interaction.area_layer_id == area_layer_id {
return Some(window_interaction); return Some(window_interaction);
@ -610,9 +655,9 @@ fn resize_hover(
area_layer_id: LayerId, area_layer_id: LayerId,
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
let pointer = ctx.input().pointer.interact_pos()?; let pointer = ctx.input(|i| i.pointer.interact_pos())?;
if ctx.input().pointer.any_down() && !ctx.input().pointer.any_pressed() { if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
return None; // already dragging (something) return None; // already dragging (something)
} }
@ -622,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 // Another widget will become active if we drag here
return None; return None;
} }
@ -784,8 +829,8 @@ fn show_title_bar(
collapsible: bool, collapsible: bool,
) -> TitleBar { ) -> TitleBar {
let inner_response = ui.horizontal(|ui| { let inner_response = ui.horizontal(|ui| {
let height = title let height = ui
.font_height(&ui.fonts(), ui.style()) .fonts(|fonts| title.font_height(fonts, ui.style()))
.max(ui.spacing().interact_size.y); .max(ui.spacing().interact_size.y);
ui.set_min_height(height); ui.set_min_height(height);
@ -879,8 +924,11 @@ impl TitleBar {
ui.painter().hline(outer_rect.x_range(), y, stroke); ui.painter().hline(outer_rect.x_range(), y, stroke);
} }
// Don't cover the close- and collapse buttons:
let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0));
if ui if ui
.interact(self.rect, self.id, Sense::click()) .interact(double_click_rect, self.id, Sense::click())
.double_clicked() .double_clicked()
&& collapsible && collapsible
{ {

File diff suppressed because it is too large Load diff

View file

@ -13,10 +13,10 @@ use crate::emath::*;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RawInput { pub struct RawInput {
/// Position and size of the area that egui should use. /// Position and size of the area that egui should use, in points.
/// Usually you would set this to /// Usually you would set this to
/// ///
/// `Some(Rect::from_pos_size(Default::default(), screen_size))`. /// `Some(Rect::from_pos_size(Default::default(), screen_size_in_points))`.
/// ///
/// but you could also constrain egui to some smaller portion of your window if you like. /// but you could also constrain egui to some smaller portion of your window if you like.
/// ///
@ -133,7 +133,7 @@ impl RawInput {
} }
/// A file about to be dropped into egui. /// A file about to be dropped into egui.
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct HoveredFile { pub struct HoveredFile {
/// Set by the `egui-winit` backend. /// Set by the `egui-winit` backend.
@ -144,7 +144,7 @@ pub struct HoveredFile {
} }
/// A file dropped into egui. /// A file dropped into egui.
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DroppedFile { pub struct DroppedFile {
/// Set by the `egui-winit` backend. /// Set by the `egui-winit` backend.
@ -187,6 +187,15 @@ pub enum Event {
/// Was it pressed or released? /// Was it pressed or released?
pressed: bool, pressed: bool,
/// 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. /// The state of the modifier keys at the time of the event.
modifiers: Modifiers, modifiers: Modifiers,
}, },
@ -268,6 +277,10 @@ pub enum Event {
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
force: f32, force: f32,
}, },
/// An assistive technology (e.g. screen reader) requested an action.
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit::ActionRequest),
} }
/// Mouse button (or similar for touch input) /// Mouse button (or similar for touch input)
@ -297,7 +310,11 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// State of the modifier keys. These must be fed to egui. /// State of the modifier keys. These must be fed to egui.
/// ///
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`]. /// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
#[derive(Clone, Copy, Debug, Default, PartialEq)] ///
/// 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, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers { pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac). /// Either of the alt keys are down (option ⌥ on Mac).
@ -321,10 +338,6 @@ pub struct Modifiers {
} }
impl Modifiers { impl Modifiers {
pub fn new() -> Self {
Default::default()
}
pub const NONE: Self = Self { pub const NONE: Self = Self {
alt: false, alt: false,
ctrl: false, ctrl: false,
@ -354,6 +367,8 @@ impl Modifiers {
mac_cmd: false, mac_cmd: false,
command: false, command: false,
}; };
#[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"]
pub const ALT_SHIFT: Self = Self { pub const ALT_SHIFT: Self = Self {
alt: true, alt: true,
ctrl: false, ctrl: false,
@ -380,24 +395,50 @@ impl Modifiers {
command: true, command: true,
}; };
#[inline(always)] /// ```
/// # use egui::Modifiers;
/// assert_eq!(
/// Modifiers::CTRL | Modifiers::ALT,
/// Modifiers { ctrl: true, alt: true, ..Default::default() }
/// );
/// assert_eq!(
/// Modifiers::ALT.plus(Modifiers::CTRL),
/// Modifiers::CTRL.plus(Modifiers::ALT),
/// );
/// assert_eq!(
/// Modifiers::CTRL | Modifiers::ALT,
/// Modifiers::CTRL.plus(Modifiers::ALT),
/// );
/// ```
#[inline]
pub const fn plus(self, rhs: Self) -> Self {
Self {
alt: self.alt | rhs.alt,
ctrl: self.ctrl | rhs.ctrl,
shift: self.shift | rhs.shift,
mac_cmd: self.mac_cmd | rhs.mac_cmd,
command: self.command | rhs.command,
}
}
#[inline]
pub fn is_none(&self) -> bool { pub fn is_none(&self) -> bool {
self == &Self::default() self == &Self::default()
} }
#[inline(always)] #[inline]
pub fn any(&self) -> bool { pub fn any(&self) -> bool {
!self.is_none() !self.is_none()
} }
/// Is shift the only pressed button? /// Is shift the only pressed button?
#[inline(always)] #[inline]
pub fn shift_only(&self) -> bool { pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command) self.shift && !(self.alt || self.command)
} }
/// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed.
#[inline(always)] #[inline]
pub fn command_only(&self) -> bool { pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command !self.alt && !self.shift && self.command
} }
@ -453,17 +494,82 @@ impl Modifiers {
impl std::ops::BitOr for Modifiers { impl std::ops::BitOr for Modifiers {
type Output = Self; type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self { fn bitor(self, rhs: Self) -> Self {
Self { self.plus(rhs)
alt: self.alt | rhs.alt,
ctrl: self.ctrl | rhs.ctrl,
shift: self.shift | rhs.shift,
mac_cmd: self.mac_cmd | rhs.mac_cmd,
command: self.command | rhs.command,
} }
} }
// ----------------------------------------------------------------------------
/// Names of different modifier keys.
///
/// Used to name modifiers.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ModifierNames<'a> {
pub is_short: bool,
pub alt: &'a str,
pub ctrl: &'a str,
pub shift: &'a str,
pub mac_cmd: &'a str,
/// What goes between the names
pub concat: &'a str,
} }
impl ModifierNames<'static> {
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font.
pub const SYMBOLS: Self = Self {
is_short: true,
alt: "",
ctrl: "^",
shift: "",
mac_cmd: "",
concat: "",
};
/// Alt, Ctrl, Shift, Cmd
pub const NAMES: Self = Self {
is_short: false,
alt: "Alt",
ctrl: "Ctrl",
shift: "Shift",
mac_cmd: "Cmd",
concat: "+",
};
}
impl<'a> ModifierNames<'a> {
pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String {
let mut s = String::new();
let mut append_if = |modifier_is_active, modifier_name| {
if modifier_is_active {
if !s.is_empty() {
s += self.concat;
}
s += modifier_name;
}
};
if is_mac {
append_if(modifiers.ctrl, self.ctrl);
append_if(modifiers.shift, self.shift);
append_if(modifiers.alt, self.alt);
append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd);
} else {
append_if(modifiers.ctrl || modifiers.command, self.ctrl);
append_if(modifiers.alt, self.alt);
append_if(modifiers.shift, self.shift);
}
s
}
}
// ----------------------------------------------------------------------------
/// Keyboard keys. /// Keyboard keys.
/// ///
/// Includes all keys egui is interested in (such as `Home` and `End`) /// Includes all keys egui is interested in (such as `Home` and `End`)
@ -492,6 +598,11 @@ pub enum Key {
PageUp, PageUp,
PageDown, PageDown,
/// The virtual keycode for the Minus key.
Minus,
/// The virtual keycode for the Plus/Equals key.
PlusEquals,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num0, Num0,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
@ -563,6 +674,151 @@ pub enum Key {
F20, F20,
} }
impl Key {
/// Emoji or name representing the key
pub fn symbol_or_name(self) -> &'static str {
// TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys).
// Before we do we must first make sure they are supported in `Fonts` though,
// so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something.
match self {
Key::ArrowDown => "",
Key::ArrowLeft => "",
Key::ArrowRight => "",
Key::ArrowUp => "",
Key::Minus => "-",
Key::PlusEquals => "+",
_ => self.name(),
}
}
/// Human-readable English name.
pub fn name(self) -> &'static str {
match self {
Key::ArrowDown => "Down",
Key::ArrowLeft => "Left",
Key::ArrowRight => "Right",
Key::ArrowUp => "Up",
Key::Escape => "Escape",
Key::Tab => "Tab",
Key::Backspace => "Backspace",
Key::Enter => "Enter",
Key::Space => "Space",
Key::Insert => "Insert",
Key::Delete => "Delete",
Key::Home => "Home",
Key::End => "End",
Key::PageUp => "PageUp",
Key::PageDown => "PageDown",
Key::Minus => "Minus",
Key::PlusEquals => "Plus",
Key::Num0 => "0",
Key::Num1 => "1",
Key::Num2 => "2",
Key::Num3 => "3",
Key::Num4 => "4",
Key::Num5 => "5",
Key::Num6 => "6",
Key::Num7 => "7",
Key::Num8 => "8",
Key::Num9 => "9",
Key::A => "A",
Key::B => "B",
Key::C => "C",
Key::D => "D",
Key::E => "E",
Key::F => "F",
Key::G => "G",
Key::H => "H",
Key::I => "I",
Key::J => "J",
Key::K => "K",
Key::L => "L",
Key::M => "M",
Key::N => "N",
Key::O => "O",
Key::P => "P",
Key::Q => "Q",
Key::R => "R",
Key::S => "S",
Key::T => "T",
Key::U => "U",
Key::V => "V",
Key::W => "W",
Key::X => "X",
Key::Y => "Y",
Key::Z => "Z",
Key::F1 => "F1",
Key::F2 => "F2",
Key::F3 => "F3",
Key::F4 => "F4",
Key::F5 => "F5",
Key::F6 => "F6",
Key::F7 => "F7",
Key::F8 => "F8",
Key::F9 => "F9",
Key::F10 => "F10",
Key::F11 => "F11",
Key::F12 => "F12",
Key::F13 => "F13",
Key::F14 => "F14",
Key::F15 => "F15",
Key::F16 => "F16",
Key::F17 => "F17",
Key::F18 => "F18",
Key::F19 => "F19",
Key::F20 => "F20",
}
}
}
// ----------------------------------------------------------------------------
/// A keyboard shortcut, e.g. `Ctrl+Alt+W`.
///
/// Can be used with [`crate::InputState::consume_shortcut`]
/// and [`crate::Context::format_shortcut`].
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct KeyboardShortcut {
pub modifiers: Modifiers,
pub key: Key,
}
impl KeyboardShortcut {
pub const fn new(modifiers: Modifiers, key: Key) -> Self {
Self { modifiers, key }
}
pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String {
let mut s = names.format(&self.modifiers, is_mac);
if !s.is_empty() {
s += names.concat;
}
if names.is_short {
s += self.key.symbol_or_name();
} else {
s += self.key.name();
}
s
}
}
#[test]
fn format_kb_shortcut() {
let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F);
assert_eq!(
cmd_shift_f.format(&ModifierNames::NAMES, false),
"Ctrl+Shift+F"
);
assert_eq!(
cmd_shift_f.format(&ModifierNames::NAMES, true),
"Shift+Cmd+F"
);
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F");
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
}
// ----------------------------------------------------------------------------
impl RawInput { impl RawInput {
pub fn ui(&self, ui: &mut crate::Ui) { pub fn ui(&self, ui: &mut crate::Ui) {
let Self { let Self {

View file

@ -70,7 +70,7 @@ pub struct PlatformOutput {
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() { /// if ui.button("📋").clicked() {
/// ui.output().copied_text = "some_text".to_string(); /// ui.output_mut(|o| o.copied_text = "some_text".to_string());
/// } /// }
/// # }); /// # });
/// ``` /// ```
@ -85,6 +85,9 @@ pub struct PlatformOutput {
/// Screen-space position of text edit cursor (used for IME). /// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>, pub text_cursor_pos: Option<crate::Pos2>,
#[cfg(feature = "accesskit")]
pub accesskit_update: Option<accesskit::TreeUpdate>,
} }
impl PlatformOutput { impl PlatformOutput {
@ -121,6 +124,8 @@ impl PlatformOutput {
mut events, mut events,
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = newer; } = newer;
self.cursor_icon = cursor_icon; self.cursor_icon = cursor_icon;
@ -133,6 +138,13 @@ impl PlatformOutput {
self.events.append(&mut events); self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor; self.mutable_text_under_cursor = mutable_text_under_cursor;
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos); self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
#[cfg(feature = "accesskit")]
{
// egui produces a complete AccessKit tree for each frame,
// so overwrite rather than appending.
self.accesskit_update = accesskit_update;
}
} }
/// Take everything ephemeral (everything except `cursor_icon` currently) /// Take everything ephemeral (everything except `cursor_icon` currently)
@ -144,7 +156,7 @@ impl PlatformOutput {
} }
/// What URL to open, and how. /// What URL to open, and how.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct OpenUrl { pub struct OpenUrl {
pub url: String, pub url: String,
@ -178,7 +190,7 @@ impl OpenUrl {
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration. /// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
/// ///
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>. /// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum CursorIcon { pub enum CursorIcon {
/// Normal cursor icon, whatever that is. /// Normal cursor icon, whatever that is.
@ -372,6 +384,19 @@ pub enum OutputEvent {
ValueChanged(WidgetInfo), ValueChanged(WidgetInfo),
} }
impl OutputEvent {
pub fn widget_info(&self) -> &WidgetInfo {
match self {
OutputEvent::Clicked(info)
| OutputEvent::DoubleClicked(info)
| OutputEvent::TripleClicked(info)
| OutputEvent::FocusGained(info)
| OutputEvent::TextSelectionChanged(info)
| OutputEvent::ValueChanged(info) => info,
}
}
}
impl std::fmt::Debug for OutputEvent { impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {

View file

@ -1,14 +1,21 @@
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use crate::*; use crate::{id::IdSet, *};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct TooltipRect { pub(crate) struct TooltipFrameState {
pub id: Id, pub common_id: Id,
pub rect: Rect, pub rect: Rect,
pub count: usize, pub count: usize,
} }
#[cfg(feature = "accesskit")]
#[derive(Clone)]
pub(crate) struct AccessKitFrameState {
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
pub(crate) parent_stack: Vec<Id>,
}
/// State that is collected during a frame and then cleared. /// State that is collected during a frame and then cleared.
/// Short-term (single frame) memory. /// Short-term (single frame) memory.
#[derive(Clone)] #[derive(Clone)]
@ -32,7 +39,7 @@ pub(crate) struct FrameState {
/// If a tooltip has been shown this frame, where was it? /// If a tooltip has been shown this frame, where was it?
/// This is used to prevent multiple tooltips to cover each other. /// This is used to prevent multiple tooltips to cover each other.
/// Initialized to `None` at the start of each frame. /// Initialized to `None` at the start of each frame.
pub(crate) tooltip_rect: Option<TooltipRect>, pub(crate) tooltip_state: Option<TooltipFrameState>,
/// Set to [`InputState::scroll_delta`] on the start of each frame. /// Set to [`InputState::scroll_delta`] on the start of each frame.
/// ///
@ -41,6 +48,15 @@ pub(crate) struct FrameState {
/// horizontal, vertical /// horizontal, vertical
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2], pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
#[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 { impl Default for FrameState {
@ -50,9 +66,13 @@ impl Default for FrameState {
available_rect: Rect::NAN, available_rect: Rect::NAN,
unused_rect: Rect::NAN, unused_rect: Rect::NAN,
used_by_panels: Rect::NAN, used_by_panels: Rect::NAN,
tooltip_rect: None, tooltip_state: None,
scroll_delta: Vec2::ZERO, scroll_delta: Vec2::ZERO,
scroll_target: [None, None], scroll_target: [None, None],
#[cfg(feature = "accesskit")]
accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),
} }
} }
} }
@ -64,18 +84,29 @@ impl FrameState {
available_rect, available_rect,
unused_rect, unused_rect,
used_by_panels, used_by_panels,
tooltip_rect, tooltip_state,
scroll_delta, scroll_delta,
scroll_target, scroll_target,
#[cfg(feature = "accesskit")]
accesskit_state,
highlight_this_frame,
highlight_next_frame,
} = self; } = self;
used_ids.clear(); used_ids.clear();
*available_rect = input.screen_rect(); *available_rect = input.screen_rect();
*unused_rect = input.screen_rect(); *unused_rect = input.screen_rect();
*used_by_panels = Rect::NOTHING; *used_by_panels = Rect::NOTHING;
*tooltip_rect = None; *tooltip_state = None;
*scroll_delta = input.scroll_delta; *scroll_delta = input.scroll_delta;
*scroll_target = [None, None]; *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. /// How much space is still available after panels has been added.

View file

@ -8,14 +8,14 @@ pub(crate) struct State {
impl State { impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> { 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) { pub fn store(self, ctx: &Context, id: Id) {
// We don't persist Grids, because // We don't persist Grids, because
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time) // 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 // 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) { fn set_min_col_width(&mut self, col: usize, width: f32) {
@ -51,6 +51,9 @@ pub(crate) struct GridLayout {
style: std::sync::Arc<Style>, style: std::sync::Arc<Style>,
id: Id, id: Id,
/// First frame (no previous know state).
is_first_frame: bool,
/// State previous frame (if any). /// State previous frame (if any).
/// This can be used to predict future sizes of cells. /// This can be used to predict future sizes of cells.
prev_state: State, prev_state: State,
@ -71,8 +74,9 @@ pub(crate) struct GridLayout {
} }
impl GridLayout { impl GridLayout {
pub(crate) fn new(ui: &Ui, id: Id) -> Self { pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
let prev_state = State::load(ui.ctx(), id).unwrap_or_default(); let is_first_frame = prev_state.is_none();
let prev_state = prev_state.unwrap_or_default();
// TODO(emilk): respect current layout // TODO(emilk): respect current layout
@ -88,6 +92,7 @@ impl GridLayout {
ctx: ui.ctx().clone(), ctx: ui.ctx().clone(),
style: ui.style().clone(), style: ui.style().clone(),
id, id,
is_first_frame,
prev_state, prev_state,
curr_state: State::default(), curr_state: State::default(),
initial_available, initial_available,
@ -125,7 +130,16 @@ impl GridLayout {
let is_last_column = Some(self.col + 1) == self.num_columns; let is_last_column = Some(self.col + 1) == self.num_columns;
let width = if is_last_column { let width = if is_last_column {
(self.initial_available.right() - region.cursor.left()).at_most(self.max_cell_size.x) // The first frame we don't really know the widths of the previous columns,
// so returning a big available width here can cause trouble.
if self.is_first_frame {
self.curr_state
.col_width(self.col)
.unwrap_or(self.min_cell_size.x)
} else {
(self.initial_available.right() - region.cursor.left())
.at_most(self.max_cell_size.x)
}
} else if self.max_cell_size.x.is_finite() { } else if self.max_cell_size.x.is_finite() {
// TODO(emilk): should probably heed `prev_state` here too // TODO(emilk): should probably heed `prev_state` here too
self.max_cell_size.x self.max_cell_size.x
@ -264,7 +278,7 @@ impl GridLayout {
pub struct Grid { pub struct Grid {
id_source: Id, id_source: Id,
num_columns: Option<usize>, num_columns: Option<usize>,
striped: bool, striped: Option<bool>,
min_col_width: Option<f32>, min_col_width: Option<f32>,
min_row_height: Option<f32>, min_row_height: Option<f32>,
max_cell_size: Vec2, max_cell_size: Vec2,
@ -278,7 +292,7 @@ impl Grid {
Self { Self {
id_source: Id::new(id_source), id_source: Id::new(id_source),
num_columns: None, num_columns: None,
striped: false, striped: None,
min_col_width: None, min_col_width: None,
min_row_height: None, min_row_height: None,
max_cell_size: Vec2::INFINITY, max_cell_size: Vec2::INFINITY,
@ -296,9 +310,9 @@ impl Grid {
/// If `true`, add a subtle background color to every other row. /// If `true`, add a subtle background color to every other row.
/// ///
/// This can make a table easier to read. /// 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 { pub fn striped(mut self, striped: bool) -> Self {
self.striped = striped; self.striped = Some(striped);
self self
} }
@ -357,18 +371,22 @@ impl Grid {
spacing, spacing,
start_row, start_row,
} = self; } = 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_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 min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing); let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
let id = ui.make_persistent_id(id_source);
let prev_state = State::load(ui.ctx(), id);
// Each grid cell is aligned LEFT_CENTER. // Each grid cell is aligned LEFT_CENTER.
// If somebody wants to wrap more things inside a cell, // If somebody wants to wrap more things inside a cell,
// then we should pick a default layout that matches that alignment, // then we should pick a default layout that matches that alignment,
// which we do here: // which we do here:
let max_rect = ui.cursor().intersect(ui.max_rect()); let max_rect = ui.cursor().intersect(ui.max_rect());
ui.allocate_ui_at_rect(max_rect, |ui| { ui.allocate_ui_at_rect(max_rect, |ui| {
ui.set_visible(prev_state.is_some()); // Avoid visible first-frame jitter
ui.horizontal(|ui| { ui.horizontal(|ui| {
let id = ui.make_persistent_id(id_source);
let grid = GridLayout { let grid = GridLayout {
num_columns, num_columns,
striped, striped,
@ -376,7 +394,7 @@ impl Grid {
max_cell_size, max_cell_size,
spacing, spacing,
row: start_row, row: start_row,
..GridLayout::new(ui, id) ..GridLayout::new(ui, id, prev_state)
}; };
ui.set_grid(grid); ui.set_grid(grid);

117
crates/egui/src/gui_zoom.rs Normal file
View file

@ -0,0 +1,117 @@
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`].
//!
use crate::*;
/// The suggested keyboard shortcuts for global gui zooming.
pub mod kb_shortcuts {
use super::*;
pub const ZOOM_IN: KeyboardShortcut =
KeyboardShortcut::new(Modifiers::COMMAND, Key::PlusEquals);
pub const ZOOM_OUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Minus);
pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0);
}
/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing
/// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser.
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// egui::gui_zoom::zoom_with_keyboard_shortcuts(
/// ctx,
/// frame.info().native_pixels_per_point,
/// );
/// }
/// ```
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
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(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
zoom_in(ctx);
}
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
zoom_out(ctx);
}
}
}
const MIN_PIXELS_PER_POINT: f32 = 0.2;
const MAX_PIXELS_PER_POINT: f32 = 4.0;
/// Make everything larger.
pub fn zoom_in(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point += 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Make everything smaller.
pub fn zoom_out(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point -= 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Show buttons for zooming the ui.
///
/// This is meant to be called from within a menu (See [`Ui::menu_button`]).
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// ui.menu_button("View", |ui| {
/// egui::gui_zoom::zoom_menu_buttons(
/// ui,
/// frame.info().native_pixels_per_point,
/// );
/// });
/// }
/// ```
pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option<f32>) {
if ui
.add_enabled(
ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT,
Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)),
)
.clicked()
{
zoom_in(ui.ctx());
ui.close_menu();
}
if ui
.add_enabled(
ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT,
Button::new("Zoom Out")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)),
)
.clicked()
{
zoom_out(ui.ctx());
ui.close_menu();
}
if let Some(native_pixels_per_point) = native_pixels_per_point {
if ui
.add_enabled(
ui.ctx().pixels_per_point() != native_pixels_per_point,
Button::new("Reset Zoom")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)),
)
.clicked()
{
ui.ctx().set_pixels_per_point(native_pixels_per_point);
ui.close_menu();
}
}
}

View file

@ -45,16 +45,16 @@ impl Id {
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer). /// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl std::hash::Hash) -> Id { pub fn new(source: impl std::hash::Hash) -> Id {
use std::hash::Hasher; use std::hash::{BuildHasher, Hasher};
let mut hasher = epaint::ahash::AHasher::default(); let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
source.hash(&mut hasher); source.hash(&mut hasher);
Id(hasher.finish()) Id(hasher.finish())
} }
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Id { pub fn with(self, child: impl std::hash::Hash) -> Id {
use std::hash::Hasher; use std::hash::{BuildHasher, Hasher};
let mut hasher = epaint::ahash::AHasher::default(); let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0); hasher.write_u64(self.0);
child.hash(&mut hasher); child.hash(&mut hasher);
Id(hasher.finish()) Id(hasher.finish())
@ -69,6 +69,11 @@ impl Id {
pub(crate) fn value(&self) -> u64 { pub(crate) fn value(&self) -> u64 {
self.0 self.0
} }
#[cfg(feature = "accesskit")]
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
std::num::NonZeroU64::new(self.0).unwrap().into()
}
} }
impl std::fmt::Debug for Id { impl std::fmt::Debug for Id {
@ -77,6 +82,21 @@ impl std::fmt::Debug for Id {
} }
} }
/// Convenience
impl From<&'static str> for Id {
#[inline]
fn from(string: &'static str) -> Self {
Self::new(string)
}
}
impl From<String> for Id {
#[inline]
fn from(string: String) -> Self {
Self::new(string)
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Idea taken from the `nohash_hasher` crate. // Idea taken from the `nohash_hasher` crate.
@ -148,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. /// `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>; pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;

View file

@ -138,7 +138,11 @@ impl Default for InputState {
impl InputState { impl InputState {
#[must_use] #[must_use]
pub fn begin_frame(mut self, new: RawInput, requested_repaint_last_frame: bool) -> InputState { pub fn begin_frame(
mut self,
mut new: RawInput,
requested_repaint_last_frame: bool,
) -> InputState {
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
let unstable_dt = (time - self.time) as f32; let unstable_dt = (time - self.time) as f32;
@ -160,11 +164,17 @@ impl InputState {
let mut keys_down = self.keys_down; let mut keys_down = self.keys_down;
let mut scroll_delta = Vec2::ZERO; let mut scroll_delta = Vec2::ZERO;
let mut zoom_factor_delta = 1.0; let mut zoom_factor_delta = 1.0;
for event in &new.events { for event in &mut new.events {
match event { match event {
Event::Key { key, pressed, .. } => { Event::Key {
key,
pressed,
repeat,
..
} => {
if *pressed { if *pressed {
keys_down.insert(*key); let first_press = keys_down.insert(*key);
*repeat = !first_press;
} else { } else {
keys_down.remove(key); keys_down.remove(key);
} }
@ -178,6 +188,7 @@ impl InputState {
_ => {} _ => {}
} }
} }
InputState { InputState {
pointer, pointer,
touch_states: self.touch_states, touch_states: self.touch_states,
@ -245,9 +256,11 @@ impl InputState {
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty() self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
} }
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once. /// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool { ///
let mut match_found = false; /// Includes key-repeat events.
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
let mut count = 0usize;
self.events.retain(|event| { self.events.retain(|event| {
let is_match = matches!( let is_match = matches!(
@ -255,35 +268,54 @@ impl InputState {
Event::Key { Event::Key {
key: ev_key, key: ev_key,
modifiers: ev_mods, modifiers: ev_mods,
pressed: true pressed: true,
..
} if *ev_key == key && ev_mods.matches(modifiers) } if *ev_key == key && ev_mods.matches(modifiers)
); );
match_found |= is_match; count += is_match as usize;
!is_match !is_match
}); });
match_found count
}
/// 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
}
/// 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? /// Was the given key pressed this frame?
///
/// Includes key-repeat events.
pub fn key_pressed(&self, desired_key: Key) -> bool { pub fn key_pressed(&self, desired_key: Key) -> bool {
self.num_presses(desired_key) > 0 self.num_presses(desired_key) > 0
} }
/// How many times were the given key pressed this frame? /// How many times was the given key pressed this frame?
///
/// Includes key-repeat events.
pub fn num_presses(&self, desired_key: Key) -> usize { pub fn num_presses(&self, desired_key: Key) -> usize {
self.events self.events
.iter() .iter()
.filter(|event| { .filter(|event| {
matches!( matches!(
event, event,
Event::Key { Event::Key { key, pressed: true, .. }
key, if *key == desired_key
pressed: true,
..
} if *key == desired_key
) )
}) })
.count() .count()
@ -336,7 +368,7 @@ impl InputState {
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// let mut zoom = 1.0; // no zoom /// let mut zoom = 1.0; // no zoom
/// let mut rotation = 0.0; // no rotation /// 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 { /// if let Some(multi_touch) = multi_touch {
/// zoom *= multi_touch.zoom_delta; /// zoom *= multi_touch.zoom_delta;
/// rotation += multi_touch.rotation_delta; /// rotation += multi_touch.rotation_delta;
@ -380,6 +412,33 @@ impl InputState {
} }
} }
} }
#[cfg(feature = "accesskit")]
pub fn accesskit_action_requests(
&self,
id: crate::Id,
action: accesskit::Action,
) -> impl Iterator<Item = &accesskit::ActionRequest> {
let accesskit_id = id.accesskit_id();
self.events.iter().filter_map(move |event| {
if let Event::AccessKitActionRequest(request) = event {
if request.target == accesskit_id && request.action == action {
return Some(request);
}
}
None
})
}
#[cfg(feature = "accesskit")]
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
self.accesskit_action_requests(id, action).next().is_some()
}
#[cfg(feature = "accesskit")]
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
self.accesskit_action_requests(id, action).count()
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -388,9 +447,10 @@ impl InputState {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub(crate) struct Click { pub(crate) struct Click {
pub pos: Pos2, pub pos: Pos2,
pub button: PointerButton,
/// 1 or 2 (double-click) or 3 (triple-click) /// 1 or 2 (double-click) or 3 (triple-click)
pub count: u32, pub count: u32,
/// Allows you to check for e.g. shift-click /// Allows you to check for e.g. shift-click
pub modifiers: Modifiers, pub modifiers: Modifiers,
} }
@ -412,7 +472,10 @@ pub(crate) enum PointerEvent {
position: Pos2, position: Pos2,
button: PointerButton, button: PointerButton,
}, },
Released(Option<Click>), Released {
click: Option<Click>,
button: PointerButton,
},
} }
impl PointerEvent { impl PointerEvent {
@ -421,11 +484,11 @@ impl PointerEvent {
} }
pub fn is_release(&self) -> bool { pub fn is_release(&self) -> bool {
matches!(self, PointerEvent::Released(_)) matches!(self, PointerEvent::Released { .. })
} }
pub fn is_click(&self) -> bool { pub fn is_click(&self) -> bool {
matches!(self, PointerEvent::Released(Some(_click))) matches!(self, PointerEvent::Released { click: Some(_), .. })
} }
} }
@ -580,7 +643,6 @@ impl PointerState {
Some(Click { Some(Click {
pos, pos,
button,
count, count,
modifiers, modifiers,
}) })
@ -588,7 +650,8 @@ impl PointerState {
None None
}; };
self.pointer_events.push(PointerEvent::Released(click)); self.pointer_events
.push(PointerEvent::Released { click, button });
self.press_origin = None; self.press_origin = None;
self.press_start_time = None; self.press_start_time = None;
@ -716,11 +779,28 @@ impl PointerState {
self.pointer_events.iter().any(|event| event.is_release()) 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? /// Was the button given released this frame?
pub fn button_released(&self, button: PointerButton) -> bool { pub fn button_released(&self, button: PointerButton) -> bool {
self.pointer_events self.pointer_events
.iter() .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? /// Was the primary button released this frame?
@ -752,16 +832,28 @@ impl PointerState {
/// Was the button given double clicked this frame? /// Was the button given double clicked this frame?
pub fn button_double_clicked(&self, button: PointerButton) -> bool { pub fn button_double_clicked(&self, button: PointerButton) -> bool {
self.pointer_events self.pointer_events.iter().any(|event| {
.iter() matches!(
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double())) &event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_double()
)
})
} }
/// Was the button given triple clicked this frame? /// Was the button given triple clicked this frame?
pub fn button_triple_clicked(&self, button: PointerButton) -> bool { pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
self.pointer_events self.pointer_events.iter().any(|event| {
.iter() matches!(
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple())) &event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_triple()
)
})
} }
/// Was the primary button clicked this frame? /// Was the primary button clicked this frame?
@ -774,18 +866,6 @@ impl PointerState {
self.button_clicked(PointerButton::Secondary) 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? /// Is this button currently down?
#[inline(always)] #[inline(always)]
pub fn button_down(&self, button: PointerButton) -> bool { pub fn button_down(&self, button: PointerButton) -> bool {

View file

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

View file

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

View file

@ -39,6 +39,7 @@ impl Order {
Self::Tooltip, Self::Tooltip,
Self::Debug, Self::Debug,
]; ];
pub const TOP: Self = Self::Debug;
#[inline(always)] #[inline(always)]
pub fn allow_interaction(&self) -> bool { pub fn allow_interaction(&self) -> bool {
@ -109,7 +110,7 @@ impl LayerId {
} }
/// A unique identifier of a specific [`Shape`] in a [`PaintList`]. /// A unique identifier of a specific [`Shape`] in a [`PaintList`].
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct ShapeIdx(usize); pub struct ShapeIdx(usize);
/// A list of [`Shape`]s paired with a clip rectangle. /// A list of [`Shape`]s paired with a clip rectangle.
@ -130,9 +131,12 @@ impl PaintList {
idx idx
} }
pub fn extend(&mut self, clip_rect: Rect, mut shapes: Vec<Shape>) { pub fn extend<I: IntoIterator<Item = Shape>>(&mut self, clip_rect: Rect, shapes: I) {
self.0 self.0.extend(
.extend(shapes.drain(..).map(|shape| ClippedShape(clip_rect, shape))); shapes
.into_iter()
.map(|shape| ClippedShape(clip_rect, shape)),
);
} }
/// Modify an existing [`Shape`]. /// Modify an existing [`Shape`].

View file

@ -75,7 +75,7 @@ impl Region {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp). /// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction { pub enum Direction {
LeftToRight, LeftToRight,
@ -114,32 +114,32 @@ impl Direction {
/// }); /// });
/// # }); /// # });
/// ``` /// ```
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Layout { pub struct Layout {
/// Main axis direction /// Main axis direction
main_dir: Direction, pub main_dir: Direction,
/// If true, wrap around when reading the end of the main direction. /// If true, wrap around when reading the end of the main direction.
/// For instance, for `main_dir == Direction::LeftToRight` this will /// For instance, for `main_dir == Direction::LeftToRight` this will
/// wrap to a new row when we reach the right side of the `max_rect`. /// wrap to a new row when we reach the right side of the `max_rect`.
main_wrap: bool, pub main_wrap: bool,
/// How to align things on the main axis. /// How to align things on the main axis.
main_align: Align, pub main_align: Align,
/// Justify the main axis? /// Justify the main axis?
main_justify: bool, pub main_justify: bool,
/// How to align things on the cross axis. /// How to align things on the cross axis.
/// For vertical layouts: put things to left, center or right? /// For vertical layouts: put things to left, center or right?
/// For horizontal layouts: put things to top, center or bottom? /// For horizontal layouts: put things to top, center or bottom?
cross_align: Align, pub cross_align: Align,
/// Justify the cross axis? /// Justify the cross axis?
/// For vertical layouts justify mean all widgets get maximum width. /// For vertical layouts justify mean all widgets get maximum width.
/// For horizontal layouts justify mean all widgets get maximum height. /// For horizontal layouts justify mean all widgets get maximum height.
cross_justify: bool, pub cross_justify: bool,
} }
impl Default for Layout { impl Default for Layout {
@ -183,7 +183,7 @@ impl Layout {
/// Place elements vertically, top to bottom. /// Place elements vertically, top to bottom.
/// ///
/// Use the provided horizontal alignmen. /// Use the provided horizontal alignment.
#[inline(always)] #[inline(always)]
pub fn top_down(halign: Align) -> Self { pub fn top_down(halign: Align) -> Self {
Self { Self {
@ -196,7 +196,7 @@ impl Layout {
} }
} }
/// Top-down layout justifed so that buttons etc fill the full available width. /// Top-down layout justified so that buttons etc fill the full available width.
#[inline(always)] #[inline(always)]
pub fn top_down_justified(halign: Align) -> Self { pub fn top_down_justified(halign: Align) -> Self {
Self::top_down(halign).with_cross_justify(true) Self::top_down(halign).with_cross_justify(true)
@ -204,7 +204,7 @@ impl Layout {
/// Place elements vertically, bottom up. /// Place elements vertically, bottom up.
/// ///
/// Use the provided horizontal alignmen. /// Use the provided horizontal alignment.
#[inline(always)] #[inline(always)]
pub fn bottom_up(halign: Align) -> Self { pub fn bottom_up(halign: Align) -> Self {
Self { Self {
@ -231,6 +231,8 @@ impl Layout {
/// For when you want to add a single widget to a layout, and that widget /// For when you want to add a single widget to a layout, and that widget
/// should use up all available space. /// should use up all available space.
///
/// Only one widget may be added to the inner `Ui`!
#[inline(always)] #[inline(always)]
pub fn centered_and_justified(main_dir: Direction) -> Self { pub fn centered_and_justified(main_dir: Direction) -> Self {
Self { Self {

View file

@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>. //! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//! //!
//! `egui` is in heavy development, with each new version having breaking changes. //! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.61.0 or later to use `egui`. //! You need to have rust 1.62.0 or later to use `egui`.
//! //!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe). //! which uses [`eframe`](https://docs.rs/eframe).
@ -177,7 +177,7 @@
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames. //! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
//! //!
//! It can be useful to read the code for the toggle switch example widget to get a better understanding //! It can be useful to read the code for the toggle switch example widget to get a better understanding
//! of how egui works: <https://github.com/emilk/egui/blob/master/egui_demo_lib/src/demo/toggle_switch.rs>. //! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//! //!
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>. //! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
//! //!
@ -305,6 +305,7 @@ mod context;
mod data; mod data;
mod frame_state; mod frame_state;
pub(crate) mod grid; pub(crate) mod grid;
pub mod gui_zoom;
mod id; mod id;
mod input_state; mod input_state;
pub mod introspection; pub mod introspection;
@ -312,6 +313,7 @@ pub mod layers;
mod layout; mod layout;
mod memory; mod memory;
pub mod menu; pub mod menu;
pub mod os;
mod painter; mod painter;
pub(crate) mod placer; pub(crate) mod placer;
mod response; mod response;
@ -322,18 +324,23 @@ pub mod util;
pub mod widget_text; pub mod widget_text;
pub mod widgets; pub mod widgets;
#[cfg(feature = "accesskit")]
pub use accesskit;
pub use epaint; pub use epaint;
pub use epaint::ecolor;
pub use epaint::emath; pub use epaint::emath;
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
#[cfg(feature = "color-hex")] #[cfg(feature = "color-hex")]
pub use epaint::hex_color; pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
pub use epaint::{ pub use epaint::{
color, mutex, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TexturesDelta}, textures::{TextureFilter, TextureOptions, TexturesDelta},
ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback, ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo,
PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId, Rounding, Shape, Stroke, TextureHandle, TextureId,
}; };
pub mod text { pub mod text {
@ -356,11 +363,11 @@ pub use {
input_state::{InputState, MultiTouchInfo, PointerState}, input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order}, layers::{LayerId, Order},
layout::*, layout::*,
memory::Memory, memory::{Memory, Options},
painter::Painter, painter::Painter,
response::{InnerResponse, Response}, response::{InnerResponse, Response},
sense::Sense, sense::Sense,
style::{FontSelection, Style, TextStyle, Visuals}, style::{FontSelection, Margin, Style, TextStyle, Visuals},
text::{Galley, TextFormat}, text::{Galley, TextFormat},
ui::Ui, ui::Ui,
widget_text::{RichText, WidgetText}, widget_text::{RichText, WidgetText},
@ -373,7 +380,7 @@ pub use {
pub fn warn_if_debug_build(ui: &mut crate::Ui) { pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
ui.label( ui.label(
RichText::new("‼ Debug build ‼") RichText::new("⚠ Debug build ⚠")
.small() .small()
.color(ui.visuals().warn_fg_color), .color(ui.visuals().warn_fg_color),
) )
@ -502,22 +509,34 @@ pub mod special_emojis {
} }
/// The different types of built-in widgets in egui /// The different types of built-in widgets in egui
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WidgetType { pub enum WidgetType {
Label, // TODO(emilk): emit Label events Label, // TODO(emilk): emit Label events
/// e.g. a hyperlink /// e.g. a hyperlink
Link, Link,
TextEdit, TextEdit,
Button, Button,
Checkbox, Checkbox,
RadioButton, RadioButton,
SelectableLabel, SelectableLabel,
ComboBox, ComboBox,
Slider, Slider,
DragValue, DragValue,
ColorButton, ColorButton,
ImageButton, ImageButton,
CollapsingHeader, CollapsingHeader,
/// If you cannot fit any of the above slots. /// If you cannot fit any of the above slots.
@ -547,3 +566,8 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
}); });
}); });
} }
#[cfg(feature = "accesskit")]
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}

View file

@ -52,9 +52,10 @@ pub struct Memory {
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>; /// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
/// ///
/// # let mut ctx = egui::Context::default(); /// # let mut ctx = egui::Context::default();
/// let mut memory = ctx.memory(); /// ctx.memory_mut(|mem| {
/// let cache = memory.caches.cache::<CharCountCache<'_>>(); /// let cache = mem.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5); /// assert_eq!(cache.get("hello"), 5);
/// });
/// ``` /// ```
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage, pub caches: crate::util::cache::CacheStorage,
@ -102,9 +103,15 @@ pub struct Options {
/// Controls the tessellator. /// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions, pub tessellation_options: epaint::TessellationOptions,
/// This does not at all change the behavior of egui, /// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
/// but 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. /// 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, pub screen_reader: bool,
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas. /// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
@ -166,7 +173,7 @@ pub(crate) struct Interaction {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub(crate) struct Focus { pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field). /// The widget with keyboard focus (i.e. a text input field).
id: Option<Id>, pub(crate) id: Option<Id>,
/// What had keyboard focus previous frame? /// What had keyboard focus previous frame?
id_previous_frame: Option<Id>, id_previous_frame: Option<Id>,
@ -174,6 +181,9 @@ pub(crate) struct Focus {
/// Give focus to this widget next frame /// Give focus to this widget next frame
id_next_frame: Option<Id>, id_next_frame: Option<Id>,
#[cfg(feature = "accesskit")]
id_requested_by_accesskit: Option<accesskit::NodeId>,
/// If set, the next widget that is interested in focus will automatically get it. /// If set, the next widget that is interested in focus will automatically get it.
/// Probably because the user pressed Tab. /// Probably because the user pressed Tab.
give_to_next: bool, give_to_next: bool,
@ -231,6 +241,11 @@ impl Focus {
self.id = Some(id); self.id = Some(id);
} }
#[cfg(feature = "accesskit")]
{
self.id_requested_by_accesskit = None;
}
self.pressed_tab = false; self.pressed_tab = false;
self.pressed_shift_tab = false; self.pressed_shift_tab = false;
for event in &new_input.events { for event in &new_input.events {
@ -240,6 +255,7 @@ impl Focus {
key: crate::Key::Escape, key: crate::Key::Escape,
pressed: true, pressed: true,
modifiers: _, modifiers: _,
..
} }
) { ) {
self.id = None; self.id = None;
@ -251,6 +267,7 @@ impl Focus {
key: crate::Key::Tab, key: crate::Key::Tab,
pressed: true, pressed: true,
modifiers, modifiers,
..
} = event } = event
{ {
if !self.is_focus_locked { if !self.is_focus_locked {
@ -261,6 +278,18 @@ impl Focus {
} }
} }
} }
#[cfg(feature = "accesskit")]
{
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus,
target,
data: None,
}) = event
{
self.id_requested_by_accesskit = Some(*target);
}
}
} }
} }
@ -281,6 +310,17 @@ impl Focus {
} }
fn interested_in_focus(&mut self, id: Id) { fn interested_in_focus(&mut self, id: Id) {
#[cfg(feature = "accesskit")]
{
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
self.id = Some(id);
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.pressed_tab = false;
self.pressed_shift_tab = false;
}
}
if self.give_to_next && !self.had_focus_last_frame(id) { if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id); self.id = Some(id);
self.give_to_next = false; self.give_to_next = false;
@ -293,7 +333,7 @@ impl Focus {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.pressed_shift_tab = false; self.pressed_shift_tab = false;
} }
} else if self.pressed_tab && self.id == None && !self.give_to_next { } else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it: // nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
self.id = Some(id); self.id = Some(id);
self.pressed_tab = false; self.pressed_tab = false;
@ -372,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. /// 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) { if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked self.interaction.focus.is_focus_locked
} else { } else {
@ -400,8 +440,13 @@ impl Memory {
/// Register this widget as being interested in getting keyboard focus. /// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab. /// This will allow the user to select it with tab and shift-tab.
/// This is normally done automatically when handling interactions,
/// but it is sometimes useful to pre-register interest in focus,
/// e.g. before deciding which type of underlying widget to use,
/// as in the [`crate::DragValue`] widget, so a widget can be focused
/// and rendered correctly in a single frame.
#[inline(always)] #[inline(always)]
pub(crate) fn interested_in_focus(&mut self, id: Id) { pub fn interested_in_focus(&mut self, id: Id) {
self.interaction.focus.interested_in_focus(id); self.interaction.focus.interested_in_focus(id);
} }
@ -441,6 +486,10 @@ impl Memory {
self.popup == Some(popup_id) || self.everything_is_visible() 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) { pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some(popup_id); self.popup = Some(popup_id);
} }
@ -551,8 +600,8 @@ impl Areas {
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> { pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame self.visible_last_frame
.iter() .iter()
.cloned() .copied()
.chain(self.visible_current_frame.iter().cloned()) .chain(self.visible_current_frame.iter().copied())
.collect() .collect()
} }

View file

@ -31,11 +31,11 @@ pub(crate) struct BarState {
impl BarState { impl BarState {
fn load(ctx: &Context, bar_id: Id) -> Self { 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) { 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. /// Show a menu at pointer if primary-clicked response.
@ -66,10 +66,10 @@ impl std::ops::DerefMut for BarState {
fn set_menu_style(style: &mut Style) { fn set_menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0); style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::none(); style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.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(); style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
} }
/// The menu bar goes well in a [`TopBottomPanel::top`], /// The menu bar goes well in a [`TopBottomPanel::top`],
@ -100,6 +100,20 @@ pub fn menu_button<R>(
stationary_menu_impl(ui, title, Box::new(add_contents)) 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. /// Construct a nested sub menu in another menu.
/// ///
/// Opens on hover. /// Opens on hover.
@ -117,7 +131,7 @@ pub(crate) fn submenu_button<R>(
/// wrapper for the contents of every menu. /// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>( pub(crate) fn menu_ui<'c, R>(
ctx: &Context, ctx: &Context,
menu_id: impl std::hash::Hash, menu_id: impl Into<Id>,
menu_state_arc: &Arc<RwLock<MenuState>>, menu_state_arc: &Arc<RwLock<MenuState>>,
add_contents: impl FnOnce(&mut Ui) -> R + 'c, add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
@ -129,9 +143,10 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id) let area = Area::new(menu_id)
.order(Order::Foreground) .order(Order::Foreground)
.constrain(true)
.fixed_pos(pos) .fixed_pos(pos)
.interactable(true) .interactable(true)
.drag_bounds(Rect::EVERYTHING); .drag_bounds(ctx.screen_rect());
let inner_response = area.show(ctx, |ui| { let inner_response = area.show(ctx, |ui| {
set_menu_style(ui.style_mut()); set_menu_style(ui.style_mut());
@ -166,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
let mut button = Button::new(title); let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) { 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); 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) 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. /// Response to secondary clicks (right-clicks) by showing the given menu.
pub(crate) fn context_menu( pub(crate) fn context_menu(
response: &Response, response: &Response,
@ -277,10 +311,9 @@ impl MenuRoot {
root: &mut MenuRootManager, root: &mut MenuRootManager,
id: Id, id: Id,
) -> MenuResponse { ) -> MenuResponse {
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380). if (response.clicked() && root.is_menu_open(id))
let input = response.ctx.input(); || response.ctx.input(|i| i.key_pressed(Key::Escape))
{
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
// menu open and button clicked or esc pressed // menu open and button clicked or esc pressed
return MenuResponse::Close; return MenuResponse::Close;
} else if (response.clicked() && !root.is_menu_open(id)) } else if (response.clicked() && !root.is_menu_open(id))
@ -288,10 +321,26 @@ impl MenuRoot {
{ {
// menu not open and button clicked // menu not open and button clicked
// or button hovered while other menu is open // or button hovered while other menu is open
let pos = response.rect.left_bottom(); 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(|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();
}
if pos.x + menu_rect.width() > screen_rect.max.x {
pos.x = screen_rect.max.x - menu_rect.width();
}
}
return MenuResponse::Create(pos, id); return MenuResponse::Create(pos, id);
} else if input.pointer.any_pressed() && input.pointer.primary_down() { } else if response
if let Some(pos) = input.pointer.interact_pos() { .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 let Some(root) = root.inner.as_mut() {
if root.id == id { if root.id == id {
// pressed somewhere while this menu is open // pressed somewhere while this menu is open
@ -314,7 +363,8 @@ impl MenuRoot {
id: Id, id: Id,
) -> MenuResponse { ) -> MenuResponse {
let response = response.interact(Sense::click()); 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 pointer.any_pressed() {
if let Some(pos) = pointer.interact_pos() { if let Some(pos) = pointer.interact_pos() {
let mut destroy = false; let mut destroy = false;
@ -334,6 +384,7 @@ impl MenuRoot {
} }
} }
MenuResponse::Stay MenuResponse::Stay
})
} }
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) { fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
@ -395,7 +446,7 @@ impl SubMenuButton {
sub_id: Id, sub_id: Id,
) -> &'a WidgetVisuals { ) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) { if menu_state.is_open(sub_id) {
&ui.style().visuals.widgets.hovered &ui.style().visuals.widgets.open
} else { } else {
ui.style().interact(response) ui.style().interact(response)
} }
@ -424,11 +475,12 @@ impl SubMenuButton {
text_galley.size().x + icon_galley.size().x, text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y), 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); let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| { response.widget_info(|| {
crate::WidgetInfo::labeled(crate::WidgetType::Button, &text_galley.text()) crate::WidgetInfo::labeled(crate::WidgetType::Button, text_galley.text())
}); });
if ui.is_rect_visible(rect) { if ui.is_rect_visible(rect) {
@ -444,7 +496,7 @@ impl SubMenuButton {
ui.painter().rect_filled( ui.painter().rect_filled(
rect.expand(visuals.expansion), rect.expand(visuals.expansion),
visuals.rounding, visuals.rounding,
visuals.bg_fill, visuals.weak_bg_fill,
); );
} }
@ -476,7 +528,7 @@ impl SubMenu {
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
let sub_id = ui.id().with(self.button.index); let sub_id = ui.id().with(self.button.index);
let button = self.button.show(ui, &*self.parent_state.read(), sub_id); let button = self.button.show(ui, &self.parent_state.read(), sub_id);
self.parent_state self.parent_state
.write() .write()
.submenu_button_interaction(ui, sub_id, &button); .submenu_button_interaction(ui, sub_id, &button);
@ -556,15 +608,15 @@ impl MenuState {
/// Sense button interaction opening and closing submenu. /// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) { 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); 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 // ensure to repaint once even when pointer is not moving
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} else if !open && button.hovered() { } else if !open && button.hovered() {
let pos = button.rect.right_top(); let pos = button.rect.right_top();
self.open_submenu(sub_id, pos); 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(); self.close_submenu();
} }
} }

76
crates/egui/src/os.rs Normal file
View file

@ -0,0 +1,76 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OperatingSystem {
/// Unknown OS - could be wasm
Unknown,
/// Android OS.
Android,
/// Apple iPhone OS.
IOS,
/// Linux or Unix other than Android.
Nix,
/// MacOS.
Mac,
/// Windows.
Windows,
}
impl Default for OperatingSystem {
fn default() -> Self {
Self::from_target_os()
}
}
impl OperatingSystem {
pub const fn from_target_os() -> Self {
if cfg!(target_arch = "wasm32") {
Self::Unknown
} else if cfg!(target_os = "android") {
Self::Android
} else if cfg!(target_os = "ios") {
Self::IOS
} else if cfg!(target_os = "macos") {
Self::Mac
} else if cfg!(target_os = "windows") {
Self::Android
} else if cfg!(target_os = "linux")
|| cfg!(target_os = "dragonfly")
|| cfg!(target_os = "freebsd")
|| cfg!(target_os = "netbsd")
|| cfg!(target_os = "openbsd")
{
Self::Nix
} else {
Self::Unknown
}
}
/// Helper: try to guess from the user-agent of a browser.
pub fn from_user_agent(user_agent: &str) -> Self {
if user_agent.contains("Android") {
Self::Android
} else if user_agent.contains("like Mac") {
Self::IOS
} else if user_agent.contains("Win") {
Self::Windows
} else if user_agent.contains("Mac") {
Self::Mac
} else if user_agent.contains("Linux")
|| user_agent.contains("X11")
|| user_agent.contains("Unix")
{
Self::Nix
} else {
#[cfg(feature = "tracing")]
tracing::warn!(
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
user_agent);
Self::Unknown
}
}
}

View file

@ -7,7 +7,6 @@ use crate::{
Color32, Context, FontId, Color32, Context, FontId,
}; };
use epaint::{ use epaint::{
mutex::{RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley}, text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke, CircleShape, RectShape, Rounding, Shape, Stroke,
}; };
@ -105,10 +104,12 @@ impl Painter {
&self.ctx &self.ctx
} }
/// Available fonts. /// Read-only access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
#[inline(always)] #[inline(always)]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> { pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx.fonts() self.ctx.fonts(reader)
} }
/// Where we paint /// Where we paint
@ -152,8 +153,9 @@ impl Painter {
/// ## Low level /// ## Low level
impl Painter { impl Painter {
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> { #[inline]
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id)) 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) { fn transform_shape(&self, shape: &mut Shape) {
@ -167,30 +169,30 @@ impl Painter {
/// NOTE: all coordinates are screen coordinates! /// NOTE: all coordinates are screen coordinates!
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx { pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) { 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 { } else {
let mut shape = shape.into(); let mut shape = shape.into();
self.transform_shape(&mut shape); self.transform_shape(&mut shape);
self.paint_list().add(self.clip_rect, shape) self.paint_list(|l| l.add(self.clip_rect, shape))
} }
} }
/// Add many shapes at once. /// Add many shapes at once.
/// ///
/// Calling this once is generally faster than calling [`Self::add`] multiple times. /// Calling this once is generally faster than calling [`Self::add`] multiple times.
pub fn extend(&self, mut shapes: Vec<Shape>) { pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
if self.fade_to_color == Some(Color32::TRANSPARENT) { if self.fade_to_color == Some(Color32::TRANSPARENT) {
return; return;
} }
if !shapes.is_empty() {
if self.fade_to_color.is_some() { if self.fade_to_color.is_some() {
for shape in &mut shapes { let shapes = shapes.into_iter().map(|mut shape| {
self.transform_shape(shape); self.transform_shape(&mut shape);
} shape
} });
self.paint_list(|l| l.extend(self.clip_rect, shapes));
self.paint_list().extend(self.clip_rect, shapes); } else {
} self.paint_list(|l| l.extend(self.clip_rect, shapes));
};
} }
/// Modify an existing [`Shape`]. /// Modify an existing [`Shape`].
@ -200,7 +202,7 @@ impl Painter {
} }
let mut shape = shape.into(); let mut shape = shape.into();
self.transform_shape(&mut shape); 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));
} }
} }
@ -208,12 +210,17 @@ impl Painter {
impl Painter { impl Painter {
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) { pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
self.rect_stroke(rect, 0.0, (1.0, color)); self.rect(
rect,
0.0,
color.additive().linear_multiply(0.015),
(1.0, color),
);
self.text( self.text(
rect.min, rect.min,
Align2::LEFT_TOP, Align2::LEFT_TOP,
text.to_string(), text.to_string(),
FontId::monospace(14.0), FontId::monospace(12.0),
color, color,
); );
} }
@ -232,13 +239,13 @@ impl Painter {
color: Color32, color: Color32,
text: impl ToString, text: impl ToString,
) -> Rect { ) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(14.0), color); let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
let frame_rect = rect.expand(2.0); let frame_rect = rect.expand(2.0);
self.add(Shape::rect_filled( self.add(Shape::rect_filled(
frame_rect, frame_rect,
0.0, 0.0,
Color32::from_black_alpha(240), Color32::from_black_alpha(150),
)); ));
self.galley(rect.min, galley); self.galley(rect.min, galley);
frame_rect frame_rect
@ -352,6 +359,16 @@ impl Painter {
self.line_segment([tip, tip - tip_length * (rot * dir)], stroke); self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke); self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
} }
/// An image at the given position.
///
/// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
/// unless you want to crop or flip the image.
///
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
self.add(Shape::image(texture_id, rect, uv, tint));
}
} }
/// ## Text /// ## Text
@ -390,7 +407,7 @@ impl Painter {
color: crate::Color32, color: crate::Color32,
wrap_width: f32, wrap_width: f32,
) -> Arc<Galley> { ) -> 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`. /// Will line break at `\n`.
@ -403,7 +420,7 @@ impl Painter {
font_id: FontId, font_id: FontId,
color: crate::Color32, color: crate::Color32,
) -> Arc<Galley> { ) -> 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`]. /// Paint text that has already been layed out in a [`Galley`].
@ -433,6 +450,6 @@ impl Painter {
fn tint_shape_towards(shape: &mut Shape, target: Color32) { fn tint_shape_towards(shape: &mut Shape, target: Color32) {
epaint::shape_transform::adjust_colors(shape, &|color| { epaint::shape_transform::adjust_colors(shape, &|color| {
*color = crate::color::tint_color_towards(*color, target); *color = crate::ecolor::tint_color_towards(*color, target);
}); });
} }

View file

@ -13,6 +13,7 @@ use crate::{
/// ///
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned. /// 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. /// [`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)] #[derive(Clone)]
pub struct Response { pub struct Response {
// CONTEXT: // CONTEXT:
@ -42,6 +43,10 @@ pub struct Response {
#[doc(hidden)] #[doc(hidden)]
pub hovered: bool, 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. /// The pointer clicked this thing this frame.
#[doc(hidden)] #[doc(hidden)]
pub clicked: [bool; NUM_POINTER_BUTTONS], pub clicked: [bool; NUM_POINTER_BUTTONS],
@ -52,7 +57,7 @@ pub struct Response {
pub double_clicked: [bool; NUM_POINTER_BUTTONS], pub double_clicked: [bool; NUM_POINTER_BUTTONS],
/// The thing was triple-clicked. /// 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 /// The widgets is being dragged
#[doc(hidden)] #[doc(hidden)]
@ -90,6 +95,7 @@ impl std::fmt::Debug for Response {
sense, sense,
enabled, enabled,
hovered, hovered,
highlighted,
clicked, clicked,
double_clicked, double_clicked,
triple_clicked, triple_clicked,
@ -106,6 +112,7 @@ impl std::fmt::Debug for Response {
.field("sense", sense) .field("sense", sense)
.field("enabled", enabled) .field("enabled", enabled)
.field("hovered", hovered) .field("hovered", hovered)
.field("highlighted", highlighted)
.field("clicked", clicked) .field("clicked", clicked)
.field("double_clicked", double_clicked) .field("double_clicked", double_clicked)
.field("triple_clicked", triple_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, // 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). // even if we aren't clickable (or even enabled).
// This is important for windows and such that should close then the user clicks elsewhere. // 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() { if pointer.any_click() {
// We detect clicks/hover on a "interact_rect" that is slightly larger than // We detect clicks/hover on a "interact_rect" that is slightly larger than
@ -190,6 +198,7 @@ impl Response {
} else { } else {
false false
} }
})
} }
/// Was the widget enabled? /// Was the widget enabled?
@ -211,20 +220,24 @@ impl Response {
self.hovered 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 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) /// This function only returns true if the UI as a whole (e.g. window)
/// also has the keyboard focus. That makes this function suitable /// also has the keyboard focus. That makes this function suitable
/// for style choices, e.g. a thicker border around focused widgets. /// for style choices, e.g. a thicker border around focused widgets.
pub fn has_focus(&self) -> bool { pub fn has_focus(&self) -> bool {
// Access input and memory in separate statements to prevent deadlock. self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
let has_global_focus = self.ctx.input().raw.has_focus;
has_global_focus && self.ctx.memory().has_focus(self.id)
} }
/// True if this widget has keyboard focus this frame, but didn't last frame. /// True if this widget has keyboard focus this frame, but didn't last frame.
pub fn gained_focus(&self) -> bool { 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, /// The widget had keyboard focus and lost it,
@ -236,29 +249,29 @@ impl Response {
/// # let mut my_text = String::new(); /// # let mut my_text = String::new();
/// # fn do_request(_: &str) {} /// # fn do_request(_: &str) {}
/// let response = ui.text_edit_singleline(&mut my_text); /// 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); /// do_request(&my_text);
/// } /// }
/// # }); /// # });
/// ``` /// ```
pub fn lost_focus(&self) -> bool { 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. /// Request that this widget get keyboard focus.
pub fn request_focus(&self) { 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. /// Surrender keyboard focus for this widget.
pub fn surrender_focus(&self) { 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. /// The widgets is being dragged.
/// ///
/// To find out which button(s), query [`crate::PointerState::button_down`] /// 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`]. /// 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`]). /// [`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 { 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? /// Did a drag on this widgets begin this frame?
pub fn drag_started(&self) -> bool { 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. /// The widget was being dragged, but now it has been released.
@ -283,10 +301,15 @@ impl Response {
self.drag_released 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? /// If dragged, how many points were we dragged and in what direction?
pub fn drag_delta(&self) -> Vec2 { pub fn drag_delta(&self) -> Vec2 {
if self.dragged() { if self.dragged() {
self.ctx.input().pointer.delta() self.ctx.input(|i| i.pointer.delta())
} else { } else {
Vec2::ZERO Vec2::ZERO
} }
@ -302,7 +325,7 @@ impl Response {
/// None if the pointer is outside the response area. /// None if the pointer is outside the response area.
pub fn hover_pos(&self) -> Option<Pos2> { pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() { if self.hovered() {
self.ctx.input().pointer.hover_pos() self.ctx.input(|i| i.pointer.hover_pos())
} else { } else {
None None
} }
@ -386,27 +409,36 @@ impl Response {
self self
} }
/// Was the tooltip open last frame?
pub fn is_tooltip_open(&self) -> bool {
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id.with("__tooltip"))
}
fn should_show_hover_ui(&self) -> bool { 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; return true;
} }
if !self.hovered || !self.ctx.input().pointer.has_pointer() { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false; return false;
} }
if self.ctx.style().interaction.show_tooltips_only_when_still if self.ctx.style().interaction.show_tooltips_only_when_still {
&& !self.ctx.input().pointer.is_still() // We only show the tooltip when the mouse pointer is still,
{ // but once shown we keep showing it until the mouse leaves the parent.
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
// wait for mouse to stop // wait for mouse to stop
self.ctx.request_repaint(); self.ctx.request_repaint();
return false; return false;
} }
}
// We don't want tooltips of things while we are dragging them, // 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. // but we do want tooltips while holding down on an item on a touch screen.
if self.ctx.input().pointer.any_down() if self
&& self.ctx.input().pointer.has_moved_too_much_for_a_click .ctx
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
{ {
return false; return false;
} }
@ -435,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. /// Show this text when hovering if the widget is disabled.
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self { pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_disabled_hover_ui(|ui| { self.on_disabled_hover_ui(|ui| {
@ -445,7 +488,15 @@ impl Response {
/// When hovered, use this icon for the mouse cursor. /// When hovered, use this icon for the mouse cursor.
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self { pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() { 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 self
} }
@ -494,8 +545,10 @@ impl Response {
/// # }); /// # });
/// ``` /// ```
pub fn scroll_to_me(&self, align: Option<Align>) { 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_mut(|state| {
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align)); state.scroll_target[0] = Some((self.rect.x_range(), align));
state.scroll_target[1] = Some((self.rect.y_range(), align));
});
} }
/// For accessibility. /// For accessibility.
@ -517,10 +570,109 @@ impl Response {
None None
}; };
if let Some(event) = event { if let Some(event) = event {
self.ctx.output().events.push(event); self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
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")]
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, 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 {
builder.add_action(accesskit::Action::Focus);
}
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,
builder: &mut accesskit::NodeBuilder,
info: crate::WidgetInfo,
) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
self.fill_accesskit_node_common(builder);
builder.set_role(match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField,
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
Role::Button
}
WidgetType::Checkbox => Role::CheckBox,
WidgetType::RadioButton => Role::RadioButton,
WidgetType::SelectableLabel => Role::ToggleButton,
WidgetType::ComboBox => Role::PopupButton,
WidgetType::Slider => Role::Slider,
WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown,
});
if let Some(label) = info.label {
builder.set_name(label);
}
if let Some(value) = info.current_text_value {
builder.set_value(value);
}
if let Some(value) = info.value {
builder.set_numeric_value(value);
}
if let Some(selected) = info.selected {
builder.set_checked_state(if selected {
CheckedState::True
} else {
CheckedState::False
});
}
}
/// Associate a label with a control for accessibility.
///
/// # Example
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut text = "Arthur".to_string();
/// ui.horizontal(|ui| {
/// let label = ui.label("Your name: ");
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
/// });
/// # });
/// ```
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| {
builder.push_labelled_by(id.accesskit_id());
});
#[cfg(not(feature = "accesskit"))]
{
let _ = id;
}
self
}
/// Response to secondary clicks (right-clicks) by showing the given menu. /// Response to secondary clicks (right-clicks) by showing the given menu.
/// ///
/// ``` /// ```
@ -561,6 +713,7 @@ impl Response {
sense: self.sense.union(other.sense), sense: self.sense.union(other.sense),
enabled: self.enabled || other.enabled, enabled: self.enabled || other.enabled,
hovered: self.hovered || other.hovered, hovered: self.hovered || other.hovered,
highlighted: self.highlighted || other.highlighted,
clicked: [ clicked: [
self.clicked[0] || other.clicked[0], self.clicked[0] || other.clicked[0],
self.clicked[1] || other.clicked[1], self.clicked[1] || other.clicked[1],
@ -592,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: /// To summarize the response from many widgets you can use this pattern:
/// ///
/// ``` /// ```

View file

@ -16,6 +16,7 @@ pub struct Sense {
impl Sense { impl Sense {
/// Senses no clicks or drags. Only senses mouse hover. /// Senses no clicks or drags. Only senses mouse hover.
#[doc(alias = "none")]
pub fn hover() -> Self { pub fn hover() -> Self {
Self { Self {
click: false, click: false,

View file

@ -2,7 +2,7 @@
#![allow(clippy::if_same_then_else)] #![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
use epaint::{Rounding, Shadow, Stroke}; use epaint::{Rounding, Shadow, Stroke};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -21,11 +21,12 @@ pub enum TextStyle {
/// Normal labels. Easily readable, doesn't take up too much space. /// Normal labels. Easily readable, doesn't take up too much space.
Body, Body,
/// Same size as [`Self::Body`], but used when monospace is important (for aligning number, code snippets, etc). /// Same size as [`Self::Body`], but used when monospace is important (for code snippets, aligning numbers, etc).
Monospace, Monospace,
/// Buttons. Maybe slightly bigger than [`Self::Body`]. /// Buttons. Maybe slightly bigger than [`Self::Body`].
/// Signifies that he item is interactive. ///
/// Signifies that he item can be interacted with.
Button, Button,
/// Heading. Probably larger than [`Self::Body`]. /// Heading. Probably larger than [`Self::Body`].
@ -173,6 +174,9 @@ pub struct Style {
/// ``` /// ```
pub text_styles: BTreeMap<TextStyle, FontId>, 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 /// 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. /// to wrap the text at the right edge of the [`Ui`] they are in.
/// By default this is `None`. /// By default this is `None`.
@ -215,6 +219,7 @@ impl Style {
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals { pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
let mut visuals = *self.visuals.widgets.style(response); let mut visuals = *self.visuals.widgets.style(response);
if selected { if selected {
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
visuals.bg_fill = self.visuals.selection.bg_fill; visuals.bg_fill = self.visuals.selection.bg_fill;
// visuals.bg_stroke = self.visuals.selection.stroke; // visuals.bg_stroke = self.visuals.selection.stroke;
visuals.fg_stroke = self.visuals.selection.stroke; visuals.fg_stroke = self.visuals.selection.stroke;
@ -252,6 +257,9 @@ pub struct Spacing {
/// Button size is text size plus this on each side /// Button size is text size plus this on each side
pub button_padding: Vec2, pub button_padding: Vec2,
/// Horizontal and vertical margins within a menu frame.
pub menu_margin: Margin,
/// Indent collapsing regions etc by this much. /// Indent collapsing regions etc by this much.
pub indent: f32, pub indent: f32,
@ -260,8 +268,11 @@ pub struct Spacing {
/// Anything clickable should be (at least) this size. /// Anything clickable should be (at least) this size.
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ? pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox). /// Default width of a [`Slider`].
pub slider_width: f32, // TODO(emilk): rename big_interact_size ? pub slider_width: f32,
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
pub combo_width: f32,
/// Default width of a [`TextEdit`]. /// Default width of a [`TextEdit`].
pub text_edit_width: f32, pub text_edit_width: f32,
@ -288,6 +299,15 @@ pub struct Spacing {
pub combo_height: f32, pub combo_height: f32,
pub scroll_bar_width: f32, 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,
} }
impl Spacing { impl Spacing {
@ -351,6 +371,10 @@ impl Margin {
pub fn right_bottom(&self) -> Vec2 { pub fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom) vec2(self.right, self.bottom)
} }
pub fn is_same(&self) -> bool {
self.left == self.right && self.left == self.top && self.left == self.bottom
}
} }
impl From<f32> for Margin { impl From<f32> for Margin {
@ -455,12 +479,20 @@ pub struct Visuals {
pub window_rounding: Rounding, pub window_rounding: Rounding,
pub window_shadow: Shadow, pub window_shadow: Shadow,
pub window_fill: Color32,
pub window_stroke: Stroke,
pub menu_rounding: Rounding,
/// Panel background color
pub panel_fill: Color32,
pub popup_shadow: Shadow, pub popup_shadow: Shadow,
pub resize_corner_size: f32, pub resize_corner_size: f32,
pub text_cursor_width: f32, pub text_cursor_width: f32,
/// show where the text cursor would be if you clicked /// show where the text cursor would be if you clicked
pub text_cursor_preview: bool, pub text_cursor_preview: bool,
@ -472,6 +504,18 @@ pub struct Visuals {
/// Show a background behind collapsing headers. /// Show a background behind collapsing headers.
pub collapsing_header_frame: bool, 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 { impl Visuals {
@ -486,7 +530,7 @@ impl Visuals {
} }
pub fn weak_text_color(&self) -> Color32 { pub fn weak_text_color(&self) -> Color32 {
crate::color::tint_color_towards(self.text_color(), self.window_fill()) self.gray_out(self.text_color())
} }
#[inline(always)] #[inline(always)]
@ -497,12 +541,25 @@ impl Visuals {
/// Window background color. /// Window background color.
#[inline(always)] #[inline(always)]
pub fn window_fill(&self) -> Color32 { pub fn window_fill(&self) -> Color32 {
self.widgets.noninteractive.bg_fill self.window_fill
} }
#[inline(always)] #[inline(always)]
pub fn window_stroke(&self) -> Stroke { pub fn window_stroke(&self) -> Stroke {
self.widgets.noninteractive.bg_stroke self.window_stroke
}
/// When fading out things, we fade the colors towards this.
// TODO(emilk): replace with an alpha
#[inline(always)]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill
}
/// Returned a "grayed out" version of the given color.
#[inline(always)]
pub fn gray_out(&self, color: Color32) -> Color32 {
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
} }
} }
@ -529,7 +586,9 @@ pub struct Widgets {
/// The style of an interactive widget, such as a button, at rest. /// The style of an interactive widget, such as a button, at rest.
pub inactive: WidgetVisuals, 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, pub hovered: WidgetVisuals,
/// The style of an interactive widget as you are clicking or dragging it. /// The style of an interactive widget as you are clicking or dragging it.
@ -545,7 +604,7 @@ impl Widgets {
&self.noninteractive &self.noninteractive
} else if response.is_pointer_button_down_on() || response.has_focus() { } else if response.is_pointer_button_down_on() || response.has_focus() {
&self.active &self.active
} else if response.hovered() { } else if response.hovered() || response.highlighted() {
&self.hovered &self.hovered
} else { } else {
&self.inactive &self.inactive
@ -557,9 +616,17 @@ impl Widgets {
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetVisuals { 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, 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, /// For surrounding rectangle of things that need it,
/// like buttons, the box of the checkbox, etc. /// like buttons, the box of the checkbox, etc.
/// Should maybe be called `frame_stroke`. /// Should maybe be called `frame_stroke`.
@ -583,41 +650,41 @@ impl WidgetVisuals {
} }
/// Options for help debug egui by adding extra visualization /// Options for help debug egui by adding extra visualization
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DebugOptions { pub struct DebugOptions {
/// However over widgets to see their rectangles /// However over widgets to see their rectangles
pub debug_on_hover: bool, pub debug_on_hover: bool,
/// Show which widgets make their parent wider /// Show which widgets make their parent wider
pub show_expand_width: bool, pub show_expand_width: bool,
/// Show which widgets make their parent higher /// Show which widgets make their parent higher
pub show_expand_height: bool, pub show_expand_height: bool,
pub show_resize: bool, pub show_resize: bool,
/// Show an overlay on all interactive widgets.
pub show_interactive_widgets: bool,
/// Show what widget blocks the interaction of another widget.
pub show_blocking_widget: bool,
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// The default text styles of the default egui theme. /// The default text styles of the default egui theme.
pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> { pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
let mut text_styles = BTreeMap::new(); use FontFamily::{Monospace, Proportional};
text_styles.insert(
TextStyle::Small, [
FontId::new(10.0, FontFamily::Proportional), (TextStyle::Small, FontId::new(9.0, Proportional)),
); (TextStyle::Body, FontId::new(12.5, Proportional)),
text_styles.insert(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional)); (TextStyle::Button, FontId::new(12.5, Proportional)),
text_styles.insert( (TextStyle::Heading, FontId::new(18.0, Proportional)),
TextStyle::Button, (TextStyle::Monospace, FontId::new(12.0, Monospace)),
FontId::new(14.0, FontFamily::Proportional), ]
); .into()
text_styles.insert(
TextStyle::Heading,
FontId::new(20.0, FontFamily::Proportional),
);
text_styles.insert(
TextStyle::Monospace,
FontId::new(14.0, FontFamily::Monospace),
);
text_styles
} }
impl Default for Style { impl Default for Style {
@ -626,6 +693,7 @@ impl Default for Style {
override_font_id: None, override_font_id: None,
override_text_style: None, override_text_style: None,
text_styles: default_text_styles(), text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
wrap: None, wrap: None,
spacing: Spacing::default(), spacing: Spacing::default(),
interaction: Interaction::default(), interaction: Interaction::default(),
@ -642,10 +710,12 @@ impl Default for Spacing {
Self { Self {
item_spacing: vec2(8.0, 3.0), item_spacing: vec2(8.0, 3.0),
window_margin: Margin::same(6.0), window_margin: Margin::same(6.0),
menu_margin: Margin::same(6.0),
button_padding: vec2(4.0, 1.0), button_padding: vec2(4.0, 1.0),
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing` indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
interact_size: vec2(40.0, 18.0), interact_size: vec2(40.0, 18.0),
slider_width: 100.0, slider_width: 100.0,
combo_width: 100.0,
text_edit_width: 280.0, text_edit_width: 280.0,
icon_width: 14.0, icon_width: 14.0,
icon_width_inner: 8.0, icon_width_inner: 8.0,
@ -653,6 +723,9 @@ impl Default for Spacing {
tooltip_width: 600.0, tooltip_width: 600.0,
combo_height: 200.0, combo_height: 200.0,
scroll_bar_width: 8.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, indent_ends_with_horizontal_line: false,
} }
} }
@ -663,7 +736,7 @@ impl Default for Interaction {
Self { Self {
resize_grab_radius_side: 5.0, resize_grab_radius_side: 5.0,
resize_grab_radius_corner: 10.0, resize_grab_radius_corner: 10.0,
show_tooltips_only_when_still: false, show_tooltips_only_when_still: true,
} }
} }
} }
@ -677,13 +750,21 @@ impl Visuals {
widgets: Widgets::default(), widgets: Widgets::default(),
selection: Selection::default(), selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255), 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 extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
code_bg_color: Color32::from_gray(64), code_bg_color: Color32::from_gray(64),
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
error_fg_color: Color32::from_rgb(255, 0, 0), // red error_fg_color: Color32::from_rgb(255, 0, 0), // red
window_rounding: Rounding::same(6.0), window_rounding: Rounding::same(6.0),
window_shadow: Shadow::big_dark(), window_shadow: Shadow::big_dark(),
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(), popup_shadow: Shadow::small_dark(),
resize_corner_size: 12.0, resize_corner_size: 12.0,
text_cursor_width: 2.0, text_cursor_width: 2.0,
@ -691,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 clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true, button_frame: true,
collapsing_header_frame: false, collapsing_header_frame: false,
indent_has_left_vline: true,
striped: false,
slider_trailing_fill: false,
} }
} }
@ -701,12 +787,18 @@ impl Visuals {
widgets: Widgets::light(), widgets: Widgets::light(),
selection: Selection::light(), selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255), 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 extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
code_bg_color: Color32::from_gray(230), code_bg_color: Color32::from_gray(230),
warn_fg_color: Color32::from_rgb(255, 0, 0), // red also, beecause orange doesn't look great because of https://github.com/emilk/egui/issues/1455 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.
error_fg_color: Color32::from_rgb(255, 0, 0), // red error_fg_color: Color32::from_rgb(255, 0, 0), // red
window_shadow: Shadow::big_light(), window_shadow: Shadow::big_light(),
window_fill: Color32::from_gray(248),
window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
panel_fill: Color32::from_gray(248),
popup_shadow: Shadow::small_light(), popup_shadow: Shadow::small_light(),
..Self::dark() ..Self::dark()
} }
@ -745,20 +837,23 @@ impl Widgets {
pub fn dark() -> Self { pub fn dark() -> Self {
Self { Self {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
bg_fill: Color32::from_gray(27), // window background weak_bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines 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 fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
rounding: Rounding::same(2.0), rounding: Rounding::same(2.0),
expansion: 0.0, expansion: 0.0,
}, },
inactive: WidgetVisuals { 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(), bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
rounding: Rounding::same(2.0), rounding: Rounding::same(2.0),
expansion: 0.0, expansion: 0.0,
}, },
hovered: WidgetVisuals { hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(70),
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 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)), fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
@ -766,6 +861,7 @@ impl Widgets {
expansion: 1.0, expansion: 1.0,
}, },
active: WidgetVisuals { active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(55),
bg_fill: Color32::from_gray(55), bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE), bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE),
@ -773,6 +869,7 @@ impl Widgets {
expansion: 1.0, expansion: 1.0,
}, },
open: WidgetVisuals { open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
bg_fill: Color32::from_gray(27), bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
@ -785,20 +882,23 @@ impl Widgets {
pub fn light() -> Self { pub fn light() -> Self {
Self { Self {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
bg_fill: Color32::from_gray(248), // window background - should be distinct from TextEdit background weak_bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines 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 fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
rounding: Rounding::same(2.0), rounding: Rounding::same(2.0),
expansion: 0.0, expansion: 0.0,
}, },
inactive: WidgetVisuals { 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(), bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
rounding: Rounding::same(2.0), rounding: Rounding::same(2.0),
expansion: 0.0, expansion: 0.0,
}, },
hovered: WidgetVisuals { hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
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 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), fg_stroke: Stroke::new(1.5, Color32::BLACK),
@ -806,6 +906,7 @@ impl Widgets {
expansion: 1.0, expansion: 1.0,
}, },
active: WidgetVisuals { active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(165),
bg_fill: Color32::from_gray(165), bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK), bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK), fg_stroke: Stroke::new(2.0, Color32::BLACK),
@ -813,6 +914,7 @@ impl Widgets {
expansion: 1.0, expansion: 1.0,
}, },
open: WidgetVisuals { open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220), bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)), bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK), fg_stroke: Stroke::new(1.0, Color32::BLACK),
@ -839,6 +941,7 @@ impl Style {
override_font_id, override_font_id,
override_text_style, override_text_style,
text_styles, text_styles,
drag_value_text_style,
wrap: _, wrap: _,
spacing, spacing,
interaction, interaction,
@ -880,6 +983,19 @@ impl Style {
}); });
ui.end_row(); 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.label("Animation duration:");
ui.add( ui.add(
Slider::new(animation_time, 0.0..=1.0) Slider::new(animation_time, 0.0..=1.0)
@ -923,10 +1039,12 @@ impl Spacing {
let Self { let Self {
item_spacing, item_spacing,
window_margin, window_margin,
menu_margin,
button_padding, button_padding,
indent, indent,
interact_size, interact_size,
slider_width, slider_width,
combo_width,
text_edit_width, text_edit_width,
icon_width, icon_width,
icon_width_inner, icon_width_inner,
@ -935,39 +1053,15 @@ impl Spacing {
indent_ends_with_horizontal_line, indent_ends_with_horizontal_line,
combo_height, combo_height,
scroll_bar_width, scroll_bar_width,
scroll_handle_min_length,
scroll_bar_inner_margin,
scroll_bar_outer_margin,
} = self; } = self;
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing")); ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
let margin_range = 0.0..=20.0; margin_ui(ui, "Window margin:", window_margin);
ui.horizontal(|ui| { margin_ui(ui, "Menu margin:", menu_margin);
ui.add(
DragValue::new(&mut window_margin.left)
.clamp_range(margin_range.clone())
.prefix("left: "),
);
ui.add(
DragValue::new(&mut window_margin.right)
.clamp_range(margin_range.clone())
.prefix("right: "),
);
ui.label("Window margins x");
});
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut window_margin.top)
.clamp_range(margin_range.clone())
.prefix("top: "),
);
ui.add(
DragValue::new(&mut window_margin.bottom)
.clamp_range(margin_range)
.prefix("bottom: "),
);
ui.label("Window margins y");
});
ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding")); ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding"));
ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size")) ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size"))
@ -980,13 +1074,29 @@ impl Spacing {
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0)); ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
ui.label("Slider width"); 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.horizontal(|ui| {
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
ui.label("TextEdit width"); ui.label("TextEdit width");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0)); ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
ui.label("Scroll-bar width width"); 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");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_outer_margin).clamp_range(0.0..=32.0));
ui.label("Scroll-bar outer margin");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -1027,6 +1137,55 @@ impl Spacing {
} }
} }
fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) {
let margin_range = 0.0..=20.0;
ui.horizontal(|ui| {
ui.label(text);
let mut same = margin.is_same();
ui.checkbox(&mut same, "Same");
if same {
let mut value = margin.left;
ui.add(DragValue::new(&mut value).clamp_range(margin_range.clone()));
*margin = Margin::same(value);
} else {
if margin.is_same() {
// HACK: prevent collapse:
margin.right = margin.left + 1.0;
margin.bottom = margin.left + 2.0;
margin.top = margin.left + 3.0;
}
ui.add(
DragValue::new(&mut margin.left)
.clamp_range(margin_range.clone())
.prefix("L: "),
)
.on_hover_text("Left margin");
ui.add(
DragValue::new(&mut margin.right)
.clamp_range(margin_range.clone())
.prefix("R: "),
)
.on_hover_text("Right margin");
ui.add(
DragValue::new(&mut margin.top)
.clamp_range(margin_range.clone())
.prefix("T: "),
)
.on_hover_text("Top margin");
ui.add(
DragValue::new(&mut margin.bottom)
.clamp_range(margin_range)
.prefix("B: "),
)
.on_hover_text("Bottom margin");
}
});
}
impl Interaction { impl Interaction {
pub fn ui(&mut self, ui: &mut crate::Ui) { pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { let Self {
@ -1096,13 +1255,17 @@ impl Selection {
impl WidgetVisuals { impl WidgetVisuals {
pub fn ui(&mut self, ui: &mut crate::Ui) { pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { let Self {
bg_fill, weak_bg_fill,
bg_fill: mandatory_bg_fill,
bg_stroke, bg_stroke,
rounding, rounding,
fg_stroke, fg_stroke,
expansion, expansion,
} = self; } = 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"); stroke_ui(ui, bg_stroke, "background stroke");
rounding_ui(ui, rounding); rounding_ui(ui, rounding);
@ -1158,20 +1321,35 @@ impl Visuals {
code_bg_color, code_bg_color,
warn_fg_color, warn_fg_color,
error_fg_color, error_fg_color,
window_rounding, window_rounding,
window_shadow, window_shadow,
window_fill,
window_stroke,
menu_rounding,
panel_fill,
popup_shadow, popup_shadow,
resize_corner_size, resize_corner_size,
text_cursor_width, text_cursor_width,
text_cursor_preview, text_cursor_preview,
clip_rect_margin, clip_rect_margin,
button_frame, button_frame,
collapsing_header_frame, collapsing_header_frame,
indent_has_left_vline,
striped,
slider_trailing_fill,
} = self; } = self;
ui.collapsing("Background Colors", |ui| { 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, &mut widgets.noninteractive.bg_fill, "Windows"); ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
"Used for faint accentuation of interactive things, like striped grids.", "Used for faint accentuation of interactive things, like striped grids.",
); );
@ -1180,14 +1358,15 @@ impl Visuals {
}); });
ui.collapsing("Window", |ui| { ui.collapsing("Window", |ui| {
// Common shortcuts ui_color(ui, window_fill, "Fill");
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill"); stroke_ui(ui, window_stroke, "Outline");
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
rounding_ui(ui, window_rounding); rounding_ui(ui, window_rounding);
shadow_ui(ui, window_shadow, "Shadow"); 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)); ui.collapsing("Widgets", |ui| widgets.ui(ui));
@ -1220,6 +1399,14 @@ impl Visuals {
ui.checkbox(button_frame, "Button has a frame"); ui.checkbox(button_frame, "Button has a frame");
ui.checkbox(collapsing_header_frame, "Collapsing header 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)); ui.vertical_centered(|ui| reset_button(ui, self));
} }
@ -1229,21 +1416,33 @@ impl DebugOptions {
pub fn ui(&mut self, ui: &mut crate::Ui) { pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { let Self {
debug_on_hover, debug_on_hover,
show_expand_width: debug_expand_width, show_expand_width,
show_expand_height: debug_expand_height, show_expand_height,
show_resize: debug_resize, show_resize,
show_interactive_widgets,
show_blocking_widget,
} = self; } = self;
ui.checkbox(debug_on_hover, "Show debug info on hover"); ui.checkbox(debug_on_hover, "Show debug info on hover");
ui.checkbox( ui.checkbox(
debug_expand_width, show_expand_width,
"Show which widgets make their parent wider", "Show which widgets make their parent wider",
); );
ui.checkbox( ui.checkbox(
debug_expand_height, show_expand_height,
"Show which widgets make their parent higher", "Show which widgets make their parent higher",
); );
ui.checkbox(debug_resize, "Debug Resize"); ui.checkbox(show_resize, "Debug Resize");
ui.checkbox(
show_interactive_widgets,
"Show an overlay on all interactive widgets",
);
ui.checkbox(
show_blocking_widget,
"Show wha widget blocks the interaction of another widget",
);
ui.vertical_centered(|ui| reset_button(ui, self)); ui.vertical_centered(|ui| reset_button(ui, self));
} }

View file

@ -3,11 +3,11 @@
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use epaint::mutex::RwLock;
use crate::{ use crate::{
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer, containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
widgets::*, *, util::IdTypeMap, widgets::*, *,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -239,7 +239,7 @@ impl Ui {
self.enabled &= enabled; self.enabled &= enabled;
if !self.enabled && self.is_visible() { if !self.enabled && self.is_visible() {
self.painter self.painter
.set_fade_to_color(Some(self.visuals().window_fill())); .set_fade_to_color(Some(self.visuals().fade_out_to_color()));
} }
} }
@ -314,84 +314,9 @@ impl Ui {
self.painter().layer_id() 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 /// The height of text of this text style
pub fn text_style_height(&self, style: &TextStyle) -> f32 { 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. /// 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 /// # Sizes etc
@ -581,18 +587,19 @@ impl Ui {
/// Use this to generate widget ids for widgets that have persistent state in [`Memory`]. /// Use this to generate widget ids for widgets that have persistent state in [`Memory`].
pub fn make_persistent_id<IdSource>(&self, id_source: IdSource) -> Id pub fn make_persistent_id<IdSource>(&self, id_source: IdSource) -> Id
where where
IdSource: Hash + std::fmt::Debug, IdSource: Hash,
{ {
self.id.with(&id_source) self.id.with(&id_source)
} }
pub(crate) fn next_auto_id(&self) -> Id { /// This is the `Id` that will be assigned to the next widget added to this `Ui`.
pub fn next_auto_id(&self) -> Id {
Id::new(self.next_auto_id_source) Id::new(self.next_auto_id_source)
} }
pub(crate) fn auto_id_with<IdSource>(&self, id_source: IdSource) -> Id pub fn auto_id_with<IdSource>(&self, id_source: IdSource) -> Id
where where
IdSource: Hash + std::fmt::Debug, IdSource: Hash,
{ {
Id::new(self.next_auto_id_source).with(id_source) Id::new(self.next_auto_id_source).with(id_source)
} }
@ -617,6 +624,22 @@ impl Ui {
) )
} }
/// Check for clicks, and drags on a specific region that is hovered.
/// This can be used once you have checked that some shape you are painting has been hovered,
/// and want to check for clicks and drags on hovered items this frame.
/// The given [`Rect`] should approximately be where the thing is,
/// as it is just where warnings will be painted if there is an [`Id`] clash.
pub fn interact_with_hovered(
&self,
rect: Rect,
hovered: bool,
id: Id,
sense: Sense,
) -> Response {
self.ctx()
.interact_with_hovered(self.layer_id(), id, rect, sense, self.enabled, hovered)
}
/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]?
/// ///
/// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance, /// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance,
@ -944,7 +967,8 @@ impl Ui {
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) { pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 { for d in 0..2 {
let range = rect.min[d]..=rect.max[d]; 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)));
} }
} }
@ -973,7 +997,8 @@ impl Ui {
let target = self.next_widget_position(); let target = self.next_widget_position();
for d in 0..2 { for d in 0..2 {
let target = target[d]; 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)));
} }
} }
@ -1005,7 +1030,8 @@ impl Ui {
/// # }); /// # });
/// ``` /// ```
pub fn scroll_with_delta(&self, delta: Vec2) { 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);
} }
} }
@ -1430,7 +1456,7 @@ impl Ui {
text: impl Into<WidgetText>, text: impl Into<WidgetText>,
) -> Response { ) -> Response {
let mut response = self.radio(*current_value == alternative, text); let mut response = self.radio(*current_value == alternative, text);
if response.clicked() { if response.clicked() && *current_value != alternative {
*current_value = alternative; *current_value = alternative;
response.mark_changed(); response.mark_changed();
} }
@ -1458,7 +1484,7 @@ impl Ui {
text: impl Into<WidgetText>, text: impl Into<WidgetText>,
) -> Response { ) -> Response {
let mut response = self.selectable_label(*current_value == selected_value, text); let mut response = self.selectable_label(*current_value == selected_value, text);
if response.clicked() { if response.clicked() && *current_value != selected_value {
*current_value = selected_value; *current_value = selected_value;
response.mark_changed(); response.mark_changed();
} }
@ -1536,7 +1562,7 @@ impl Ui {
/// ui.ctx().load_texture( /// ui.ctx().load_texture(
/// "my-image", /// "my-image",
/// egui::ColorImage::example(), /// egui::ColorImage::example(),
/// egui::TextureFilter::Linear /// Default::default()
/// ) /// )
/// }); /// });
/// ///
@ -1546,7 +1572,7 @@ impl Ui {
/// } /// }
/// ``` /// ```
/// ///
/// Se also [`crate::Image`] and [`crate::ImageButton`]. /// See also [`crate::Image`] and [`crate::ImageButton`].
#[inline] #[inline]
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response { pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self) Image::new(texture_id, size).ui(self)
@ -1763,25 +1789,32 @@ impl Ui {
}; };
let ret = add_contents(&mut child_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; let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
if left_vline || end_with_horizontal_line {
if end_with_horizontal_line { if end_with_horizontal_line {
child_ui.add_space(4.0); 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 stroke = self.visuals().widgets.noninteractive.bg_stroke;
let left_top = child_rect.min - 0.5 * indent * Vec2::X; let left_top = child_rect.min - 0.5 * indent * Vec2::X;
let left_top = self.painter().round_pos_to_pixels(left_top); 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 = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
let left_bottom = self.painter().round_pos_to_pixels(left_bottom); 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); self.painter.line_segment([left_top, left_bottom], stroke);
}
if end_with_horizontal_line { if end_with_horizontal_line {
let fudge = 2.0; // looks nicer with button rounding in collapsing headers 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); let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
self.painter self.painter
.line_segment([left_bottom, right_bottom], stroke); .line_segment([left_bottom, right_bottom], stroke);
} }
}
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
InnerResponse::new(ret, response) InnerResponse::new(ret, response)
@ -1990,7 +2023,14 @@ impl Ui {
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover())) InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
} }
#[deprecated = "Use ui.vertical_centered or ui.centered_and_justified"]
pub fn centered<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> InnerResponse<R> {
self.vertical_centered(add_contents)
}
/// This will make the next added widget centered and justified in the available space. /// This will make the next added widget centered and justified in the available space.
///
/// Only one widget may be added to the inner `Ui`!
pub fn centered_and_justified<R>( pub fn centered_and_justified<R>(
&mut self, &mut self,
add_contents: impl FnOnce(&mut Self) -> R, add_contents: impl FnOnce(&mut Self) -> R,
@ -2025,7 +2065,7 @@ impl Ui {
self.placer.set_row_height(height); self.placer.set_row_height(height);
} }
/// Temporarily split split an Ui into several columns. /// Temporarily split a [`Ui`] into several columns.
/// ///
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
@ -2133,6 +2173,43 @@ impl Ui {
menu::menu_button(self, title, add_contents) 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] #[inline]
pub fn is_empty(&mut self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_empty()
} }
#[inline] #[inline]
pub fn len(&mut self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.0.len()
} }
/// Count how many values are stored but not yet deserialized. /// Count how many values are stored but not yet deserialized.
#[inline] #[inline]
pub fn count_serialized(&mut self) -> usize { pub fn count_serialized(&self) -> usize {
self.0 self.0
.values() .values()
.filter(|e| matches!(e, Element::Serialized { .. })) .filter(|e| matches!(e, Element::Serialized { .. }))
@ -479,7 +479,7 @@ impl IdTypeMap {
} }
/// Count the number of values are stored with the given type. /// 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>(); let key = TypeId::of::<T>();
self.0 self.0
.iter() .iter()

View file

@ -2,11 +2,10 @@
pub mod cache; pub mod cache;
pub(crate) mod fixed_cache; pub(crate) mod fixed_cache;
mod history;
pub mod id_type_map; pub mod id_type_map;
pub mod undoer; pub mod undoer;
pub use history::History;
pub use id_type_map::IdTypeMap; pub use id_type_map::IdTypeMap;
pub use epaint::emath::History;
pub use epaint::util::{hash, hash_with}; pub use epaint::util::{hash, hash_with};

View file

@ -293,12 +293,12 @@ impl RichText {
let underline = if underline { let underline = if underline {
crate::Stroke::new(1.0, line_color) crate::Stroke::new(1.0, line_color)
} else { } else {
crate::Stroke::none() crate::Stroke::NONE
}; };
let strikethrough = if strikethrough { let strikethrough = if strikethrough {
crate::Stroke::new(1.0, line_color) crate::Stroke::new(1.0, line_color)
} else { } else {
crate::Stroke::none() crate::Stroke::NONE
}; };
let valign = if raised { let valign = if raised {
@ -573,14 +573,14 @@ impl WidgetText {
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign); let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap.max_width = wrap_width; text_job.job.wrap.max_width = wrap_width;
WidgetTextGalley { 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, galley_has_color: text_job.job_has_color,
} }
} }
Self::LayoutJob(mut job) => { Self::LayoutJob(mut job) => {
job.wrap.max_width = wrap_width; job.wrap.max_width = wrap_width;
WidgetTextGalley { WidgetTextGalley {
galley: ui.fonts().layout_job(job), galley: ui.fonts(|f| f.layout_job(job)),
galley_has_color: true, galley_has_color: true,
} }
} }

View file

@ -21,6 +21,7 @@ use crate::*;
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button { pub struct Button {
text: WidgetText, text: WidgetText,
shortcut_text: WidgetText,
wrap: Option<bool>, wrap: Option<bool>,
/// None means default for interact /// None means default for interact
fill: Option<Color32>, fill: Option<Color32>,
@ -29,6 +30,7 @@ pub struct Button {
small: bool, small: bool,
frame: Option<bool>, frame: Option<bool>,
min_size: Vec2, min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>, image: Option<widgets::Image>,
} }
@ -36,6 +38,7 @@ impl Button {
pub fn new(text: impl Into<WidgetText>) -> Self { pub fn new(text: impl Into<WidgetText>) -> Self {
Self { Self {
text: text.into(), text: text.into(),
shortcut_text: Default::default(),
wrap: None, wrap: None,
fill: None, fill: None,
stroke: None, stroke: None,
@ -43,27 +46,21 @@ impl Button {
small: false, small: false,
frame: None, frame: None,
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
rounding: None,
image: None, image: None,
} }
} }
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the size Vec2 provided. /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn image_and_text( pub fn image_and_text(
texture_id: TextureId, texture_id: TextureId,
size: impl Into<Vec2>, image_size: impl Into<Vec2>,
text: impl Into<WidgetText>, text: impl Into<WidgetText>,
) -> Self { ) -> Self {
Self { Self {
text: text.into(), image: Some(widgets::Image::new(texture_id, image_size)),
fill: None, ..Self::new(text)
stroke: None,
sense: Sense::click(),
small: false,
frame: None,
wrap: None,
min_size: Vec2::ZERO,
image: Some(widgets::Image::new(texture_id, size)),
} }
} }
@ -116,16 +113,34 @@ impl Button {
self self
} }
pub(crate) fn min_size(mut self, min_size: Vec2) -> Self { /// Set the minimum size of the button.
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size; self.min_size = min_size;
self 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`).
///
/// The text can be created with [`Context::format_shortcut`].
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
self.shortcut_text = shortcut_text.into();
self
}
} }
impl Widget for Button { impl Widget for Button {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Button { let Button {
text, text,
shortcut_text,
wrap, wrap,
fill, fill,
stroke, stroke,
@ -133,6 +148,7 @@ impl Widget for Button {
small, small,
frame, frame,
min_size, min_size,
rounding,
image, image,
} = self; } = self;
@ -142,27 +158,48 @@ impl Widget for Button {
if small { if small {
button_padding.y = 0.0; button_padding.y = 0.0;
} }
let total_extra = button_padding + button_padding;
let wrap_width = ui.available_width() - total_extra.x; let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); if let Some(image) = image {
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
}
if !shortcut_text.is_empty() {
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
}
let mut desired_size = text.size() + 2.0 * button_padding; let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button);
let shortcut_text = (!shortcut_text.is_empty())
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
let mut desired_size = text.size();
if let Some(image) = image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y);
}
if let Some(shortcut_text) = &shortcut_text {
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 { if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
} }
desired_size = desired_size.at_least(min_size); desired_size = desired_size.at_least(min_size);
if let Some(image) = image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y + 2.0 * button_padding.y);
}
let (rect, response) = ui.allocate_at_least(desired_size, sense); let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text())); response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
if ui.is_rect_visible(rect) { if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_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 { let text_pos = if let Some(image) = image {
let icon_spacing = ui.spacing().icon_spacing; let icon_spacing = ui.spacing().icon_spacing;
pos2( pos2(
@ -174,28 +211,31 @@ impl Widget for Button {
.align_size_within_rect(text.size(), rect.shrink2(button_padding)) .align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min .min
}; };
if frame {
let fill = fill.unwrap_or(visuals.bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
ui.painter().rect(
rect.expand(visuals.expansion),
visuals.rounding,
fill,
stroke,
);
}
text.paint_with_visuals(ui.painter(), text_pos, visuals); text.paint_with_visuals(ui.painter(), text_pos, visuals);
if let Some(shortcut_text) = shortcut_text {
let shortcut_text_pos = pos2(
rect.max.x - button_padding.x - shortcut_text.size().x,
rect.center().y - 0.5 * shortcut_text.size().y,
);
shortcut_text.paint_with_fallback_color(
ui.painter(),
shortcut_text_pos,
ui.visuals().weak_text_color(),
);
} }
if let Some(image) = image { if let Some(image) = image {
let image_rect = Rect::from_min_size( 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.size(),
); );
image.paint_at(ui, image_rect); image.paint_at(ui, image_rect);
} }
}
response response
} }
@ -229,6 +269,10 @@ impl<'a> Checkbox<'a> {
text: text.into(), text: text.into(),
} }
} }
pub fn without_text(checked: &'a mut bool) -> Self {
Self::new(checked, WidgetText::default())
}
} }
impl<'a> Widget for Checkbox<'a> { impl<'a> Widget for Checkbox<'a> {
@ -500,7 +544,7 @@ impl Widget for ImageButton {
( (
expansion, expansion,
visuals.rounding, visuals.rounding,
visuals.bg_fill, visuals.weak_bg_fill,
visuals.bg_stroke, visuals.bg_stroke,
) )
} else { } else {

View file

@ -2,7 +2,7 @@
use crate::util::fixed_cache::FixedCache; use crate::util::fixed_cache::FixedCache;
use crate::*; use crate::*;
use epaint::{color::*, *}; use epaint::{ecolor::*, *};
fn contrast_color(color: impl Into<Rgba>) -> Color32 { fn contrast_color(color: impl Into<Rgba>) -> Color32 {
if color.into().intensity() < 0.5 { if color.into().intensity() < 0.5 {
@ -211,7 +211,7 @@ fn color_slider_2d(
} }
/// What options to show for alpha /// What options to show for alpha
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum Alpha { pub enum Alpha {
// Set alpha to 1.0, and show no option for it. // Set alpha to 1.0, and show no option for it.
Opaque, Opaque,
@ -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 ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque { if alpha == Alpha::Opaque {
ui.output().copied_text = format!("{}, {}, {}", r, g, b); ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
} else { } else {
ui.output().copied_text = format!("{}, {}, {}, {}", r, g, b, a); ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
} }
} }
@ -341,22 +341,26 @@ 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 { pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let popup_id = ui.auto_id_with("popup"); 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); let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips { if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color"); button_response = button_response.on_hover_text("Click to edit color");
} }
if button_response.clicked() { 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 // 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) let area_response = Area::new(popup_id)
.order(Order::Foreground) .order(Order::Foreground)
.current_pos(button_response.rect.max) .fixed_pos(button_response.rect.max)
.constrain(true)
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = 210.0; ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
Frame::popup(ui.style()).show(ui, |ui| { Frame::popup(ui.style()).show(ui, |ui| {
if color_picker_hsva_2d(ui, hsva, alpha) { if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed(); button_response.mark_changed();
@ -366,9 +370,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
.response; .response;
if !button_response.clicked() 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());
} }
} }
@ -421,7 +425,7 @@ pub fn color_edit_button_rgb(ui: &mut Ui, rgb: &mut [f32; 3]) -> Response {
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache: // To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva { fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
let rgba = rgba.into(); let rgba = rgba.into();
use_color_cache(ctx, |cc| cc.get(&rgba).cloned()).unwrap_or_else(|| Hsva::from(rgba)) use_color_cache(ctx, |cc| cc.get(&rgba).copied()).unwrap_or_else(|| Hsva::from(rgba))
} }
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache: // To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
@ -432,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: // 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 { 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())))
} }

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