Merge remote-tracking branch 'egui/master' into dynamic-grid

This commit is contained in:
René Rössler 2022-02-27 13:30:29 +01:00
commit 1cd2a3c984
107 changed files with 4529 additions and 3858 deletions

View file

@ -132,7 +132,7 @@ jobs:
toolchain: 1.56.0 toolchain: 1.56.0
override: true override: true
- run: sudo apt-get update && sudo apt-get install libspeechd-dev - run: sudo apt-get update && sudo apt-get install libspeechd-dev
- run: cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_glium -p egui_glow -p egui_extras --lib --no-deps --all-features - run: cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_extras -p egui_glium -p egui_glow --lib --no-deps --all-features
doc_web: doc_web:
name: cargo doc web name: cargo doc web
@ -153,3 +153,17 @@ jobs:
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:
profile: minimal
toolchain: 1.56.0
override: true
- run: rustup target add wasm32-unknown-unknown
- run: cargo install wasm-bindgen-cli
- run: ./sh/wasm_bindgen_check.sh

View file

@ -1,3 +1,5 @@
{ {
"editor.formatOnSave": true "files.insertFinalNewline": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true
} }

View file

@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUT
## Crate overview ## Crate overview
The crates in this repository are: `egui, emath, epaint, egui, epi, egui-winit, egui_web, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`. The crates in this repository are: `egui, emath, epaint, egui_extras, epi, egui-winit, egui_web, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`.
### `egui`: The main GUI library. ### `egui`: The main GUI library.
Example code: `if ui.button("Click me").clicked() { … }` Example code: `if ui.button("Click me").clicked() { … }`
@ -21,6 +21,9 @@ Example: `Shape::Circle { center, radius, fill, stroke }`
Depends on `emath`, [`ab_glyph`](https://crates.io/crates/ab_glyph), [`atomic_refcell`](https://crates.io/crates/atomic_refcell), [`ahash`](https://crates.io/crates/ahash). Depends on `emath`, [`ab_glyph`](https://crates.io/crates/ab_glyph), [`atomic_refcell`](https://crates.io/crates/atomic_refcell), [`ahash`](https://crates.io/crates/ahash).
### `egui_extras`
This adds additional features on top of `egui`.
### `epi` ### `epi`
Depends only on `egui`. Depends only on `egui`.
Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`. Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`.

View file

@ -1,59 +1,102 @@
# 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_web`](egui_web/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`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/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!
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22 - Improved font selection and image handling
### Added ⭐ ### Added ⭐
* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)): * Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)):
* You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`. * You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`.
* Easily change text styles with `Style::text_styles`. * Easily change text styles with `Style::text_styles`.
* Added `Ui::text_style_height`. * Added `Ui::text_style_height`.
* Added `TextStyle::resolve`. * Added `TextStyle::resolve`.
* Made the v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)).
* Plot:
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)).
* Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)).
* Added plot pointer coordinates with `Plot::coordinates_formatter`. ([#1235](https://github.com/emilk/egui/pull/1235)).
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
* `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)). * `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)).
* `Ui::input_mut` to modify how subsequent widgets see the `InputState` and a convenience method `InputState::consume_key` for shortcuts or hotkeys ([#1212](https://github.com/emilk/egui/pull/1212)).
* Added `Ui::add_visible` and `Ui::add_visible_ui`. * Added `Ui::add_visible` and `Ui::add_visible_ui`.
* Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)).
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)). * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)).
* Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)). * Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)).
* Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)).
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
* Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)). * Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)).
* Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)).
* Added `ui.weak(text)`. * Added `ui.weak(text)`.
* Added `Slider::step_by` ([1225](https://github.com/emilk/egui/pull/1225)).
* Added `Context::move_to_top` and `Context::top_most_layer` for managing the layer on the top ([#1242](https://github.com/emilk/egui/pull/1242)).
* Support a subset of macOS' emacs input field keybindings in `TextEdit` ([#1243](https://github.com/emilk/egui/pull/1243)).
* Added ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)).
* Added `Ui::scroll_to_rect` ([1252](https://github.com/emilk/egui/pull/1252)).
### Changed 🔧 ### Changed 🔧
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
* `if let Some(pos) = ui.input().pointer.latest_pos()` and similar must now be rewritten on two lines. * `if let Some(pos) = ui.input().pointer.latest_pos()` and similar must now be rewritten on two lines.
* Search for this problem in your code using the regex `if let .*input`. * Search for this problem in your code using the regex `if let .*input`.
* Better contrast in the default light mode style ([#1238](https://github.com/emilk/egui/pull/1238)).
* Renamed `CtxRef` to `Context` ([#1050](https://github.com/emilk/egui/pull/1050)). * Renamed `CtxRef` to `Context` ([#1050](https://github.com/emilk/egui/pull/1050)).
* `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)). * `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)).
* Renamed `Ui::visible` to `Ui::is_visible`. * Renamed `Ui::visible` to `Ui::is_visible`.
* Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)). * Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)).
* For integrations:
* `FontImage` has been replaced by `TexturesDelta` (found in `Output`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)).
* The painter must support partial texture updates ([#1149](https://github.com/emilk/egui/pull/1149)).
* Added `RawInput::max_texture_side` which should be filled in with e.g. `GL_MAX_TEXTURE_SIZE` ([#1154](https://github.com/emilk/egui/pull/1154)).
* Replaced `Style::body_text_style` with more generic `Style::text_styles` ([#1154](https://github.com/emilk/egui/pull/1154)). * Replaced `Style::body_text_style` with more generic `Style::text_styles` ([#1154](https://github.com/emilk/egui/pull/1154)).
* `TextStyle` is no longer `Copy` ([#1154](https://github.com/emilk/egui/pull/1154)). * `TextStyle` is no longer `Copy` ([#1154](https://github.com/emilk/egui/pull/1154)).
* Replaced `TextEdit::text_style` with `TextEdit::font` ([#1154](https://github.com/emilk/egui/pull/1154)). * Replaced `TextEdit::text_style` with `TextEdit::font` ([#1154](https://github.com/emilk/egui/pull/1154)).
* Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)).
* Replaced Frame's `margin: Vec2` with `margin: Margin`, allowing for different margins on opposing sides ([#1219](https://github.com/emilk/egui/pull/1219)).
* `Plot::highlight` now takes a `bool` argument ([#1159](https://github.com/emilk/egui/pull/1159)). * `Plot::highlight` now takes a `bool` argument ([#1159](https://github.com/emilk/egui/pull/1159)).
* `ScrollArea::show` now returns a `ScrollAreaOutput`, so you might need to add `.inner` after the call to it ([#1166](https://github.com/emilk/egui/pull/1166)). * `ScrollArea::show` now returns a `ScrollAreaOutput`, so you might need to add `.inner` after the call to it ([#1166](https://github.com/emilk/egui/pull/1166)).
* Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)).
* Replaced Frame's `margin: Vec2` with `margin: Margin`, allowing for different margins on opposing sides ([#1219](https://github.com/emilk/egui/pull/1219)).
* Renamed `Plot::custom_label_func` to `Plot::label_formatter` ([#1235](https://github.com/emilk/egui/pull/1235)).
* `Areas::layer_id_at` ignores non-interatable layers (i.e. Tooltips) ([#1240](https://github.com/emilk/egui/pull/1240)).
* `ScrollArea`:s will not shrink below a certain minimum size, set by `min_scrolled_width/min_scrolled_height` ([1255](https://github.com/emilk/egui/pull/1255)).
* For integrations:
* `Output` has now been renamed `PlatformOutput` and `Context::run` now returns the new `FullOutput` ([#1292](https://github.com/emilk/egui/pull/1292)).
* `FontImage` has been replaced by `TexturesDelta` (found in `FullOutput`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)).
* The painter must support partial texture updates ([#1149](https://github.com/emilk/egui/pull/1149)).
* Added `RawInput::max_texture_side` which should be filled in with e.g. `GL_MAX_TEXTURE_SIZE` ([#1154](https://github.com/emilk/egui/pull/1154)).
### Fixed 🐛 ### Fixed 🐛
* Context menus now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)).
* Plot `Orientation` was not public, although fields using this type were ([#1130](https://github.com/emilk/egui/pull/1130)). * Plot `Orientation` was not public, although fields using this type were ([#1130](https://github.com/emilk/egui/pull/1130)).
* Fixed `enable_drag` for Windows ([#1108](https://github.com/emilk/egui/pull/1108)). * Context menus now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)).
* Calling `Context::set_pixels_per_point` before the first frame will now work. * Calling `Context::set_pixels_per_point` before the first frame will now work.
* Tooltips that don't fit the window don't flicker anymore ([#1240](https://github.com/emilk/egui/pull/1240)).
* Scroll areas now follow text cursor ([#1252](https://github.com/emilk/egui/pull/1252)).
* Slider: correctly respond with drag and focus events when interacting with the value directly ([1270](https://github.com/emilk/egui/pull/1270)).
### Contributors 🙏 ### Contributors 🙏
* [AlexxxRu](https://github.com/alexxxru): [#1108](https://github.com/emilk/egui/pull/1108). * [4JX](https://github.com/4JX)
* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050). * [55nknown](https://github.com/55nknown)
* [juancampa](https://github.com/juancampa): [#1147](https://github.com/emilk/egui/pull/1147). * [AlanRace](https://github.com/AlanRace)
* [AlexxxRu](https://github.com/AlexxxRu)
* [awaken1ng](https://github.com/awaken1ng)
* [BctfN0HUK7Yg](https://github.com/BctfN0HUK7Yg)
* [Bromeon](https://github.com/Bromeon)
* [cat-state](https://github.com/cat)
* [danielkeller](https://github.com/danielkeller)
* [dvec](https://github.com/dvec)
* [Friz64](https://github.com/Friz64)
* [Gordon01](https://github.com/Gordon01)
* [HackerFoo](https://github.com/HackerFoo)
* [juancampa](https://github.com/juancampa)
* [justinj](https://github.com/justinj)
* [lampsitter](https://github.com/lampsitter)
* [LordMZTE](https://github.com/LordMZTE)
* [manuel-i](https://github.com/manuel)
* [Mingun](https://github.com/Mingun)
* [niklaskorz](https://github.com/niklaskorz)
* [nongiach](https://github.com/nongiach)
* [parasyte](https://github.com/parasyte)
* [psiphi75](https://github.com/psiphi75)
* [s-nie](https://github.com/s)
* [t18b219k](https://github.com/t18b219k)
* [terhechte](https://github.com/terhechte)
* [xudesheng](https://github.com/xudesheng)
* [yusdacra](https://github.com/yusdacra)
## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame` ## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame`
@ -575,7 +618,7 @@ This is when I started the CHANGELOG.md, after almost two years of development.
* 2020-08-10: renamed the project to "egui" * 2020-08-10: renamed the project to "egui"
* 2020-05-30: first release on crates.io (0.1.0) * 2020-05-30: first release on crates.io (0.1.0)
* 2020-05-01: serious work starts (pandemic project) * 2020-04-01: serious work starts (pandemic project)
* 2019-03-12: gave a talk about what would later become egui: https://www.youtube.com/watch?v=-pmwLHw5Gbs * 2019-03-12: gave a talk about what would later become egui: https://www.youtube.com/watch?v=-pmwLHw5Gbs
* 2018-12-23: [initial commit](https://github.com/emilk/egui/commit/856bbf4dae4a69693a0324da34e8b0dd3754dfdf) * 2018-12-23: [initial commit](https://github.com/emilk/egui/commit/856bbf4dae4a69693a0324da34e8b0dd3754dfdf)
* 2018-11-04: started tinkering on a train * 2018-11-04: started tinkering on a train

133
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[the egui discord](https://discord.gg/JFcEma9bJq).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

2617
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -148,7 +148,7 @@ Light Theme:
## Integrations ## Integrations
egui is build to be easy to integrate into any existing game engine or platform you are working on. egui is built to be easy to integrate into any existing game engine or platform you are working on.
egui itself doesn't know or care on what OS it is running or how to render things to the screen - that is the job of the egui integration. egui itself doesn't know or care on what OS it is running or how to render things to the screen - that is the job of the egui integration.
An integration needs to do the following each frame: An integration needs to do the following each frame:
@ -160,7 +160,7 @@ An integration needs to do the following each frame:
### Official integrations ### Official integrations
If you making an app, your best bet is using [`eframe`](https://github.com/emilk/egui/tree/master/eframe), the official egui framework. It lets you write apps that works on both the web and native. `eframe` is just a thin wrapper over `egui_web` and `egui_glium` (see below). If you're making an app, your best bet is using [`eframe`](https://github.com/emilk/egui/tree/master/eframe), the official egui framework. It lets you write apps that work on both the web and native. `eframe` is just a thin wrapper over `egui_web` and `egui_glium` (see below).
These are the official egui integrations: These are the official egui integrations:
@ -192,7 +192,7 @@ Missing an integration for the thing you're working on? Create one, it's easy!
### Writing your own egui integration ### Writing your own egui integration
You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html), paint [`egui::ClippedMesh`](https://docs.rs/epaint/latest/epaint/struct.ClippedMesh.html):es and handle [`egui::Output`](https://docs.rs/egui/latest/egui/struct.Output.html). The basic structure is this: You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this:
``` rust ``` rust
let mut egui_ctx = egui::CtxRef::default(); let mut egui_ctx = egui::CtxRef::default();
@ -201,20 +201,19 @@ let mut egui_ctx = egui::CtxRef::default();
loop { loop {
// Gather input (mouse, touches, keyboard, screen size, etc): // Gather input (mouse, touches, keyboard, screen size, etc):
let raw_input: egui::RawInput = my_integration.gather_input(); let raw_input: egui::RawInput = my_integration.gather_input();
let (output, shapes) = egui_ctx.run(raw_input, |egui_ctx| { let full_output = egui_ctx.run(raw_input, |egui_ctx| {
my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here
}); });
let clipped_meshes = egui_ctx.tessellate(shapes); // creates triangles to paint let clipped_meshes = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint
my_integration.set_egui_textures(&output.textures_delta.set); my_integration.paint(&full_output.textures_delta, clipped_meshes);
my_integration.paint(clipped_meshes);
my_integration.free_egui_textures(&output.textures_delta.free);
my_integration.set_cursor_icon(output.cursor_icon); let platform_output = full_output.platform_output;
if !output.copied_text.is_empty() { my_integration.set_cursor_icon(platform_output.cursor_icon);
my_integration.set_clipboard_text(output.copied_text); if !platform_output.copied_text.is_empty() {
my_integration.set_clipboard_text(platform_output.copied_text);
} }
// See `egui::Output` for more // See `egui::FullOutput` and `egui::PlatformOutput` for more
} }
``` ```
@ -303,7 +302,19 @@ Also see [GitHub Discussions](https://github.com/emilk/egui/discussions/categori
Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::set_fonts`. Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::set_fonts`.
### Can I customize the look of egui? ### Can I customize the look of egui?
Yes! You can customize the colors, spacing and sizes of everything. By default egui comes with a dark and a light theme. Yes! You can customize the colors, spacing, fonts and sizes of everything using `Context::set_style`.
Here is an example (from https://github.com/AlexxxRu/TinyPomodoro):
<img src="media/pompodoro-skin.png" width="50%">
### How do I use egui with `async`?
If you call `.await` in your GUI code, the UI will freeze, with is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like:
* Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)). Make sure to use [`try_recv`](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html#method.try_recv) so you don't block the gui thread!
* `Arc<Mutex<Value>>` (background thread sets a value; GUI thread reads it)
* [`poll_promise::Promise`](https://docs.rs/poll-promise) (example: [`eframe/examples/download_image.rs`](https://github.com/emilk/egui/blob/master/eframe/examples/download_image.rs))
* [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.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. 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.

View file

@ -28,10 +28,16 @@ deny = [
] ]
skip = [ skip = [
{ name = "time" }, # old version pulled in by unmaintianed crate 'chrono' { name = "ahash" }, # old version via dark-light
{ name = "arrayvec" }, # old version via tiny-skia
{ name = "hashbrown" }, # old version via dark-light
{ name = "time" }, # old version pulled in by unmaintianed crate 'chrono'
{ name = "ttf-parser" }, # different versions pulled in by ab_glyph and usvg
] ]
skip-tree = [ skip-tree = [
{ name = "eframe", version = "0.16.0" }, { name = "criterion" }, # dev-dependnecy
{ name = "glium" }, # legacy crate, lots of old dependencies
{ name = "glutin" }, # legacy crate, lots of old dependencies
] ]

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -128,20 +128,20 @@
.catch(on_wasm_error); .catch(on_wasm_error);
function on_wasm_loaded() { function on_wasm_loaded() {
console.debug("wasm loaded. starting egui app…"); console.debug("wasm loaded. starting app…");
// This call installs a bunch of callbacks and then returns: // This call installs a bunch of callbacks and then returns:
wasm_bindgen.start("the_canvas_id"); wasm_bindgen.start("the_canvas_id");
console.debug("egui app started."); console.debug("app started.");
document.getElementById("center_text").remove(); document.getElementById("center_text").remove();
} }
function on_wasm_error(error) { function on_wasm_error(error) {
console.error("Failed to start egui: " + error); console.error("Failed to start: " + error);
document.getElementById("center_text").innerHTML = ` document.getElementById("center_text").innerHTML = `
<p> <p>
An error occurred loading egui An error occurred during loading:
</p> </p>
<p style="font-family:Courier New"> <p style="font-family:Courier New">
${error} ${error}

View file

@ -5,6 +5,9 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)). * Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)).
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)). * The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)). * The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).
@ -12,8 +15,10 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
* Fix horizontal scrolling direction on Linux. * Fix horizontal scrolling direction on Linux.
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038)) * Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
* Added `NativeOptions::initial_window_pos`. * Added `NativeOptions::initial_window_pos`.
* Fixed `enable_drag` for Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). * Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)). * Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)).
* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29

View file

@ -1,6 +1,6 @@
[package] [package]
name = "eframe" name = "eframe"
version = "0.16.0" version = "0.17.0"
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"
@ -49,27 +49,24 @@ screen_reader = [
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false } egui = { version = "0.17.0", path = "../egui", default-features = false }
epi = { version = "0.16.0", path = "../epi" } epi = { version = "0.17.0", path = "../epi" }
# native: # native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false } egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false }
egui_glium = { version = "0.16.0", path = "../egui_glium", default-features = false, features = ["clipboard", "epi", "links"], optional = true } egui_glium = { version = "0.17.0", path = "../egui_glium", default-features = false, features = ["clipboard", "epi", "links"], optional = true }
egui_glow = { version = "0.16.0", path = "../egui_glow", default-features = false, features = ["clipboard", "epi", "links", "winit"], optional = true } egui_glow = { version = "0.17.0", path = "../egui_glow", default-features = false, features = ["clipboard", "epi", "links", "winit"], optional = true }
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
egui_web = { version = "0.16.0", path = "../egui_web", default-features = false, features = ["glow"] } egui_web = { version = "0.17.0", path = "../egui_web", default-features = false, features = ["glow"] }
[dev-dependencies] [dev-dependencies]
# For examples:
egui_extras = { path = "../egui_extras", features = ["image", "svg"] }
ehttp = "0.2" ehttp = "0.2"
image = { version = "0.24", default-features = false, features = ["jpeg", "png"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png"] }
poll-promise = "0.1" poll-promise = "0.1"
rfd = "0.7" rfd = "0.8"
# svg.rs example:
resvg = "0.20"
tiny-skia = "0.6"
usvg = "0.20"

View file

@ -1,6 +1,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
use egui_extras::RetainedImage;
use poll_promise::Promise; use poll_promise::Promise;
fn main() { fn main() {
@ -11,7 +12,7 @@ fn main() {
#[derive(Default)] #[derive(Default)]
struct MyApp { struct MyApp {
/// `None` when download hasn't started yet. /// `None` when download hasn't started yet.
promise: Option<Promise<ehttp::Result<egui::TextureHandle>>>, promise: Option<Promise<ehttp::Result<RetainedImage>>>,
} }
impl epi::App for MyApp { impl epi::App for MyApp {
@ -24,14 +25,13 @@ impl epi::App for MyApp {
// Begin download. // Begin download.
// We download the image using `ehttp`, a library that works both in WASM and on native. // We download the image using `ehttp`, a library that works both in WASM and on native.
// We use the `poll-promise` library to communicate with the UI thread. // We use the `poll-promise` library to communicate with the UI thread.
let ctx = ctx.clone();
let frame = frame.clone(); let frame = frame.clone();
let (sender, promise) = Promise::new(); let (sender, promise) = Promise::new();
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024"); let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
ehttp::fetch(request, move |response| { ehttp::fetch(request, move |response| {
let image = response.and_then(parse_response);
sender.send(image); // send the results back to the UI thread.
frame.request_repaint(); // wake up UI thread frame.request_repaint(); // wake up UI thread
let texture = response.and_then(|response| parse_response(&ctx, response));
sender.send(texture); // send the results back to the UI thread.
}); });
promise promise
}); });
@ -43,24 +43,17 @@ impl epi::App for MyApp {
Some(Err(err)) => { Some(Err(err)) => {
ui.colored_label(egui::Color32::RED, err); // something went wrong ui.colored_label(egui::Color32::RED, err); // something went wrong
} }
Some(Ok(texture)) => { Some(Ok(image)) => {
let mut size = texture.size_vec2(); image.show_max_size(ui, ui.available_size());
size *= (ui.available_width() / size.x).min(1.0);
size *= (ui.available_height() / size.y).min(1.0);
ui.image(texture, size);
} }
}); });
} }
} }
fn parse_response( fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> {
ctx: &egui::Context,
response: ehttp::Response,
) -> Result<egui::TextureHandle, String> {
let content_type = response.content_type().unwrap_or_default(); let content_type = response.content_type().unwrap_or_default();
if content_type.starts_with("image/") { if content_type.starts_with("image/") {
let image = load_image(&response.bytes).map_err(|err| err.to_string())?; RetainedImage::from_image_bytes(&response.url, &response.bytes)
Ok(ctx.load_texture("my-image", image))
} else { } else {
Err(format!( Err(format!(
"Expected image, found content-type {:?}", "Expected image, found content-type {:?}",
@ -68,14 +61,3 @@ fn parse_response(
)) ))
} }
} }
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View file

@ -22,19 +22,17 @@ impl epi::App for MyApp {
} }
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
let Self { name, age } = self;
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application"); ui.heading("My egui Application");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Your name: "); ui.label("Your name: ");
ui.text_edit_singleline(name); ui.text_edit_singleline(&mut self.name);
}); });
ui.add(egui::Slider::new(age, 0..=120).text("age")); ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() { if ui.button("Click each year").clicked() {
*age += 1; self.age += 1;
} }
ui.label(format!("Hello '{}', age {}", name, age)); ui.label(format!("Hello '{}', age {}", self.name, self.age));
}); });
// Resize the native window to be just the size we need it to be: // Resize the native window to be just the size we need it to be:

View file

@ -1,10 +1,22 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
use egui_extras::RetainedImage;
#[derive(Default)]
struct MyApp { struct MyApp {
texture: Option<egui::TextureHandle>, image: RetainedImage,
}
impl Default for MyApp {
fn default() -> Self {
Self {
image: RetainedImage::from_image_bytes(
"rust-logo-256x256.png",
include_bytes!("rust-logo-256x256.png"),
)
.unwrap(),
}
}
} }
impl epi::App for MyApp { impl epi::App for MyApp {
@ -13,17 +25,15 @@ impl epi::App for MyApp {
} }
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
let image = load_image(include_bytes!("rust-logo-256x256.png")).unwrap();
ctx.load_texture("rust-logo", image)
});
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("This is an image:"); ui.heading("This is an image:");
ui.image(texture, texture.size_vec2()); self.image.show(ui);
ui.heading("This is an image you can click:"); ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(texture, texture.size_vec2())); ui.add(egui::ImageButton::new(
self.image.texture_id(ctx),
self.image.size_vec2(),
));
}); });
} }
} }
@ -32,14 +42,3 @@ fn main() {
let options = eframe::NativeOptions::default(); let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options); eframe::run_native(Box::new(MyApp::default()), options);
} }
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View file

@ -1,82 +1,23 @@
//! A good way of displaying an SVG image in egui. //! A good way of displaying an SVG image in egui.
//! //!
//! Requires the dependencies `resvg`, `tiny-skia`, `usvg` //! Requires the dependency `egui_extras` with the `svg` feature.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
/// Load an SVG and rasterize it into an egui image.
fn load_svg_data(svg_data: &[u8]) -> Result<egui::ColorImage, String> {
let mut opt = usvg::Options::default();
opt.fontdb.load_system_fonts();
let rtree = usvg::Tree::from_data(svg_data, &opt.to_ref()).map_err(|err| err.to_string())?;
let pixmap_size = rtree.svg_node().size.to_screen_size();
let [w, h] = [pixmap_size.width(), pixmap_size.height()];
let mut pixmap = tiny_skia::Pixmap::new(w, h)
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?;
resvg::render(
&rtree,
usvg::FitTo::Original,
tiny_skia::Transform::default(),
pixmap.as_mut(),
)
.ok_or_else(|| "Failed to render SVG".to_owned())?;
let image = egui::ColorImage::from_rgba_unmultiplied(
[pixmap.width() as _, pixmap.height() as _],
pixmap.data(),
);
Ok(image)
}
// ----------------------------------------------------------------------------
/// An SVG image to be shown in egui
struct SvgImage {
image: egui::ColorImage,
texture: Option<egui::TextureHandle>,
}
impl SvgImage {
/// Pass itn the bytes of an SVG that you've loaded from disk
pub fn from_svg_data(bytes: &[u8]) -> Result<Self, String> {
Ok(Self {
image: load_svg_data(bytes)?,
texture: None,
})
}
pub fn show_max_size(&mut self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
let mut desired_size = egui::vec2(self.image.width() as _, self.image.height() as _);
desired_size *= (max_size.x / desired_size.x).min(1.0);
desired_size *= (max_size.y / desired_size.y).min(1.0);
self.show_size(ui, desired_size)
}
pub fn show_size(&mut self, ui: &mut egui::Ui, desired_size: egui::Vec2) -> egui::Response {
// We need to convert the SVG to a texture to display it:
// Future improvement: tell backend to do mip-mapping of the image to
// make it look smoother when downsized.
let svg_texture = self
.texture
.get_or_insert_with(|| ui.ctx().load_texture("svg", self.image.clone()));
ui.image(svg_texture, desired_size)
}
}
// ----------------------------------------------------------------------------
struct MyApp { struct MyApp {
svg_image: SvgImage, svg_image: egui_extras::RetainedImage,
} }
impl Default for MyApp { impl Default for MyApp {
fn default() -> Self { fn default() -> Self {
Self { Self {
svg_image: SvgImage::from_svg_data(include_bytes!("rustacean-flat-happy.svg")).unwrap(), svg_image: egui_extras::RetainedImage::from_svg_bytes(
"rustacean-flat-happy.svg",
include_bytes!("rustacean-flat-happy.svg"),
)
.unwrap(),
} }
} }
} }

View file

@ -1,15 +1,17 @@
# Changelog for egui-winit # Changelog for egui-winit
All notable changes to the `egui-winit` integration will be noted in this file. All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* Fixed horizontal scrolling direction on Linux. * Fixed horizontal scrolling direction on Linux.
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatability ([#1023](https://github.com/emilk/egui/pull/1023)) * Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatability ([#1023](https://github.com/emilk/egui/pull/1023))
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Fixed `enable_drag` on Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). * Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)). * Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)).
* Fixed `enable_drag` for Windows. Now called only once just after left click ([#1108](https://github.com/emilk/egui/pull/1108)).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui-winit" name = "egui-winit"
version = "0.16.0" version = "0.17.0"
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"
@ -43,17 +43,17 @@ convert_bytemuck = ["egui/convert_bytemuck"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = ["single_threaded", "tracing"] } egui = { version = "0.17.0", path = "../egui", default-features = false, features = ["single_threaded", "tracing"] }
instant = { version = "0.1", features = ["wasm-bindgen"] } instant = { version = "0.1", features = ["wasm-bindgen"] }
tracing = "0.1" tracing = "0.1"
winit = "0.26.1" winit = "0.26.1"
epi = { version = "0.16.0", path = "../epi", optional = true } epi = { version = "0.17.0", path = "../epi", optional = true }
copypasta = { version = "0.7", optional = true } copypasta = { version = "0.7", optional = true }
dark-light = { version = "0.2.1", optional = true } # detect dark mode system preference dark-light = { version = "0.2.1", optional = true } # detect dark mode system preference
serde = { version = "1.0", optional = true, features = ["derive"] } serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.5", optional = true } webbrowser = { version = "0.6", optional = true }
# feature screen_reader # feature screen_reader
tts = { version = "0.20", optional = true } tts = { version = "0.20", optional = true }

View file

@ -1,7 +1,4 @@
use egui::Vec2; pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
use winit::dpi::LogicalSize;
pub fn points_to_size(points: Vec2) -> LogicalSize<f64> {
winit::dpi::LogicalSize { winit::dpi::LogicalSize {
width: points.x as f64, width: points.x as f64,
height: points.y as f64, height: points.y as f64,
@ -222,6 +219,7 @@ pub struct EpiIntegration {
frame: epi::Frame, frame: epi::Frame,
persistence: crate::epi::Persistence, persistence: crate::epi::Persistence,
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput,
egui_winit: crate::State, egui_winit: crate::State,
pub app: Box<dyn epi::App>, pub app: Box<dyn epi::App>,
/// When set, it is time to quit /// When set, it is time to quit
@ -267,6 +265,7 @@ impl EpiIntegration {
persistence, persistence,
egui_ctx, egui_ctx,
egui_winit: crate::State::new(max_texture_side, window), egui_winit: crate::State::new(max_texture_side, window),
pending_full_output: Default::default(),
app, app,
quit: false, quit: false,
can_drag_window: false, can_drag_window: false,
@ -295,8 +294,8 @@ impl EpiIntegration {
fn warm_up(&mut self, window: &winit::window::Window) { fn warm_up(&mut self, window: &winit::window::Window) {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true); self.egui_ctx.memory().set_everything_is_visible(true);
let (_, textures_delta, _) = self.update(window); let full_output = self.update(window);
self.egui_ctx.output().textures_delta = textures_delta; // 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() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations(); self.egui_ctx.clear_animations();
} }
@ -323,37 +322,39 @@ impl EpiIntegration {
self.egui_winit.on_event(&self.egui_ctx, event); self.egui_winit.on_event(&self.egui_ctx, event);
} }
/// Returns `needs_repaint` and shapes to paint. pub fn update(&mut self, window: &winit::window::Window) -> egui::FullOutput {
pub fn update(
&mut self,
window: &winit::window::Window,
) -> (bool, egui::TexturesDelta, Vec<egui::epaint::ClippedShape>) {
let frame_start = instant::Instant::now(); let frame_start = instant::Instant::now();
let raw_input = self.egui_winit.take_egui_input(window); let raw_input = self.egui_winit.take_egui_input(window);
let (egui_output, shapes) = self.egui_ctx.run(raw_input, |egui_ctx| { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
self.app.update(egui_ctx, &self.frame); self.app.update(egui_ctx, &self.frame);
}); });
self.pending_full_output.append(full_output);
let full_output = std::mem::take(&mut self.pending_full_output);
let needs_repaint = egui_output.needs_repaint; {
let textures_delta = self let mut app_output = self.frame.take_app_output();
.egui_winit app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
.handle_output(window, &self.egui_ctx, egui_output); self.can_drag_window = false;
if app_output.quit {
let mut app_output = self.frame.take_app_output(); self.quit = self.app.on_exit_event();
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 }
self.can_drag_window = false; crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
if app_output.quit {
self.quit = self.app.on_exit_event();
} }
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32; let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32;
self.frame.lock().info.cpu_usage = Some(frame_time); self.frame.lock().info.cpu_usage = Some(frame_time);
(needs_repaint, textures_delta, shapes) full_output
}
pub fn handle_platform_output(
&mut self,
window: &winit::window::Window,
platform_output: egui::PlatformOutput,
) {
self.egui_winit
.handle_platform_output(window, &self.egui_ctx, platform_output);
} }
pub fn maybe_autosave(&mut self, window: &winit::window::Window) { pub fn maybe_autosave(&mut self, window: &winit::window::Window) {

View file

@ -144,7 +144,7 @@ impl State {
start_time: instant::Instant::now(), start_time: instant::Instant::now(),
egui_input: egui::RawInput { egui_input: egui::RawInput {
pixels_per_point: Some(pixels_per_point), pixels_per_point: Some(pixels_per_point),
max_texture_side, max_texture_side: Some(max_texture_side),
..Default::default() ..Default::default()
}, },
pointer_pos_in_points: None, pointer_pos_in_points: None,
@ -514,26 +514,25 @@ impl State {
/// * open any clicked urls /// * open any clicked urls
/// * update the IME /// * update the IME
/// * /// *
pub fn handle_output( pub fn handle_platform_output(
&mut self, &mut self,
window: &winit::window::Window, window: &winit::window::Window,
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
output: egui::Output, platform_output: egui::PlatformOutput,
) -> egui::TexturesDelta { ) {
if egui_ctx.options().screen_reader { if egui_ctx.options().screen_reader {
self.screen_reader.speak(&output.events_description()); self.screen_reader
.speak(&platform_output.events_description());
} }
let egui::Output { let egui::PlatformOutput {
cursor_icon, cursor_icon,
open_url, open_url,
copied_text, copied_text,
needs_repaint: _, // needs to be handled elsewhere
events: _, // handled above events: _, // handled above
mutable_text_under_cursor: _, // only used in egui_web mutable_text_under_cursor: _, // only used in egui_web
text_cursor_pos, text_cursor_pos,
textures_delta, } = platform_output;
} = 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
@ -550,8 +549,6 @@ 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 });
} }
textures_delta
} }
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) {

View file

@ -1,8 +1,8 @@
[package] [package]
name = "egui" name = "egui"
version = "0.16.1" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Simple, portable immediate mode GUI library for Rust" description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = "1.56"
homepage = "https://github.com/emilk/egui" homepage = "https://github.com/emilk/egui"
@ -58,7 +58,7 @@ multi_threaded = ["epaint/multi_threaded"]
[dependencies] [dependencies]
epaint = { version = "0.16.0", path = "../epaint", default-features = false } epaint = { version = "0.17.0", path = "../epaint", default-features = false }
ahash = "0.7" ahash = "0.7"
nohash-hasher = "0.2" nohash-hasher = "0.2"

View file

@ -204,7 +204,7 @@ impl Area {
let state = ctx.memory().areas.get(id).cloned(); let state = ctx.memory().areas.get(id).cloned();
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)), pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
@ -212,6 +212,7 @@ impl Area {
interactable, interactable,
}); });
state.pos = new_pos.unwrap_or(state.pos); state.pos = new_pos.unwrap_or(state.pos);
state.interactable = interactable;
if let Some((anchor, offset)) = anchor { if let Some((anchor, offset)) = anchor {
if is_new { if is_new {

View file

@ -1,51 +1,8 @@
//! Frame container //! Frame container
use crate::{layers::ShapeIdx, *}; use crate::{layers::ShapeIdx, style::Margin, *};
use epaint::*; use epaint::*;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Margin {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl Margin {
#[inline]
pub fn same(margin: f32) -> Self {
Self {
left: margin,
right: margin,
top: margin,
bottom: margin,
}
}
/// Margins with the same size on opposing sides
#[inline]
pub fn symmetric(x: f32, y: f32) -> Self {
Self {
left: x,
right: x,
top: y,
bottom: y,
}
}
/// Total margins on both sides
pub fn sum(&self) -> Vec2 {
Vec2::new(self.left + self.right, self.top + self.bottom)
}
}
impl From<Vec2> for Margin {
fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y)
}
}
/// Color and margin of a rectangular background of a [`Ui`]. /// Color and margin of a rectangular background of a [`Ui`].
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq)]
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]

View file

@ -16,7 +16,7 @@ pub use {
area::Area, area::Area,
collapsing_header::{CollapsingHeader, CollapsingResponse}, collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*, combo_box::*,
frame::{Frame, Margin}, frame::Frame,
panel::{CentralPanel, SidePanel, TopBottomPanel}, panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*, popup::*,
resize::Resize, resize::Resize,

View file

@ -9,6 +9,8 @@
//! The order in which you add panels matter! //! The order in which you add panels matter!
//! The first panel you add will always be the outermost, and the last you add will always be the innermost. //! The first panel you add will always be the outermost, and the last you add will always be the innermost.
//! //!
//! You must never open one top-level panel from within another panel. Add one panel, then the next.
//!
//! Always add any [`CentralPanel`] last. //! Always add any [`CentralPanel`] last.
//! //!
//! Add your [`Window`]:s after any top-level panels. //! Add your [`Window`]:s after any top-level panels.

View file

@ -81,6 +81,7 @@ pub struct ScrollArea {
has_bar: [bool; 2], has_bar: [bool; 2],
auto_shrink: [bool; 2], auto_shrink: [bool; 2],
max_size: Vec2, max_size: Vec2,
min_scrolled_size: Vec2,
always_show_scroll: bool, always_show_scroll: bool,
id_source: Option<Id>, id_source: Option<Id>,
offset_x: Option<f32>, offset_x: Option<f32>,
@ -123,6 +124,7 @@ impl ScrollArea {
has_bar, has_bar,
auto_shrink: [true; 2], auto_shrink: [true; 2],
max_size: Vec2::INFINITY, max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
always_show_scroll: false, always_show_scroll: false,
id_source: None, id_source: None,
offset_x: None, offset_x: None,
@ -152,6 +154,28 @@ impl ScrollArea {
self self
} }
/// The minimum width of a horizontal scroll area which requires scroll bars.
///
/// The `ScrollArea` will only become smaller than this if the content is smaller than this
/// (and so we don't require scroll bars).
///
/// Default: `64.0`.
pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
self.min_scrolled_size.x = min_scrolled_width;
self
}
/// The minimum height of a vertical scroll area which requires scroll bars.
///
/// The `ScrollArea` will only become smaller than this if the content is smaller than this
/// (and so we don't require scroll bars).
///
/// Default: `64.0`.
pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
self.min_scrolled_size.y = min_scrolled_height;
self
}
/// If `false` (default), the scroll bar will be hidden when not needed/ /// If `false` (default), the scroll bar will be hidden when not needed/
/// If `true`, the scroll bar will always be displayed even if not needed. /// If `true`, the scroll bar will always be displayed even if not needed.
pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self { pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
@ -288,6 +312,7 @@ impl ScrollArea {
has_bar, has_bar,
auto_shrink, auto_shrink,
max_size, max_size,
min_scrolled_size,
always_show_scroll, always_show_scroll,
id_source, id_source,
offset_x, offset_x,
@ -329,27 +354,39 @@ impl ScrollArea {
let outer_size = available_outer.size().at_most(max_size); let outer_size = available_outer.size().at_most(max_size);
let inner_size = outer_size - current_bar_use; let inner_size = {
let mut inner_size = outer_size - current_bar_use;
// Don't go so far that we shrink to zero.
// In particular, if we put a `ScrollArea` inside of a `ScrollArea`, the inner
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if has_bar[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
inner_size
};
let inner_rect = Rect::from_min_size(available_outer.min, inner_size); let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
let mut inner_child_max_size = inner_size; let mut content_max_size = inner_size;
if true { if true {
// Tell the inner Ui to *try* to fit the content without needing to scroll, // Tell the inner Ui to *try* to fit the content without needing to scroll,
// i.e. better to wrap text than showing a horizontal scrollbar! // i.e. better to wrap text and shrink images than showing a horizontal scrollbar!
} else { } else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it! // Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if has_bar[d] {
inner_child_max_size[d] = f32::INFINITY; content_max_size[d] = f32::INFINITY;
} }
} }
} }
let mut content_ui = ui.child_ui( let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
Rect::from_min_size(inner_rect.min - state.offset, inner_child_max_size), let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
*ui.layout(),
);
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin); let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
// Nice handling of forced resizing beyond the possible: // Nice handling of forced resizing beyond the possible:
@ -489,18 +526,38 @@ impl Prepared {
// 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().scroll_target[d].take();
if let Some((scroll, align)) = scroll_target { if let Some((scroll, align)) = scroll_target {
let center_factor = align.to_factor();
let min = content_ui.min_rect().min[d]; let min = content_ui.min_rect().min[d];
let visible_range = min..=min + content_ui.clip_rect().size()[d]; let clip_rect = content_ui.clip_rect();
let offset = scroll - lerp(visible_range, center_factor); let visible_range = min..=min + clip_rect.size()[d];
let start = *scroll.start();
let end = *scroll.end();
let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d]; let mut spacing = ui.spacing().item_spacing[d];
// Depending on the alignment we need to add or subtract the spacing let delta = if let Some(align) = align {
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); let center_factor = align.to_factor();
state.offset[d] = offset + spacing; let offset =
lerp(scroll, center_factor) - lerp(visible_range, center_factor);
// Depending on the alignment we need to add or subtract the spacing
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
offset + spacing - state.offset[d]
} else if start < clip_start && end < clip_end {
-(clip_start - start + spacing).min(clip_end - end - spacing)
} else if end > clip_end && start > clip_start {
(end - clip_end + spacing).min(start - clip_start - spacing)
} else {
// Ui is already in view, no need to adjust scroll.
0.0
};
if delta != 0.0 {
state.offset[d] += delta;
ui.ctx().request_repaint();
}
} }
} }
} }

View file

@ -1,8 +1,8 @@
// #![warn(missing_docs)] // #![warn(missing_docs)]
use crate::{ use crate::{
animation_manager::AnimationManager, data::output::Output, frame_state::FrameState, animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
input_state::*, layers::GraphicLayers, memory::Options, TextureHandle, *, input_state::*, layers::GraphicLayers, memory::Options, output::FullOutput, TextureHandle, *,
}; };
use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *};
@ -42,7 +42,7 @@ struct ContextImpl {
// The output of a frame: // The output of a frame:
graphics: GraphicLayers, graphics: GraphicLayers,
output: Output, output: PlatformOutput,
paint_stats: PaintStats, paint_stats: PaintStats,
@ -79,7 +79,7 @@ impl ContextImpl {
/// Load fonts unless already loaded. /// Load fonts unless already loaded.
fn update_fonts_mut(&mut self) { fn update_fonts_mut(&mut self) {
let pixels_per_point = self.input.pixels_per_point(); let pixels_per_point = self.input.pixels_per_point();
let max_texture_side = self.input.raw.max_texture_side; let max_texture_side = self.input.max_texture_side;
if let Some(font_definitions) = self.memory.new_font_definitions.take() { if let Some(font_definitions) = self.memory.new_font_definitions.take() {
let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions); let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions);
@ -108,7 +108,7 @@ impl ContextImpl {
/// Your handle to egui. /// Your handle to egui.
/// ///
/// This is the first thing you need when working with egui. /// This is the first thing you need when working with egui.
/// Contains the [`InputState`], [`Memory`], [`Output`], and more. /// Contains the [`InputState`], [`Memory`], [`PlatformOutput`], and more.
/// ///
/// [`Context`] is cheap to clone, and any clones refers to the same mutable data /// [`Context`] is cheap to clone, and any clones refers to the same mutable data
/// ([`Context`] uses refcounting internally). /// ([`Context`] uses refcounting internally).
@ -121,14 +121,14 @@ impl ContextImpl {
/// # Example: /// # Example:
/// ///
/// ``` no_run /// ``` no_run
/// # fn handle_output(_: egui::Output) {} /// # fn handle_platform_output(_: egui::PlatformOutput) {}
/// # fn paint(_: Vec<egui::ClippedMesh>) {} /// # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedMesh>) {}
/// let mut ctx = egui::Context::default(); /// let mut ctx = egui::Context::default();
/// ///
/// // Game loop: /// // Game loop:
/// loop { /// loop {
/// let raw_input = egui::RawInput::default(); /// let raw_input = egui::RawInput::default();
/// let (output, shapes) = ctx.run(raw_input, |ctx| { /// let full_output = ctx.run(raw_input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| { /// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello world!"); /// ui.label("Hello world!");
/// if ui.button("Click me").clicked() { /// if ui.button("Click me").clicked() {
@ -136,9 +136,9 @@ impl ContextImpl {
/// } /// }
/// }); /// });
/// }); /// });
/// let clipped_meshes = ctx.tessellate(shapes); // create triangles to paint /// handle_platform_output(full_output.platform_output);
/// handle_output(output); /// let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint
/// paint(clipped_meshes); /// paint(full_output.textures_delta, clipped_meshes);
/// } /// }
/// ``` /// ```
#[derive(Clone)] #[derive(Clone)]
@ -185,19 +185,15 @@ impl Context {
/// ///
/// // Each frame: /// // Each frame:
/// let input = egui::RawInput::default(); /// let input = egui::RawInput::default();
/// let (output, shapes) = ctx.run(input, |ctx| { /// let full_output = ctx.run(input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| { /// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello egui!"); /// ui.label("Hello egui!");
/// }); /// });
/// }); /// });
/// // handle output, paint shapes /// // handle full_output
/// ``` /// ```
#[must_use] #[must_use]
pub fn run( pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Context)) -> FullOutput {
&self,
new_input: RawInput,
run_ui: impl FnOnce(&Context),
) -> (Output, Vec<ClippedShape>) {
self.begin_frame(new_input); self.begin_frame(new_input);
run_ui(self); run_ui(self);
self.end_frame() self.end_frame()
@ -217,8 +213,8 @@ impl Context {
/// ui.label("Hello egui!"); /// ui.label("Hello egui!");
/// }); /// });
/// ///
/// let (output, shapes) = ctx.end_frame(); /// let full_output = ctx.end_frame();
/// // handle output, paint shapes /// // handle full_output
/// ``` /// ```
pub fn begin_frame(&self, new_input: RawInput) { pub fn begin_frame(&self, new_input: RawInput) {
self.write().begin_frame_mut(new_input); self.write().begin_frame_mut(new_input);
@ -463,8 +459,13 @@ impl Context {
} }
/// What egui outputs each frame. /// What egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output().cursor_icon = egui::CursorIcon::Progress;
/// ```
#[inline] #[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, Output> { pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
RwLockWriteGuard::map(self.write(), |c| &mut c.output) RwLockWriteGuard::map(self.write(), |c| &mut c.output)
} }
@ -719,14 +720,13 @@ impl Context {
impl Context { impl Context {
/// Call at the end of each frame. /// Call at the end of each frame.
/// Returns what has happened this frame [`crate::Output`] as well as what you need to paint.
/// You can transform the returned shapes into triangles with a call to [`Context::tessellate`].
#[must_use] #[must_use]
pub fn end_frame(&self) -> (Output, Vec<ClippedShape>) { pub fn end_frame(&self) -> FullOutput {
if self.input().wants_repaint() { if self.input().wants_repaint() {
self.request_repaint(); self.request_repaint();
} }
let textures_delta;
{ {
let ctx_impl = &mut *self.write(); let ctx_impl = &mut *self.write();
ctx_impl ctx_impl
@ -742,20 +742,26 @@ impl Context {
.set(TextureId::default(), font_image_delta); .set(TextureId::default(), font_image_delta);
} }
ctx_impl textures_delta = ctx_impl.tex_manager.0.write().take_delta();
.output };
.textures_delta
.append(ctx_impl.tex_manager.0.write().take_delta());
}
let mut output: Output = std::mem::take(&mut self.output()); let platform_output: PlatformOutput = std::mem::take(&mut self.output());
if self.read().repaint_requests > 0 {
let needs_repaint = if self.read().repaint_requests > 0 {
self.write().repaint_requests -= 1; self.write().repaint_requests -= 1;
output.needs_repaint = true; true
} } else {
false
};
let shapes = self.drain_paint_lists(); let shapes = self.drain_paint_lists();
(output, shapes)
FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
}
} }
fn drain_paint_lists(&self) -> Vec<ClippedShape> { fn drain_paint_lists(&self) -> Vec<ClippedShape> {
@ -889,6 +895,17 @@ impl Context {
self.memory().layer_id_at(pos, resize_grab_radius_side) self.memory().layer_id_at(pos, resize_grab_radius_side)
} }
/// The overall top-most layer. When an area is clicked on or interacted
/// with, it is moved above all other areas.
pub fn top_most_layer(&self) -> Option<LayerId> {
self.memory().top_most_layer()
}
/// Moves the given area to the top.
pub fn move_to_top(&self, layer_id: LayerId) {
self.memory().areas.move_to_top(layer_id);
}
pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
let pointer_pos = self.input().pointer.interact_pos(); let pointer_pos = self.input().pointer.interact_pos();
if let Some(pointer_pos) = pointer_pos { if let Some(pointer_pos) = pointer_pos {

View file

@ -33,7 +33,7 @@ pub struct RawInput {
/// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`.
/// ///
/// The default is a very small (but very portable) 2048. /// The default is a very small (but very portable) 2048.
pub max_texture_side: usize, pub max_texture_side: Option<usize>,
/// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations.
/// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds).
@ -69,7 +69,7 @@ impl Default for RawInput {
Self { Self {
screen_rect: None, screen_rect: None,
pixels_per_point: None, pixels_per_point: None,
max_texture_side: 2048, max_texture_side: None,
time: None, time: None,
predicted_dt: 1.0 / 60.0, predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(), modifiers: Modifiers::default(),
@ -89,7 +89,7 @@ impl RawInput {
RawInput { RawInput {
screen_rect: self.screen_rect.take(), screen_rect: self.screen_rect.take(),
pixels_per_point: self.pixels_per_point.take(), pixels_per_point: self.pixels_per_point.take(),
max_texture_side: self.max_texture_side, max_texture_side: self.max_texture_side.take(),
time: self.time.take(), time: self.time.take(),
predicted_dt: self.predicted_dt, predicted_dt: self.predicted_dt,
modifiers: self.modifiers, modifiers: self.modifiers,
@ -115,7 +115,7 @@ impl RawInput {
self.screen_rect = screen_rect.or(self.screen_rect); self.screen_rect = screen_rect.or(self.screen_rect);
self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); self.pixels_per_point = pixels_per_point.or(self.pixels_per_point);
self.max_texture_side = max_texture_side; // use latest self.max_texture_side = max_texture_side.or(self.max_texture_side);
self.time = time; // use latest time self.time = time; // use latest time
self.predicted_dt = predicted_dt; // use latest dt self.predicted_dt = predicted_dt; // use latest dt
self.modifiers = modifiers; // use latest self.modifiers = modifiers; // use latest
@ -165,18 +165,27 @@ pub enum Event {
/// ///
/// When the user presses enter/return, do not send a `Text` (just [`Key::Enter`]). /// When the user presses enter/return, do not send a `Text` (just [`Key::Enter`]).
Text(String), Text(String),
/// A key was pressed or released.
Key { Key {
key: Key, key: Key,
/// Was it pressed or released?
pressed: bool, pressed: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers, modifiers: Modifiers,
}, },
/// The mouse or touch moved to a new place.
PointerMoved(Pos2), PointerMoved(Pos2),
/// A mouse button was pressed or released (or a touch started or stopped).
PointerButton { PointerButton {
/// Where is the pointer?
pos: Pos2, pos: Pos2,
/// What mouse button? For touches, use [`PointerButton::Primary`].
button: PointerButton, button: PointerButton,
/// Was it the button/touch pressed this frame, or released?
pressed: bool, pressed: 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,
}, },
/// The mouse left the screen, or the last/primary touch input disappeared. /// The mouse left the screen, or the last/primary touch input disappeared.
@ -217,6 +226,7 @@ pub enum Event {
/// Unique identifier of a finger/pen. Value is stable from touch down /// Unique identifier of a finger/pen. Value is stable from touch down
/// to lift-up /// to lift-up
id: TouchId, id: TouchId,
/// One of: start move end cancel.
phase: TouchPhase, phase: TouchPhase,
/// Position of the touch (or where the touch was last detected) /// Position of the touch (or where the touch was last detected)
pos: Pos2, pos: Pos2,
@ -244,18 +254,24 @@ pub enum PointerButton {
pub const NUM_POINTER_BUTTONS: usize = 3; pub const NUM_POINTER_BUTTONS: usize = 3;
/// State of the modifier keys. These must be fed to egui. /// State of the modifier keys. These must be fed to egui.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] ///
/// The best way to compare `Modifiers` is by using [`Modifiers::matches`].
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[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).
pub alt: bool, pub alt: bool,
/// Either of the control keys are down. /// Either of the control keys are down.
/// When checking for keyboard shortcuts, consider using [`Self::command`] instead. /// When checking for keyboard shortcuts, consider using [`Self::command`] instead.
pub ctrl: bool, pub ctrl: bool,
/// Either of the shift keys are down. /// Either of the shift keys are down.
pub shift: bool, pub shift: bool,
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms. /// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
pub mac_cmd: bool, pub mac_cmd: bool,
/// On Windows and Linux, set this to the same value as `ctrl`. /// On Windows and Linux, set this to the same value as `ctrl`.
/// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`).
/// This is so that egui can, for instance, select all text by checking for `command + A` /// This is so that egui can, for instance, select all text by checking for `command + A`
@ -264,6 +280,56 @@ pub struct Modifiers {
} }
impl Modifiers { impl Modifiers {
pub fn new() -> Self {
Default::default()
}
pub const NONE: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: false,
};
pub const ALT: Self = Self {
alt: true,
ctrl: false,
shift: false,
mac_cmd: false,
command: false,
};
pub const CTRL: Self = Self {
alt: false,
ctrl: true,
shift: false,
mac_cmd: false,
command: false,
};
pub const SHIFT: Self = Self {
alt: false,
ctrl: false,
shift: true,
mac_cmd: false,
command: false,
};
/// The Mac ⌘ Command key
pub const MAC_CMD: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: true,
command: false,
};
/// On Mac: ⌘ Command key, elsewhere: Ctrl key
pub const COMMAND: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: true,
};
#[inline(always)] #[inline(always)]
pub fn is_none(&self) -> bool { pub fn is_none(&self) -> bool {
self == &Self::default() self == &Self::default()
@ -274,6 +340,7 @@ impl Modifiers {
!self.is_none() !self.is_none()
} }
/// Is shift the only pressed button?
#[inline(always)] #[inline(always)]
pub fn shift_only(&self) -> bool { pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command) self.shift && !(self.alt || self.command)
@ -284,6 +351,66 @@ impl Modifiers {
pub fn command_only(&self) -> bool { pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command !self.alt && !self.shift && self.command
} }
/// Check for equality but with proper handling of [`Self::command`].
///
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// ```
pub fn matches(&self, pattern: Modifiers) -> bool {
// alt and shift must always match the pattern:
if pattern.alt != self.alt || pattern.shift != self.shift {
return false;
}
if pattern.mac_cmd {
// Mac-specific match:
if !self.mac_cmd {
return false;
}
if pattern.ctrl != self.ctrl {
return false;
}
return true;
}
if !pattern.ctrl && !pattern.command {
// the pattern explicitly doesn't want any ctrl/command:
return !self.ctrl && !self.command;
}
// if the pattern is looking for command, then `ctrl` may or may not be set depending on platform.
// if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform.
if pattern.ctrl && !self.ctrl {
return false;
}
if pattern.command && !self.command {
return false;
}
true
}
}
impl std::ops::BitOr for Modifiers {
type Output = Self;
fn bitor(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,
}
}
} }
/// Keyboard keys. /// Keyboard keys.
@ -382,7 +509,7 @@ impl RawInput {
.on_hover_text( .on_hover_text(
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
); );
ui.label(format!("max_texture_side: {}", max_texture_side)); ui.label(format!("max_texture_side: {:?}", max_texture_side));
if let Some(time) = time { if let Some(time) = time {
ui.label(format!("time: {:.3} s", time)); ui.label(format!("time: {:.3} s", time));
} else { } else {

View file

@ -2,11 +2,56 @@
use crate::WidgetType; use crate::WidgetType;
/// What egui emits each frame. /// What egui emits each frame from [`crate::Context::run`].
///
/// The backend should use this.
#[derive(Clone, Default, PartialEq)]
pub struct FullOutput {
/// Non-rendering related output.
pub platform_output: PlatformOutput,
/// If `true`, egui is requesting immediate repaint (i.e. on the next frame).
///
/// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`.
pub needs_repaint: bool,
/// Texture changes since last frame (including the font texture).
///
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting,
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
pub textures_delta: epaint::textures::TexturesDelta,
/// What to paint.
///
/// You can use [`crate::Context::tessellate`] to turn this into triangles.
pub shapes: Vec<epaint::ClippedShape>,
}
impl FullOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
let Self {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = newer;
self.platform_output.append(platform_output);
self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint
self.textures_delta.append(textures_delta);
self.shapes = shapes; // Only paint the latest
}
}
/// The non-rendering part of what egui emits each frame.
///
/// You can access (and modify) this with [`crate::Context::output`].
///
/// The backend should use this. /// The backend should use this.
#[derive(Clone, Default, PartialEq)] #[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Output { pub struct PlatformOutput {
/// Set the cursor to this icon. /// Set the cursor to this icon.
pub cursor_icon: CursorIcon, pub cursor_icon: CursorIcon,
@ -18,14 +63,6 @@ pub struct Output {
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`]. /// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
pub copied_text: String, pub copied_text: String,
/// If `true`, egui is requesting immediate repaint (i.e. on the next frame).
///
/// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`.
///
/// As an egui user: don't set this value directly.
/// Call `Context::request_repaint()` instead and it will do so for you.
pub needs_repaint: bool,
/// Events that may be useful to e.g. a screen reader. /// Events that may be useful to e.g. a screen reader.
pub events: Vec<OutputEvent>, pub events: Vec<OutputEvent>,
@ -35,12 +72,9 @@ pub struct Output {
/// 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>,
/// Texture changes since last frame.
pub textures_delta: epaint::textures::TexturesDelta,
} }
impl Output { impl PlatformOutput {
/// Open the given url in a web browser. /// Open the given url in a web browser.
/// If egui is running in a browser, the same tab will be reused. /// If egui is running in a browser, the same tab will be reused.
pub fn open_url(&mut self, url: impl ToString) { pub fn open_url(&mut self, url: impl ToString) {
@ -70,11 +104,9 @@ impl Output {
cursor_icon, cursor_icon,
open_url, open_url,
copied_text, copied_text,
needs_repaint,
mut events, mut events,
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
textures_delta,
} = newer; } = newer;
self.cursor_icon = cursor_icon; self.cursor_icon = cursor_icon;
@ -84,11 +116,9 @@ impl Output {
if !copied_text.is_empty() { if !copied_text.is_empty() {
self.copied_text = copied_text; self.copied_text = copied_text;
} }
self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint
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);
self.textures_delta.append(textures_delta);
} }
/// Take everything ephemeral (everything except `cursor_icon` currently) /// Take everything ephemeral (everything except `cursor_icon` currently)
@ -129,7 +159,7 @@ impl OpenUrl {
/// A mouse cursor icon. /// A mouse cursor icon.
/// ///
/// egui emits a [`CursorIcon`] in [`Output`] 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)]

View file

@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use crate::*; use crate::*;
/// State that is collected during a frame and then cleared. /// State that is collected during a frame and then cleared.
@ -28,7 +30,7 @@ pub(crate) struct FrameState {
/// Cleared by the first `ScrollArea` that makes use of it. /// Cleared by the first `ScrollArea` that makes use of it.
pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ? pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
/// horizontal, vertical /// horizontal, vertical
pub(crate) scroll_target: [Option<(f32, Align)>; 2], pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
} }
impl Default for FrameState { impl Default for FrameState {
@ -40,7 +42,7 @@ impl Default for FrameState {
used_by_panels: Rect::NAN, used_by_panels: Rect::NAN,
tooltip_rect: None, tooltip_rect: None,
scroll_delta: Vec2::ZERO, scroll_delta: Vec2::ZERO,
scroll_target: [None; 2], scroll_target: [None, None],
} }
} }
} }
@ -63,7 +65,7 @@ impl FrameState {
*used_by_panels = Rect::NOTHING; *used_by_panels = Rect::NOTHING;
*tooltip_rect = None; *tooltip_rect = None;
*scroll_delta = input.scroll_delta; *scroll_delta = input.scroll_delta;
*scroll_target = [None; 2]; *scroll_target = [None, None];
} }
/// How much space is still available after panels has been added. /// How much space is still available after panels has been added.

View file

@ -49,6 +49,11 @@ pub struct InputState {
/// Also known as device pixel ratio, > 1 for high resolution screens. /// Also known as device pixel ratio, > 1 for high resolution screens.
pub pixels_per_point: f32, pub pixels_per_point: f32,
/// Maximum size of one side of a texture.
///
/// This depends on the backend.
pub max_texture_side: usize,
/// Time in seconds. Relative to whatever. Used for animation. /// Time in seconds. Relative to whatever. Used for animation.
pub time: f64, pub time: f64,
@ -82,6 +87,7 @@ impl Default for InputState {
zoom_factor_delta: 1.0, zoom_factor_delta: 1.0,
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
pixels_per_point: 1.0, pixels_per_point: 1.0,
max_texture_side: 2048,
time: 0.0, time: 0.0,
unstable_dt: 1.0 / 6.0, unstable_dt: 1.0 / 6.0,
predicted_dt: 1.0 / 6.0, predicted_dt: 1.0 / 6.0,
@ -134,6 +140,7 @@ impl InputState {
zoom_factor_delta, zoom_factor_delta,
screen_rect, screen_rect,
pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point),
max_texture_side: new.max_texture_side.unwrap_or(self.max_texture_side),
time, time,
unstable_dt, unstable_dt,
predicted_dt: new.predicted_dt, predicted_dt: new.predicted_dt,
@ -192,6 +199,28 @@ 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.
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
let mut match_found = false;
self.events.retain(|event| {
let is_match = matches!(
event,
Event::Key {
key: ev_key,
modifiers: ev_mods,
pressed: true
} if *ev_key == key && ev_mods.matches(modifiers)
);
match_found |= is_match;
!is_match
});
match_found
}
/// Was the given key pressed this frame? /// Was the given key pressed this frame?
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
@ -692,6 +721,7 @@ impl InputState {
zoom_factor_delta, zoom_factor_delta,
screen_rect, screen_rect,
pixels_per_point, pixels_per_point,
max_texture_side,
time, time,
unstable_dt, unstable_dt,
predicted_dt, predicted_dt,
@ -724,9 +754,13 @@ impl InputState {
ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta)); ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta));
ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!( ui.label(format!(
"{:?} physical pixels for each logical point", "{} physical pixels for each logical point",
pixels_per_point pixels_per_point
)); ));
ui.label(format!(
"max texture size (on each side): {}",
max_texture_side
));
ui.label(format!("time: {:.3} s", time)); ui.label(format!("time: {:.3} s", time));
ui.label(format!( ui.label(format!(
"time since previous frame: {:.1} ms", "time since previous frame: {:.1} ms",

View file

@ -146,7 +146,7 @@ impl Widget for &mut epaint::TessellationOptions {
epsilon: _, epsilon: _,
} = self; } = self;
ui.checkbox(anti_alias, "Antialias") ui.checkbox(anti_alias, "Antialias")
.on_hover_text("Turn off for small performance gain."); .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain.");
ui.add( ui.add(
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0) crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
.logarithmic(true) .logarithmic(true)

View file

@ -23,6 +23,7 @@ pub enum Order {
/// Debug layer, always painted last / on top /// Debug layer, always painted last / on top
Debug, Debug,
} }
impl Order { impl Order {
const COUNT: usize = 6; const COUNT: usize = 6;
const ALL: [Order; Self::COUNT] = [ const ALL: [Order; Self::COUNT] = [

View file

@ -110,16 +110,16 @@
//! To write your own integration for egui you need to do this: //! To write your own integration for egui you need to do this:
//! //!
//! ``` no_run //! ``` no_run
//! # fn handle_output(_: egui::Output) {} //! # fn handle_platform_output(_: egui::PlatformOutput) {}
//! # fn paint(_: Vec<egui::ClippedMesh>) {}
//! # fn gather_input() -> egui::RawInput { egui::RawInput::default() } //! # fn gather_input() -> egui::RawInput { egui::RawInput::default() }
//! # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedMesh>) {}
//! let mut ctx = egui::Context::default(); //! let mut ctx = egui::Context::default();
//! //!
//! // Game loop: //! // Game loop:
//! loop { //! loop {
//! let raw_input: egui::RawInput = gather_input(); //! let raw_input: egui::RawInput = gather_input();
//! //!
//! let (output, shapes) = ctx.run(raw_input, |ctx| { //! let full_output = ctx.run(raw_input, |ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| { //! egui::CentralPanel::default().show(&ctx, |ui| {
//! ui.label("Hello world!"); //! ui.label("Hello world!");
//! if ui.button("Click me").clicked() { //! if ui.button("Click me").clicked() {
@ -127,10 +127,9 @@
//! } //! }
//! }); //! });
//! }); //! });
//! //! handle_platform_output(full_output.platform_output);
//! let clipped_meshes = ctx.tessellate(shapes); // create triangles to paint //! let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint
//! handle_output(output); //! paint(full_output.textures_delta, clipped_meshes);
//! paint(clipped_meshes);
//! } //! }
//! ``` //! ```
//! //!
@ -385,7 +384,7 @@ pub use epaint::emath;
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2}; pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
pub use epaint::{ pub use epaint::{
color, mutex, color, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId}, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::TexturesDelta, textures::TexturesDelta,
AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Mesh, Rgba, Rounding, Shape, Stroke, AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Mesh, Rgba, Rounding, Shape, Stroke,
TextureHandle, TextureId, TextureHandle, TextureId,
@ -403,7 +402,7 @@ pub use {
context::Context, context::Context,
data::{ data::{
input::*, input::*,
output::{self, CursorIcon, Output, WidgetInfo}, output::{self, CursorIcon, FullOutput, PlatformOutput, WidgetInfo},
}, },
grid::Grid, grid::Grid,
id::{Id, IdMap}, id::{Id, IdMap},

View file

@ -105,7 +105,7 @@ pub struct Options {
pub tessellation_options: epaint::TessellationOptions, pub tessellation_options: epaint::TessellationOptions,
/// This does not at all change the behavior of egui, /// This does not at all change the behavior of egui,
/// but is a signal to any backend that we want the [`crate::Output::events`] read out loud. /// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
/// 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.
pub screen_reader: bool, pub screen_reader: bool,
@ -329,6 +329,12 @@ impl Memory {
self.areas.layer_id_at(pos, resize_interact_radius_side) self.areas.layer_id_at(pos, resize_interact_radius_side)
} }
/// The overall top-most layer. When an area is clicked on or interacted
/// with, it is moved above all other areas.
pub fn top_most_layer(&self) -> Option<LayerId> {
self.areas.order().last().copied()
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.interaction.focus.id_previous_frame == Some(id) self.interaction.focus.id_previous_frame == Some(id)
} }
@ -516,9 +522,9 @@ impl Areas {
if state.interactable { if state.interactable {
// Allow us to resize by dragging just outside the window: // Allow us to resize by dragging just outside the window:
rect = rect.expand(resize_interact_radius_side); rect = rect.expand(resize_interact_radius_side);
} if rect.contains(pos) {
if rect.contains(pos) { return Some(*layer);
return Some(*layer); }
} }
} }
} }

View file

@ -126,7 +126,7 @@ 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)
.fixed_pos(pos) .fixed_pos(pos)
.interactable(false) .interactable(true)
.drag_bounds(Rect::EVERYTHING); .drag_bounds(Rect::EVERYTHING);
let inner_response = area.show(ctx, |ui| { let inner_response = area.show(ctx, |ui| {
ui.scope(|ui| { ui.scope(|ui| {

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
emath::{lerp, Align, Pos2, Rect, Vec2}, emath::{Align, Pos2, Rect, Vec2},
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
NUM_POINTER_BUTTONS, NUM_POINTER_BUTTONS,
}; };
@ -443,7 +443,11 @@ impl Response {
) )
} }
/// Move the scroll to this UI with the specified alignment. /// Adjust the scroll position until this UI becomes visible.
///
/// If `align` is `None`, it'll scroll enough to bring the UI into view.
///
/// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`].
/// ///
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
@ -451,18 +455,15 @@ impl Response {
/// for i in 0..1000 { /// for i in 0..1000 {
/// let response = ui.button("Scroll to me"); /// let response = ui.button("Scroll to me");
/// if response.clicked() { /// if response.clicked() {
/// response.scroll_to_me(egui::Align::Center); /// response.scroll_to_me(Some(egui::Align::Center));
/// } /// }
/// } /// }
/// }); /// });
/// # }); /// # });
/// ``` /// ```
pub fn scroll_to_me(&self, align: Align) { pub fn scroll_to_me(&self, align: Option<Align>) {
let scroll_target = lerp(self.rect.x_range(), align.to_factor()); self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
self.ctx.frame_state().scroll_target[0] = Some((scroll_target, align)); self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
let scroll_target = lerp(self.rect.y_range(), align.to_factor());
self.ctx.frame_state().scroll_target[1] = Some((scroll_target, align));
} }
/// For accessibility. /// For accessibility.
@ -509,6 +510,8 @@ impl Response {
impl Response { impl Response {
/// A logical "or" operation. /// A logical "or" operation.
/// For instance `a.union(b).hovered` means "was either a or b hovered?". /// For instance `a.union(b).hovered` means "was either a or b hovered?".
///
/// The resulting [`Self::id`] will come from the first (`self`) argument.
pub fn union(&self, other: Self) -> Self { pub fn union(&self, other: Self) -> Self {
assert!(self.ctx == other.ctx); assert!(self.ctx == other.ctx);
crate::egui_assert!( crate::egui_assert!(

View file

@ -2,7 +2,7 @@
#![allow(clippy::if_same_then_else)] #![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, FontFamily, FontId, Margin, Response, RichText, WidgetText}; use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
use epaint::{mutex::Arc, Rounding, Shadow, Stroke}; use epaint::{mutex::Arc, Rounding, Shadow, Stroke};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -277,6 +277,49 @@ impl Spacing {
} }
} }
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Margin {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl Margin {
#[inline]
pub fn same(margin: f32) -> Self {
Self {
left: margin,
right: margin,
top: margin,
bottom: margin,
}
}
/// Margins with the same size on opposing sides
#[inline]
pub fn symmetric(x: f32, y: f32) -> Self {
Self {
left: x,
right: x,
top: y,
bottom: y,
}
}
/// Total margins on both sides
pub fn sum(&self) -> Vec2 {
Vec2::new(self.left + self.right, self.top + self.bottom)
}
}
impl From<Vec2> for Margin {
fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y)
}
}
/// How and when interaction happens. /// How and when interaction happens.
#[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))]
@ -564,7 +607,7 @@ impl Visuals {
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(24), faint_bg_color: Color32::from_gray(24),
extreme_bg_color: Color32::from_gray(10), 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),
window_rounding: Rounding::same(6.0), window_rounding: Rounding::same(6.0),
window_shadow: Shadow::big_dark(), window_shadow: Shadow::big_dark(),
@ -585,9 +628,9 @@ 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(240), faint_bg_color: Color32::from_gray(245),
extreme_bg_color: Color32::from_gray(250), extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
code_bg_color: Color32::from_gray(200), code_bg_color: Color32::from_gray(230),
window_shadow: Shadow::big_light(), window_shadow: Shadow::big_light(),
popup_shadow: Shadow::small_light(), popup_shadow: Shadow::small_light(),
..Self::dark() ..Self::dark()
@ -666,21 +709,21 @@ impl Widgets {
pub fn light() -> Self { pub fn light() -> Self {
Self { Self {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
bg_fill: Color32::from_gray(235), // window background bg_fill: Color32::from_gray(248), // window background - should be distinct from TextEdit background
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines
fg_stroke: Stroke::new(1.0, Color32::from_gray(100)), // 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(215), // button background bg_fill: Color32::from_gray(230), // button background
bg_stroke: Default::default(), bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // 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 {
bg_fill: Color32::from_gray(210), 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),
rounding: Rounding::same(3.0), rounding: Rounding::same(3.0),

View file

@ -338,6 +338,21 @@ impl Ui {
self.ctx().input() 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. /// The [`Memory`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().memory()`. /// Equivalent to `.ctx().memory()`.
#[inline] #[inline]
@ -351,10 +366,10 @@ impl Ui {
self.ctx().data() self.ctx().data()
} }
/// The [`Output`] of the [`Context`] associated with this ui. /// The [`PlatformOutput`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().output()`. /// Equivalent to `.ctx().output()`.
#[inline] #[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, Output> { pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
self.ctx().output() self.ctx().output()
} }
@ -889,7 +904,36 @@ impl Ui {
(response, painter) (response, painter)
} }
/// Move the scroll to this cursor position with the specified alignment. /// Adjust the scroll position of any parent [`ScrollArea`] so that the given `Rect` becomes visible.
///
/// If `align` is `None`, it'll scroll enough to bring the cursor into view.
///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`].
///
/// ```
/// # use egui::Align;
/// # egui::__run_test_ui(|ui| {
/// egui::ScrollArea::vertical().show(ui, |ui| {
/// // …
/// let response = ui.button("Center on me.");
/// if response.clicked() {
/// ui.scroll_to_rect(response.rect, Some(Align::Center));
/// }
/// });
/// # });
/// ```
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 {
let range = rect.min[d]..=rect.max[d];
self.ctx().frame_state().scroll_target[d] = Some((range, align));
}
}
/// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible.
///
/// If `align` is not provided, it'll scroll enough to bring the cursor into view.
///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`].
/// ///
/// ``` /// ```
/// # use egui::Align; /// # use egui::Align;
@ -901,15 +945,16 @@ impl Ui {
/// } /// }
/// ///
/// if scroll_bottom { /// if scroll_bottom {
/// ui.scroll_to_cursor(Align::BOTTOM); /// ui.scroll_to_cursor(Some(Align::BOTTOM));
/// } /// }
/// }); /// });
/// # }); /// # });
/// ``` /// ```
pub fn scroll_to_cursor(&mut self, align: Align) { pub fn scroll_to_cursor(&self, align: Option<Align>) {
let target = self.next_widget_position(); let target = self.next_widget_position();
for d in 0..2 { for d in 0..2 {
self.ctx().frame_state().scroll_target[d] = Some((target[d], align)); let target = target[d];
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
} }
} }
} }
@ -969,7 +1014,7 @@ impl Ui {
.inner .inner
} }
/// Add a single[`Widget`] that is possibly disabled, i.e. greyed out and non-interactive. /// Add a single [`Widget`] that is possibly disabled, i.e. greyed out and non-interactive.
/// ///
/// If you call `add_enabled` from within an already disabled `Ui`, /// If you call `add_enabled` from within an already disabled `Ui`,
/// the widget will always be disabled, even if the `enabled` argument is true. /// the widget will always be disabled, even if the `enabled` argument is true.
@ -1024,7 +1069,7 @@ impl Ui {
}) })
} }
/// Add a single[`Widget`] that is possibly invisible. /// Add a single [`Widget`] that is possibly invisible.
/// ///
/// An invisible widget still takes up the same space as if it were visible. /// An invisible widget still takes up the same space as if it were visible.
/// ///

View file

@ -51,6 +51,13 @@ impl From<&String> for RichText {
} }
} }
impl From<&mut String> for RichText {
#[inline]
fn from(text: &mut String) -> Self {
RichText::new(text.clone())
}
}
impl From<String> for RichText { impl From<String> for RichText {
#[inline] #[inline]
fn from(text: String) -> Self { fn from(text: String) -> Self {

View file

@ -145,9 +145,9 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
let picked_color = color_at(*value); let picked_color = color_at(*value);
ui.painter().add(Shape::convex_polygon( ui.painter().add(Shape::convex_polygon(
vec![ vec![
pos2(x - r, rect.bottom()), pos2(x, rect.center().y), // tip
pos2(x + r, rect.bottom()), pos2(x + r, rect.bottom()), // right bottom
pos2(x, rect.center().y), pos2(x - r, rect.bottom()), // left bottom
], ],
picked_color, picked_color,
Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)), Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
@ -357,7 +357,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
if ui.memory().is_popup_open(popup_id) { if ui.memory().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)
.default_pos(button_response.rect.max) .current_pos(button_response.rect.max)
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = 210.0; ui.spacing_mut().slider_width = 210.0;
Frame::popup(ui.style()).show(ui, |ui| { Frame::popup(ui.style()).show(ui, |ui| {

View file

@ -180,7 +180,7 @@ impl<'a> Widget for DragValue<'a> {
emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals) emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals)
}; };
let kb_edit_id = ui.auto_id_with("edit"); let kb_edit_id = ui.next_auto_id();
let is_kb_editing = ui.memory().has_focus(kb_edit_id); let is_kb_editing = ui.memory().has_focus(kb_edit_id);
let mut response = if is_kb_editing { let mut response = if is_kb_editing {

View file

@ -7,7 +7,7 @@ use epaint::Mesh;
use crate::*; use crate::*;
use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform}; use super::{LabelFormatter, PlotBounds, ScreenTransform};
use rect_elem::*; use rect_elem::*;
use values::{ClosestElem, PlotGeometry}; use values::{ClosestElem, PlotGeometry};
@ -66,7 +66,7 @@ pub(super) trait PlotItem {
elem: ClosestElem, elem: ClosestElem,
shapes: &mut Vec<Shape>, shapes: &mut Vec<Shape>,
plot: &PlotConfig<'_>, plot: &PlotConfig<'_>,
custom_label_func: &CustomLabelFuncRef, label_formatter: &LabelFormatter,
) { ) {
let points = match self.geometry() { let points = match self.geometry() {
PlotGeometry::Points(points) => points, PlotGeometry::Points(points) => points,
@ -89,7 +89,7 @@ pub(super) trait PlotItem {
let pointer = plot.transform.position_from_value(&value); let pointer = plot.transform.position_from_value(&value);
shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); shapes.push(Shape::circle_filled(pointer, 3.0, line_color));
rulers_at_value(pointer, value, self.name(), plot, shapes, custom_label_func); rulers_at_value(pointer, value, self.name(), plot, shapes, label_formatter);
} }
} }
@ -613,6 +613,7 @@ impl PlotItem for Polygon {
} }
/// Text inside the plot. /// Text inside the plot.
#[derive(Clone)]
pub struct Text { pub struct Text {
pub(super) text: WidgetText, pub(super) text: WidgetText,
pub(super) position: Value, pub(super) position: Value,
@ -807,9 +808,9 @@ impl Points {
impl PlotItem for Points { impl PlotItem for Points {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let sqrt_3 = 3f32.sqrt(); let sqrt_3 = 3_f32.sqrt();
let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0;
let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt();
let Self { let Self {
series, series,
@ -861,15 +862,20 @@ impl PlotItem for Points {
})); }));
} }
MarkerShape::Diamond => { MarkerShape::Diamond => {
let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; let points = vec![
tf(0.0, 1.0), // bottom
tf(-1.0, 0.0), // left
tf(0.0, -1.0), // top
tf(1.0, 0.0), // right
];
shapes.push(Shape::convex_polygon(points, fill, stroke)); shapes.push(Shape::convex_polygon(points, fill, stroke));
} }
MarkerShape::Square => { MarkerShape::Square => {
let points = vec![ let points = vec![
tf(frac_1_sqrt_2, frac_1_sqrt_2),
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
tf(-frac_1_sqrt_2, frac_1_sqrt_2), tf(-frac_1_sqrt_2, frac_1_sqrt_2),
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
tf(frac_1_sqrt_2, frac_1_sqrt_2),
]; ];
shapes.push(Shape::convex_polygon(points, fill, stroke)); shapes.push(Shape::convex_polygon(points, fill, stroke));
} }
@ -893,7 +899,7 @@ impl PlotItem for Points {
} }
MarkerShape::Up => { MarkerShape::Up => {
let points = let points =
vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; vec![tf(0.0, -1.0), tf(0.5 * sqrt_3, 0.5), tf(-0.5 * sqrt_3, 0.5)];
shapes.push(Shape::convex_polygon(points, fill, stroke)); shapes.push(Shape::convex_polygon(points, fill, stroke));
} }
MarkerShape::Down => { MarkerShape::Down => {
@ -912,8 +918,8 @@ impl PlotItem for Points {
MarkerShape::Right => { MarkerShape::Right => {
let points = vec![ let points = vec![
tf(1.0, 0.0), tf(1.0, 0.0),
tf(-0.5, -0.5 * sqrt_3),
tf(-0.5, 0.5 * sqrt_3), tf(-0.5, 0.5 * sqrt_3),
tf(-0.5, -0.5 * sqrt_3),
]; ];
shapes.push(Shape::convex_polygon(points, fill, stroke)); shapes.push(Shape::convex_polygon(points, fill, stroke));
} }
@ -1074,6 +1080,7 @@ impl PlotItem for Arrows {
} }
/// An image in the plot. /// An image in the plot.
#[derive(Clone)]
pub struct PlotImage { pub struct PlotImage {
pub(super) position: Value, pub(super) position: Value,
pub(super) texture_id: TextureId, pub(super) texture_id: TextureId,
@ -1380,7 +1387,7 @@ impl PlotItem for BarChart {
elem: ClosestElem, elem: ClosestElem,
shapes: &mut Vec<Shape>, shapes: &mut Vec<Shape>,
plot: &PlotConfig<'_>, plot: &PlotConfig<'_>,
_: &CustomLabelFuncRef, _: &LabelFormatter,
) { ) {
let bar = &self.bars[elem.index]; let bar = &self.bars[elem.index];
@ -1522,7 +1529,7 @@ impl PlotItem for BoxPlot {
elem: ClosestElem, elem: ClosestElem,
shapes: &mut Vec<Shape>, shapes: &mut Vec<Shape>,
plot: &PlotConfig<'_>, plot: &PlotConfig<'_>,
_: &CustomLabelFuncRef, _: &LabelFormatter,
) { ) {
let box_plot = &self.boxes[elem.index]; let box_plot = &self.boxes[elem.index];
@ -1643,7 +1650,7 @@ pub(super) fn rulers_at_value(
name: &str, name: &str,
plot: &PlotConfig<'_>, plot: &PlotConfig<'_>,
shapes: &mut Vec<Shape>, shapes: &mut Vec<Shape>,
custom_label_func: &CustomLabelFuncRef, label_formatter: &LabelFormatter,
) { ) {
let line_color = rulers_color(plot.ui); let line_color = rulers_color(plot.ui);
if plot.show_x { if plot.show_x {
@ -1663,7 +1670,7 @@ pub(super) fn rulers_at_value(
let scale = plot.transform.dvalue_dpos(); let scale = plot.transform.dvalue_dpos();
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
if let Some(custom_label) = custom_label_func { if let Some(custom_label) = label_formatter {
custom_label(name, &value) custom_label(name, &value)
} else if plot.show_x && plot.show_y { } else if plot.show_x && plot.show_y {
format!( format!(

View file

@ -1,6 +1,6 @@
//! Simple plotting library. //! Simple plotting library.
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, ops::RangeInclusive, rc::Rc};
use crate::*; use crate::*;
use epaint::ahash::AHashSet; use epaint::ahash::AHashSet;
@ -20,12 +20,44 @@ mod items;
mod legend; mod legend;
mod transform; mod transform;
type CustomLabelFunc = dyn Fn(&str, &Value) -> String; type LabelFormatterFn = dyn Fn(&str, &Value) -> String;
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>; type LabelFormatter = Option<Box<LabelFormatterFn>>;
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
type AxisFormatterFn = dyn Fn(f64) -> String;
type AxisFormatter = Option<Box<AxisFormatterFn>>; type AxisFormatter = Option<Box<AxisFormatterFn>>;
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
pub struct CoordinatesFormatter {
function: Box<dyn Fn(&Value, &PlotBounds) -> String>,
}
impl CoordinatesFormatter {
/// Create a new formatter based on the pointer coordinate and the plot bounds.
pub fn new(function: impl Fn(&Value, &PlotBounds) -> String + 'static) -> Self {
Self {
function: Box::new(function),
}
}
/// Show a fixed number of decimal places.
pub fn with_decimals(num_decimals: usize) -> Self {
Self {
function: Box::new(move |value, _| {
format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
}),
}
}
fn format(&self, value: &Value, bounds: &PlotBounds) -> String {
(self.function)(value, bounds)
}
}
impl Default for CoordinatesFormatter {
fn default() -> Self {
Self::with_decimals(3)
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Information about the plot that has to persist between frames. /// Information about the plot that has to persist between frames.
@ -146,7 +178,8 @@ pub struct Plot {
show_x: bool, show_x: bool,
show_y: bool, show_y: bool,
custom_label_func: CustomLabelFuncRef, label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2], axis_formatters: [AxisFormatter; 2],
legend_config: Option<Legend>, legend_config: Option<Legend>,
show_background: bool, show_background: bool,
@ -177,7 +210,8 @@ impl Plot {
show_x: true, show_x: true,
show_y: true, show_y: true,
custom_label_func: None, label_formatter: None,
coordinates_formatter: None,
axis_formatters: [None, None], // [None; 2] requires Copy axis_formatters: [None, None], // [None; 2] requires Copy
legend_config: None, legend_config: None,
show_background: true, show_background: true,
@ -284,7 +318,7 @@ impl Plot {
/// }); /// });
/// let line = Line::new(Values::from_values_iter(sin)); /// let line = Line::new(Values::from_values_iter(sin));
/// Plot::new("my_plot").view_aspect(2.0) /// Plot::new("my_plot").view_aspect(2.0)
/// .custom_label_func(|name, value| { /// .label_formatter(|name, value| {
/// if !name.is_empty() { /// if !name.is_empty() {
/// format!("{}: {:.*}%", name, 1, value.y).to_string() /// format!("{}: {:.*}%", name, 1, value.y).to_string()
/// } else { /// } else {
@ -294,34 +328,50 @@ impl Plot {
/// .show(ui, |plot_ui| plot_ui.line(line)); /// .show(ui, |plot_ui| plot_ui.line(line));
/// # }); /// # });
/// ``` /// ```
pub fn custom_label_func( pub fn label_formatter(
mut self, mut self,
custom_label_func: impl Fn(&str, &Value) -> String + 'static, label_formatter: impl Fn(&str, &Value) -> String + 'static,
) -> Self { ) -> Self {
self.custom_label_func = Some(Box::new(custom_label_func)); self.label_formatter = Some(Box::new(label_formatter));
self self
} }
/// Provide a function to customize the labels for the X axis. /// Show the pointer coordinates in the plot.
pub fn coordinates_formatter(
mut self,
position: Corner,
formatter: CoordinatesFormatter,
) -> Self {
self.coordinates_formatter = Some((position, formatter));
self
}
/// Provide a function to customize the labels for the X axis based on the current visible value range.
/// ///
/// This is useful for custom input domains, e.g. date/time. /// This is useful for custom input domains, e.g. date/time.
/// ///
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution, /// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
/// the formatter function can return empty strings. This is also useful if your domain is /// the formatter function can return empty strings. This is also useful if your domain is
/// discrete (e.g. only full days in a calendar). /// discrete (e.g. only full days in a calendar).
pub fn x_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self { pub fn x_axis_formatter(
mut self,
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
self.axis_formatters[0] = Some(Box::new(func)); self.axis_formatters[0] = Some(Box::new(func));
self self
} }
/// Provide a function to customize the labels for the Y axis. /// Provide a function to customize the labels for the Y axis based on the current value range.
/// ///
/// This is useful for custom value representation, e.g. percentage or units. /// This is useful for custom value representation, e.g. percentage or units.
/// ///
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution, /// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
/// the formatter function can return empty strings. This is also useful if your Y values are /// the formatter function can return empty strings. This is also useful if your Y values are
/// discrete (e.g. only integers). /// discrete (e.g. only integers).
pub fn y_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self { pub fn y_axis_formatter(
mut self,
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
self.axis_formatters[1] = Some(Box::new(func)); self.axis_formatters[1] = Some(Box::new(func));
self self
} }
@ -388,7 +438,8 @@ impl Plot {
view_aspect, view_aspect,
mut show_x, mut show_x,
mut show_y, mut show_y,
custom_label_func, label_formatter,
coordinates_formatter,
axis_formatters, axis_formatters,
legend_config, legend_config,
show_background, show_background,
@ -630,7 +681,8 @@ impl Plot {
items, items,
show_x, show_x,
show_y, show_y,
custom_label_func, label_formatter,
coordinates_formatter,
axis_formatters, axis_formatters,
show_axes, show_axes,
transform: transform.clone(), transform: transform.clone(),
@ -849,7 +901,8 @@ struct PreparedPlot {
items: Vec<Box<dyn PlotItem>>, items: Vec<Box<dyn PlotItem>>,
show_x: bool, show_x: bool,
show_y: bool, show_y: bool,
custom_label_func: CustomLabelFuncRef, label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2], axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2], show_axes: [bool; 2],
transform: ScreenTransform, transform: ScreenTransform,
@ -877,7 +930,24 @@ impl PreparedPlot {
self.hover(ui, pointer, &mut shapes); self.hover(ui, pointer, &mut shapes);
} }
ui.painter().sub_region(*transform.frame()).extend(shapes); let painter = ui.painter().sub_region(*transform.frame());
painter.extend(shapes);
if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
if let Some(pointer) = response.hover_pos() {
let font_id = TextStyle::Monospace.resolve(ui.style());
let coordinate = transform.value_from_position(pointer);
let text = formatter.format(&coordinate, transform.bounds());
let padded_frame = transform.frame().shrink(4.0);
let (anchor, position) = match corner {
Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
};
painter.text(position, anchor, text, font_id, ui.visuals().text_color());
}
}
} }
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) { fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
@ -888,6 +958,11 @@ impl PreparedPlot {
} = self; } = self;
let bounds = transform.bounds(); let bounds = transform.bounds();
let axis_range = match axis {
0 => bounds.range_x(),
1 => bounds.range_y(),
_ => panic!("Axis {} does not exist.", axis),
};
let font_id = TextStyle::Body.resolve(ui.style()); let font_id = TextStyle::Body.resolve(ui.style());
@ -947,7 +1022,7 @@ impl PreparedPlot {
let color = color_from_alpha(ui, text_alpha); let color = color_from_alpha(ui, text_alpha);
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() { let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main) formatter(value_main, &axis_range)
} else { } else {
emath::round_to_decimals(value_main, 5).to_string() // hack emath::round_to_decimals(value_main, 5).to_string() // hack
}; };
@ -982,7 +1057,7 @@ impl PreparedPlot {
transform, transform,
show_x, show_x,
show_y, show_y,
custom_label_func, label_formatter,
items, items,
.. ..
} = self; } = self;
@ -1012,10 +1087,10 @@ impl PreparedPlot {
}; };
if let Some((item, elem)) = closest { if let Some((item, elem)) = closest {
item.on_hover(elem, shapes, &plot, custom_label_func); item.on_hover(elem, shapes, &plot, label_formatter);
} else { } else {
let value = transform.value_from_position(pointer); let value = transform.value_from_position(pointer);
items::rulers_at_value(pointer, value, "", &plot, shapes, custom_label_func); items::rulers_at_value(pointer, value, "", &plot, shapes, label_formatter);
} }
} }
} }

View file

@ -120,6 +120,10 @@ impl PlotBounds {
self.min[0]..=self.max[0] self.min[0]..=self.max[0]
} }
pub(crate) fn range_y(&self) -> RangeInclusive<f64> {
self.min[1]..=self.max[1]
}
pub(crate) fn make_x_symmetrical(&mut self) { pub(crate) fn make_x_symmetrical(&mut self) {
let x_abs = self.min[0].abs().max(self.max[0].abs()); let x_abs = self.min[0].abs().max(self.max[0].abs());
self.min[0] = -x_abs; self.min[0] = -x_abs;

View file

@ -71,6 +71,8 @@ pub struct Slider<'a> {
suffix: String, suffix: String,
text: String, text: String,
text_color: Option<Color32>, text_color: Option<Color32>,
/// Sets the minimal step of the widget value
step: Option<f64>,
min_decimals: usize, min_decimals: usize,
max_decimals: Option<usize>, max_decimals: Option<usize>,
} }
@ -113,6 +115,7 @@ impl<'a> Slider<'a> {
suffix: Default::default(), suffix: Default::default(),
text: Default::default(), text: Default::default(),
text_color: None, text_color: None,
step: None,
min_decimals: 0, min_decimals: 0,
max_decimals: None, max_decimals: None,
} }
@ -199,6 +202,16 @@ impl<'a> Slider<'a> {
self self
} }
/// Sets the minimal change of the value.
/// Value `0.0` effectively disables the feature. If the new value is out of range
/// and `clamp_to_range` is enabled, you would not have the ability to change the value.
///
/// Default: `0.0` (disabled).
pub fn step_by(mut self, step: f64) -> Self {
self.step = if step != 0.0 { Some(step) } else { None };
self
}
// TODO: we should also have a "min precision". // TODO: we should also have a "min precision".
/// Set a minimum number of decimals to display. /// Set a minimum number of decimals to display.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you. /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
@ -255,6 +268,9 @@ impl<'a> Slider<'a> {
if let Some(max_decimals) = self.max_decimals { if let Some(max_decimals) = self.max_decimals {
value = emath::round_to_decimals(value, max_decimals); value = emath::round_to_decimals(value, max_decimals);
} }
if let Some(step) = self.step {
value = (value / step).round() * step;
}
set(&mut self.get_set_value, value); set(&mut self.get_set_value, value);
} }
@ -284,10 +300,10 @@ impl<'a> Slider<'a> {
impl<'a> Slider<'a> { impl<'a> Slider<'a> {
/// Just the slider, no text /// Just the slider, no text
fn allocate_slider_space(&self, ui: &mut Ui, perpendicular: f32) -> Response { fn allocate_slider_space(&self, ui: &mut Ui, thickness: f32) -> Response {
let desired_size = match self.orientation { let desired_size = match self.orientation {
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, perpendicular), SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
SliderOrientation::Vertical => vec2(perpendicular, ui.spacing().slider_width), SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
}; };
ui.allocate_response(desired_size, Sense::click_and_drag()) ui.allocate_response(desired_size, Sense::click_and_drag())
} }
@ -330,14 +346,22 @@ impl<'a> Slider<'a> {
let prev_value = self.get_value(); let prev_value = self.get_value();
let prev_position = self.position_from_value(prev_value, position_range.clone()); let prev_position = self.position_from_value(prev_value, position_range.clone());
let new_position = prev_position + kb_step; let new_position = prev_position + kb_step;
let new_value = if self.smart_aim { let new_value = match self.step {
let aim_radius = ui.input().aim_radius(); Some(step) => prev_value + (kb_step as f64 * step),
emath::smart_aim::best_in_range_f64( None if self.smart_aim => {
self.value_from_position(new_position - aim_radius, position_range.clone()), let aim_radius = ui.input().aim_radius();
self.value_from_position(new_position + aim_radius, position_range.clone()), emath::smart_aim::best_in_range_f64(
) self.value_from_position(
} else { new_position - aim_radius,
self.value_from_position(new_position, position_range.clone()) position_range.clone(),
),
self.value_from_position(
new_position + aim_radius,
position_range.clone(),
),
)
}
_ => self.value_from_position(new_position, position_range.clone()),
}; };
self.set_value(new_value); self.set_value(new_value);
} }
@ -429,19 +453,20 @@ impl<'a> Slider<'a> {
} }
} }
fn label_ui(&mut self, ui: &mut Ui) { fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
if !self.text.is_empty() { // If `DragValue` is controlled from the keyboard and `step` is defined, set speed to `step`
let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); let change = ui.input().num_presses(Key::ArrowUp) as i32
let text = RichText::new(&self.text).color(text_color); + ui.input().num_presses(Key::ArrowRight) as i32
ui.add(Label::new(text).wrap(false)); - ui.input().num_presses(Key::ArrowDown) as i32
} - ui.input().num_presses(Key::ArrowLeft) as i32;
} let speed = match self.step {
Some(step) if change != 0 => step,
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) { _ => self.current_gradient(&position_range),
};
let mut value = self.get_value(); let mut value = self.get_value();
ui.add( let response = ui.add(
DragValue::new(&mut value) DragValue::new(&mut value)
.speed(self.current_gradient(&position_range)) .speed(speed)
.clamp_range(self.clamp_range()) .clamp_range(self.clamp_range())
.min_decimals(self.min_decimals) .min_decimals(self.min_decimals)
.max_decimals_opt(self.max_decimals) .max_decimals_opt(self.max_decimals)
@ -451,6 +476,7 @@ impl<'a> Slider<'a> {
if value != self.get_value() { if value != self.get_value() {
self.set_value(value); self.set_value(value);
} }
response
} }
/// delta(value) / delta(points) /// delta(value) / delta(points)
@ -466,21 +492,35 @@ impl<'a> Slider<'a> {
} }
fn add_contents(&mut self, ui: &mut Ui) -> Response { fn add_contents(&mut self, ui: &mut Ui) -> Response {
let perpendicular = ui let thickness = ui
.text_style_height(&TextStyle::Body) .text_style_height(&TextStyle::Body)
.at_least(ui.spacing().interact_size.y); .at_least(ui.spacing().interact_size.y);
let slider_response = self.allocate_slider_space(ui, perpendicular); let mut response = self.allocate_slider_space(ui, thickness);
self.slider_ui(ui, &slider_response); self.slider_ui(ui, &response);
if self.show_value { if self.show_value {
let position_range = self.position_range(&slider_response.rect); let position_range = self.position_range(&response.rect);
self.value_ui(ui, position_range); let value_response = self.value_ui(ui, position_range);
if value_response.gained_focus()
|| value_response.has_focus()
|| value_response.lost_focus()
{
// Use the `DragValue` id as the id of the whole widget,
// so that the focus events work as expected.
response = value_response.union(response);
} else {
// Use the slider id as the id for the whole widget
response = response.union(value_response);
}
} }
if !self.text.is_empty() { if !self.text.is_empty() {
self.label_ui(ui); let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color());
let text = RichText::new(&self.text).color(text_color);
ui.add(Label::new(text).wrap(false));
} }
slider_response
response
} }
} }

View file

@ -543,6 +543,14 @@ impl<'t> TextEdit<'t> {
text_draw_pos -= vec2(offset_x, 0.0); text_draw_pos -= vec2(offset_x, 0.0);
} }
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
(cursor_range, prev_cursor_range)
{
prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range()
} else {
false
};
if ui.is_rect_visible(rect) { if ui.is_rect_visible(rect) {
painter.galley(text_draw_pos, galley.clone()); painter.galley(text_draw_pos, galley.clone());
@ -561,7 +569,7 @@ impl<'t> TextEdit<'t> {
// We paint the cursor on top of the text, in case // We paint the cursor on top of the text, in case
// the text galley has backgrounds (as e.g. `code` snippets in markup do). // the text galley has backgrounds (as e.g. `code` snippets in markup do).
paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursor_range); paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursor_range);
paint_cursor_end( let cursor_pos = paint_cursor_end(
ui, ui,
row_height, row_height,
&painter, &painter,
@ -570,15 +578,14 @@ impl<'t> TextEdit<'t> {
&cursor_range.primary, &cursor_range.primary,
); );
if response.changed || selection_changed {
ui.scroll_to_rect(cursor_pos, None); // keep cursor in view
}
if interactive && text.is_mutable() { if interactive && text.is_mutable() {
// egui_web uses `text_cursor_pos` when showing IME, // egui_web uses `text_cursor_pos` when showing IME,
// so only set it when text is editable and visible! // so only set it when text is editable and visible!
ui.ctx().output().text_cursor_pos = Some( ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
galley
.pos_from_cursor(&cursor_range.primary)
.translate(response.rect.min.to_vec2())
.left_top(),
);
} }
} }
} }
@ -586,14 +593,6 @@ impl<'t> TextEdit<'t> {
state.clone().store(ui.ctx(), id); state.clone().store(ui.ctx(), id);
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
(cursor_range, prev_cursor_range)
{
prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range()
} else {
false
};
if response.changed { if response.changed {
response.widget_info(|| { response.widget_info(|| {
WidgetInfo::text_edit( WidgetInfo::text_edit(
@ -887,7 +886,7 @@ fn paint_cursor_end(
pos: Pos2, pos: Pos2,
galley: &Galley, galley: &Galley,
cursor: &Cursor, cursor: &Cursor,
) { ) -> Rect {
let stroke = ui.visuals().selection.stroke; let stroke = ui.visuals().selection.stroke;
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2()); let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
@ -915,6 +914,8 @@ fn paint_cursor_end(
(width, stroke.color), (width, stroke.color),
); );
} }
cursor_pos
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -1091,11 +1092,31 @@ fn on_key_press(
None None
} }
Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift =>
{
move_single_cursor(&mut cursor_range.primary, galley, key, modifiers);
cursor_range.secondary = cursor_range.primary;
None
}
_ => None, _ => None,
} }
} }
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) { fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift {
match key {
Key::A => *cursor = galley.cursor_begin_of_row(cursor),
Key::E => *cursor = galley.cursor_end_of_row(cursor),
Key::P => *cursor = galley.cursor_up_one_row(cursor),
Key::N => *cursor = galley.cursor_down_one_row(cursor),
Key::B => *cursor = galley.cursor_left_one_character(cursor),
Key::F => *cursor = galley.cursor_right_one_character(cursor),
_ => (),
}
return;
}
match key { match key {
Key::ArrowLeft => { Key::ArrowLeft => {
if modifiers.alt || modifiers.ctrl { if modifiers.alt || modifiers.ctrl {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_demo_app" name = "egui_demo_app"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
@ -23,10 +23,14 @@ syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
[dependencies] [dependencies]
eframe = { version = "0.16.0", path = "../eframe" } eframe = { version = "0.17.0", path = "../eframe" }
# To use the old glium backend instead: # To use the old glium backend instead:
# eframe = { version = "0.16.0", path = "../eframe", default-features = false, features = ["default_fonts", "egui_glium"] } # eframe = { version = "0.17.0", path = "../eframe", default-features = false, features = ["default_fonts", "egui_glium"] }
egui_demo_lib = { version = "0.16.0", path = "../egui_demo_lib", features = ["extra_debug_asserts"] } egui_demo_lib = { version = "0.17.0", path = "../egui_demo_lib", features = ["extra_debug_asserts"] }
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"

View file

@ -13,6 +13,12 @@ use eframe::wasm_bindgen::{self, prelude::*};
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[wasm_bindgen] #[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let app = egui_demo_lib::WrapApp::default(); let app = egui_demo_lib::WrapApp::default();
eframe::start_web(canvas_id, Box::new(app)) eframe::start_web(canvas_id, Box::new(app))
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_demo_lib" name = "egui_demo_lib"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Example library for egui" description = "Example library for egui"
edition = "2021" edition = "2021"
@ -28,7 +28,7 @@ extra_debug_asserts = ["egui/extra_debug_asserts"]
extra_asserts = ["egui/extra_asserts"] extra_asserts = ["egui/extra_asserts"]
datetime = ["egui_extras/chrono", "chrono"] datetime = ["egui_extras/chrono", "chrono"]
http = ["ehttp", "image", "poll-promise"] http = ["egui_extras", "ehttp", "image", "poll-promise"]
persistence = [ persistence = [
"egui/persistence", "egui/persistence",
"epi/persistence", "epi/persistence",
@ -40,15 +40,18 @@ syntax_highlighting = ["syntect"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false } egui = { version = "0.17.0", path = "../egui", default-features = false }
epi = { version = "0.16.0", path = "../epi" } epi = { version = "0.17.0", path = "../epi" }
egui_extras = { version = "0.16.0", path = "../egui_extras" }
chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true } chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true }
enum-map = { version = "2", features = ["serde"] } enum-map = { version = "2", features = ["serde"] }
unicode_names2 = { version = "0.4.0", default-features = false } unicode_names2 = { version = "0.5.0", default-features = false }
# feature "http": # feature "http":
egui_extras = { version = "0.17.0", path = "../egui_extras", features = [
"image",
"datepicker",
], optional = true }
ehttp = { version = "0.2.0", optional = true } ehttp = { version = "0.2.0", optional = true }
image = { version = "0.24", default-features = false, features = [ image = { version = "0.24", default-features = false, features = [
"jpeg", "jpeg",

View file

@ -13,10 +13,10 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// The most end-to-end benchmark. // The most end-to-end benchmark.
c.bench_function("demo_with_tessellate__realistic", |b| { c.bench_function("demo_with_tessellate__realistic", |b| {
b.iter(|| { b.iter(|| {
let (_output, shapes) = ctx.run(RawInput::default(), |ctx| { let full_output = ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
ctx.tessellate(shapes) ctx.tessellate(full_output.shapes)
}) })
}); });
@ -28,11 +28,11 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
let (_output, shapes) = ctx.run(RawInput::default(), |ctx| { let full_output = ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
c.bench_function("demo_only_tessellate", |b| { c.bench_function("demo_only_tessellate", |b| {
b.iter(|| ctx.tessellate(shapes.clone())) b.iter(|| ctx.tessellate(full_output.shapes.clone()))
}); });
} }

View file

@ -83,6 +83,10 @@ impl super::View for CodeExample {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
use crate::syntax_highlighting::code_view_ui; use crate::syntax_highlighting::code_view_ui;
ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!());
});
code_view_ui( code_view_ui(
ui, ui,
r" r"

View file

@ -105,6 +105,9 @@ impl super::View for ContextMenus {
}); });
}); });
}); });
ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!());
});
} }
} }

View file

@ -16,6 +16,7 @@ struct Demos {
impl Default for Demos { impl Default for Demos {
fn default() -> Self { fn default() -> Self {
Self::from_demos(vec![ Self::from_demos(vec![
Box::new(super::paint_bezier::PaintBezier::default()),
Box::new(super::code_editor::CodeEditor::default()), Box::new(super::code_editor::CodeEditor::default()),
Box::new(super::code_example::CodeExample::default()), Box::new(super::code_example::CodeExample::default()),
Box::new(super::context_menu::ContextMenus::default()), Box::new(super::context_menu::ContextMenus::default()),
@ -26,7 +27,6 @@ impl Default for Demos {
Box::new(super::MiscDemoWindow::default()), Box::new(super::MiscDemoWindow::default()),
Box::new(super::multi_touch::MultiTouch::default()), Box::new(super::multi_touch::MultiTouch::default()),
Box::new(super::painting::Painting::default()), Box::new(super::painting::Painting::default()),
Box::new(super::paint_bezier::PaintBezier::default()),
Box::new(super::plot_demo::PlotDemo::default()), Box::new(super::plot_demo::PlotDemo::default()),
Box::new(super::scrolling::Scrolling::default()), Box::new(super::scrolling::Scrolling::default()),
Box::new(super::sliders::Sliders::default()), Box::new(super::sliders::Sliders::default()),

View file

@ -31,6 +31,10 @@ impl super::Demo for FontBook {
impl super::View for FontBook { impl super::View for FontBook {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!());
});
ui.label(format!( ui.label(format!(
"The selected font supports {} characters.", "The selected font supports {} characters.",
self.named_chars self.named_chars

View file

@ -1,250 +1,167 @@
use egui::emath::RectTransform; use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape};
use egui::epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape};
use egui::*; use egui::*;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct PaintBezier { pub struct PaintBezier {
/// Current bezier curve degree, it can be 3, 4. /// Bézier curve degree, it can be 3, 4.
bezier: usize, degree: usize,
/// Track the bezier degree before change in order to clean the remaining points. /// The control points. The [`Self::degree`] first of them are used.
degree_backup: usize, control_points: [Pos2; 4],
/// Points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes'
points: Vec<Pos2>, /// Stroke for Bézier curve.
/// Track last points set in order to draw auxiliary lines. stroke: Stroke,
backup_points: Vec<Pos2>,
/// Quadratic shapes already drawn. /// Fill for Bézier curve.
q_shapes: Vec<QuadraticBezierShape>, fill: Color32,
/// Cubic shapes already drawn.
/// Since `Shape` can't be 'serialized', we can't use Shape as variable type.
c_shapes: Vec<CubicBezierShape>,
/// Stroke for auxiliary lines. /// Stroke for auxiliary lines.
aux_stroke: Stroke, aux_stroke: Stroke,
/// Stroke for bezier curve.
stroke: Stroke,
/// Fill for bezier curve.
fill: Color32,
/// The curve should be closed or not.
closed: bool,
/// Display the bounding box or not.
show_bounding_box: bool,
/// Storke for the bounding box.
bounding_box_stroke: Stroke, bounding_box_stroke: Stroke,
} }
impl Default for PaintBezier { impl Default for PaintBezier {
fn default() -> Self { fn default() -> Self {
Self { Self {
bezier: 4, // default bezier degree, a cubic bezier curve degree: 4,
degree_backup: 4, control_points: [
points: Default::default(), pos2(50.0, 50.0),
backup_points: Default::default(), pos2(60.0, 250.0),
q_shapes: Default::default(), pos2(200.0, 200.0),
c_shapes: Default::default(), pos2(250.0, 50.0),
aux_stroke: Stroke::new(1.0, Color32::RED), ],
stroke: Stroke::new(1.0, Color32::LIGHT_BLUE), stroke: Stroke::new(1.0, Color32::LIGHT_BLUE),
fill: Default::default(), fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25),
closed: false, aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
show_bounding_box: false, bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)),
bounding_box_stroke: Stroke::new(1.0, Color32::LIGHT_GREEN),
} }
} }
} }
impl PaintBezier { impl PaintBezier {
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response { pub fn ui_control(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| { ui.collapsing("Colors", |ui| {
ui.vertical(|ui| { ui.horizontal(|ui| {
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke"); ui.label("Fill color:");
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); ui.color_edit_button_srgba(&mut self.fill);
ui.horizontal(|ui| {
ui.label("Fill Color:");
if ui.color_edit_button_srgba(&mut self.fill).changed()
&& self.fill != Color32::TRANSPARENT
{
self.closed = true;
}
if ui.checkbox(&mut self.closed, "Closed").clicked() && !self.closed {
self.fill = Color32::TRANSPARENT;
}
});
egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
}); });
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
});
ui.separator(); ui.collapsing("Global tessellation options", |ui| {
ui.vertical(|ui| { let mut tessellation_options = *(ui.ctx().tessellation_options());
{ let tessellation_options = &mut tessellation_options;
let mut tessellation_options = *(ui.ctx().tessellation_options()); tessellation_options.ui(ui);
let tessellation_options = &mut tessellation_options; let mut new_tessellation_options = ui.ctx().tessellation_options();
tessellation_options.ui(ui); *new_tessellation_options = *tessellation_options;
let mut new_tessellation_options = ui.ctx().tessellation_options(); });
*new_tessellation_options = *tessellation_options;
}
ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");
}); ui.radio_value(&mut self.degree, 4, "Cubic Bézier");
ui.separator(); ui.label("Move the points by dragging them.");
ui.vertical(|ui| { ui.small("Only convex curves can be accurately filled.");
if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked()
&& self.degree_backup != self.bezier
{
self.points.clear();
self.degree_backup = self.bezier;
};
if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked()
&& self.degree_backup != self.bezier
{
self.points.clear();
self.degree_backup = self.bezier;
};
// ui.radio_value(self.bezier, 5, "Quintic");
ui.label("Click 3 or 4 points to build a bezier curve!");
if ui.button("Clear Painting").clicked() {
self.points.clear();
self.backup_points.clear();
self.q_shapes.clear();
self.c_shapes.clear();
}
})
})
.response
} }
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response { pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
let (mut response, painter) = let (response, painter) =
ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover());
let to_screen = emath::RectTransform::from_to( let to_screen = emath::RectTransform::from_to(
Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), Rect::from_min_size(Pos2::ZERO, response.rect.size()),
response.rect, response.rect,
); );
let from_screen = to_screen.inverse();
if response.clicked() { let control_point_radius = 8.0;
if let Some(pointer_pos) = response.interact_pointer_pos() {
let canvas_pos = from_screen * pointer_pos;
self.points.push(canvas_pos);
if self.points.len() >= self.bezier {
self.backup_points = self.points.clone();
let points = self.points.drain(..).collect::<Vec<_>>();
match points.len() {
3 => {
let quadratic = QuadraticBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.q_shapes.push(quadratic);
}
4 => {
let cubic = CubicBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.c_shapes.push(cubic);
}
_ => {
unreachable!();
}
}
}
response.mark_changed(); let mut control_point_shapes = vec![];
}
}
let mut shapes = Vec::new();
for shape in self.q_shapes.iter() {
shapes.push(shape.to_screen(&to_screen).into());
if self.show_bounding_box {
shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen));
}
}
for shape in self.c_shapes.iter() {
shapes.push(shape.to_screen(&to_screen).into());
if self.show_bounding_box {
shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen));
}
}
painter.extend(shapes);
if !self.points.is_empty() { for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) {
painter.extend(build_auxiliary_line( let size = Vec2::splat(2.0 * control_point_radius);
&self.points,
&to_screen, let point_in_screen = to_screen.transform_pos(*point);
&self.aux_stroke, let point_rect = Rect::from_center_size(point_in_screen, size);
)); let point_id = response.id.with(i);
} else if !self.backup_points.is_empty() { let point_response = ui.interact(point_rect, point_id, Sense::drag());
painter.extend(build_auxiliary_line(
&self.backup_points, *point += point_response.drag_delta();
&to_screen, *point = to_screen.from().clamp(*point);
&self.aux_stroke,
let point_in_screen = to_screen.transform_pos(*point);
let stroke = ui.style().interact(&point_response).fg_stroke;
control_point_shapes.push(Shape::circle_stroke(
point_in_screen,
control_point_radius,
stroke,
)); ));
} }
let points_in_screen: Vec<Pos2> = self
.control_points
.iter()
.take(self.degree)
.map(|p| to_screen * *p)
.collect();
match self.degree {
3 => {
let points = points_in_screen.clone().try_into().unwrap();
let shape =
QuadraticBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
painter.add(epaint::RectShape::stroke(
shape.visual_bounding_rect(),
0.0,
self.bounding_box_stroke,
));
painter.add(shape);
}
4 => {
let points = points_in_screen.clone().try_into().unwrap();
let shape =
CubicBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
painter.add(epaint::RectShape::stroke(
shape.visual_bounding_rect(),
0.0,
self.bounding_box_stroke,
));
painter.add(shape);
}
_ => {
unreachable!();
}
};
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
painter.extend(control_point_shapes);
response response
} }
pub fn build_bounding_box(&self, bbox: Rect, to_screen: &RectTransform) -> Shape {
let bbox = Rect {
min: to_screen * bbox.min,
max: to_screen * bbox.max,
};
let bbox_shape = epaint::RectShape::stroke(bbox, 0.0, self.bounding_box_stroke);
bbox_shape.into()
}
}
/// An internal function to create auxiliary lines around the current bezier curve
/// or to auxiliary lines (points) before the points meet the bezier curve requirements.
fn build_auxiliary_line(
points: &[Pos2],
to_screen: &RectTransform,
aux_stroke: &Stroke,
) -> Vec<Shape> {
let mut shapes = Vec::new();
if points.len() >= 2 {
let points: Vec<Pos2> = points.iter().map(|p| to_screen * *p).collect();
shapes.push(egui::Shape::line(points, *aux_stroke));
}
for point in points.iter() {
let center = to_screen * *point;
let radius = aux_stroke.width * 3.0;
let circle = CircleShape {
center,
radius,
fill: aux_stroke.color,
stroke: *aux_stroke,
};
shapes.push(circle.into());
}
shapes
} }
impl super::Demo for PaintBezier { impl super::Demo for PaintBezier {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"✔ Bezier Curve" " Bézier Curve"
} }
fn show(&mut self, ctx: &Context, open: &mut bool) { fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _; use super::View as _;
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.default_size(vec2(512.0, 512.0))
.vscroll(false) .vscroll(false)
.resizable(false)
.default_size([300.0, 350.0])
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }
impl super::View for PaintBezier { impl super::View for PaintBezier {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
// ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// ui.add(crate::__egui_github_link_file!()); ui.add(crate::__egui_github_link_file!());
// }); });
self.ui_control(ui); self.ui_control(ui);
Frame::dark_canvas(ui.style()).show(ui, |ui| { Frame::dark_canvas(ui.style()).show(ui, |ui| {

View file

@ -2,8 +2,9 @@ use std::f64::consts::TAU;
use egui::*; use egui::*;
use plot::{ use plot::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Corner, HLine, Legend, Line, LineStyle, Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value, Values, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value,
Values,
}; };
#[derive(PartialEq)] #[derive(PartialEq)]
@ -14,6 +15,7 @@ struct LineDemo {
circle_center: Pos2, circle_center: Pos2,
square: bool, square: bool,
proportional: bool, proportional: bool,
coordinates: bool,
line_style: LineStyle, line_style: LineStyle,
} }
@ -26,6 +28,7 @@ impl Default for LineDemo {
circle_center: Pos2::new(0.0, 0.0), circle_center: Pos2::new(0.0, 0.0),
square: false, square: false,
proportional: true, proportional: true,
coordinates: true,
line_style: LineStyle::Solid, line_style: LineStyle::Solid,
} }
} }
@ -41,6 +44,7 @@ impl LineDemo {
square, square,
proportional, proportional,
line_style, line_style,
coordinates,
.. ..
} = self; } = self;
@ -76,6 +80,8 @@ impl LineDemo {
.on_hover_text("Always keep the viewport square."); .on_hover_text("Always keep the viewport square.");
ui.checkbox(proportional, "Proportional data axes") ui.checkbox(proportional, "Proportional data axes")
.on_hover_text("Tick are the same size on both axes."); .on_hover_text("Tick are the same size on both axes.");
ui.checkbox(coordinates, "Show coordinates")
.on_hover_text("Can take a custom formatting function.");
ComboBox::from_label("Line style") ComboBox::from_label("Line style")
.selected_text(line_style.to_string()) .selected_text(line_style.to_string())
@ -151,6 +157,9 @@ impl Widget for &mut LineDemo {
if self.proportional { if self.proportional {
plot = plot.data_aspect(1.0); plot = plot.data_aspect(1.0);
} }
if self.coordinates {
plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default());
}
plot.show(ui, |plot_ui| { plot.show(ui, |plot_ui| {
plot_ui.line(self.circle()); plot_ui.line(self.circle());
plot_ui.line(self.sin()); plot_ui.line(self.sin());
@ -595,7 +604,7 @@ impl ChartsDemo {
.name("Set 4") .name("Set 4")
.stack_on(&[&chart1, &chart2, &chart3]); .stack_on(&[&chart1, &chart2, &chart3]);
let mut x_fmt: fn(f64) -> String = |val| { let mut x_fmt: fn(f64, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
if val >= 0.0 && val <= 4.0 && is_approx_integer(val) { if val >= 0.0 && val <= 4.0 && is_approx_integer(val) {
// Only label full days from 0 to 4 // Only label full days from 0 to 4
format!("Day {}", val) format!("Day {}", val)
@ -605,7 +614,7 @@ impl ChartsDemo {
} }
}; };
let mut y_fmt: fn(f64) -> String = |val| { let mut y_fmt: fn(f64, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
let percent = 100.0 * val; let percent = 100.0 * val;
if is_approx_integer(percent) && !is_approx_zero(percent) { if is_approx_integer(percent) && !is_approx_zero(percent) {

View file

@ -147,7 +147,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
#[derive(PartialEq)] #[derive(PartialEq)]
struct ScrollTo { struct ScrollTo {
track_item: usize, track_item: usize,
tack_item_align: Align, tack_item_align: Option<Align>,
offset: f32, offset: f32,
} }
@ -155,7 +155,7 @@ impl Default for ScrollTo {
fn default() -> Self { fn default() -> Self {
Self { Self {
track_item: 25, track_item: 25,
tack_item_align: Align::Center, tack_item_align: Some(Align::Center),
offset: 0.0, offset: 0.0,
} }
} }
@ -180,13 +180,16 @@ impl super::View for ScrollTo {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Item align:"); ui.label("Item align:");
track_item |= ui track_item |= ui
.radio_value(&mut self.tack_item_align, Align::Min, "Top") .radio_value(&mut self.tack_item_align, Some(Align::Min), "Top")
.clicked(); .clicked();
track_item |= ui track_item |= ui
.radio_value(&mut self.tack_item_align, Align::Center, "Center") .radio_value(&mut self.tack_item_align, Some(Align::Center), "Center")
.clicked(); .clicked();
track_item |= ui track_item |= ui
.radio_value(&mut self.tack_item_align, Align::Max, "Bottom") .radio_value(&mut self.tack_item_align, Some(Align::Max), "Bottom")
.clicked();
track_item |= ui
.radio_value(&mut self.tack_item_align, None, "None (Bring into view)")
.clicked(); .clicked();
}); });
@ -213,7 +216,7 @@ impl super::View for ScrollTo {
let (current_scroll, max_scroll) = scroll_area let (current_scroll, max_scroll) = scroll_area
.show(ui, |ui| { .show(ui, |ui| {
if scroll_top { if scroll_top {
ui.scroll_to_cursor(Align::TOP); ui.scroll_to_cursor(Some(Align::TOP));
} }
ui.vertical(|ui| { ui.vertical(|ui| {
for item in 1..=50 { for item in 1..=50 {
@ -228,7 +231,7 @@ impl super::View for ScrollTo {
}); });
if scroll_bottom { if scroll_bottom {
ui.scroll_to_cursor(Align::BOTTOM); ui.scroll_to_cursor(Some(Align::BOTTOM));
} }
let margin = ui.visuals().clip_rect_margin; let margin = ui.visuals().clip_rect_margin;

View file

@ -11,6 +11,8 @@ pub struct Sliders {
pub logarithmic: bool, pub logarithmic: bool,
pub clamp_to_range: bool, pub clamp_to_range: bool,
pub smart_aim: bool, pub smart_aim: bool,
pub step: f64,
pub use_steps: bool,
pub integer: bool, pub integer: bool,
pub vertical: bool, pub vertical: bool,
pub value: f64, pub value: f64,
@ -24,6 +26,8 @@ impl Default for Sliders {
logarithmic: true, logarithmic: true,
clamp_to_range: false, clamp_to_range: false,
smart_aim: true, smart_aim: true,
step: 10.0,
use_steps: false,
integer: false, integer: false,
vertical: false, vertical: false,
value: 10.0, value: 10.0,
@ -55,6 +59,8 @@ impl super::View for Sliders {
logarithmic, logarithmic,
clamp_to_range, clamp_to_range,
smart_aim, smart_aim,
step,
use_steps,
integer, integer,
vertical, vertical,
value, value,
@ -79,6 +85,7 @@ impl super::View for Sliders {
SliderOrientation::Horizontal SliderOrientation::Horizontal
}; };
let istep = if *use_steps { *step } else { 0.0 };
if *integer { if *integer {
let mut value_i32 = *value as i32; let mut value_i32 = *value as i32;
ui.add( ui.add(
@ -87,7 +94,8 @@ impl super::View for Sliders {
.clamp_to_range(*clamp_to_range) .clamp_to_range(*clamp_to_range)
.smart_aim(*smart_aim) .smart_aim(*smart_aim)
.orientation(orientation) .orientation(orientation)
.text("i32 demo slider"), .text("i32 demo slider")
.step_by(istep),
); );
*value = value_i32 as f64; *value = value_i32 as f64;
} else { } else {
@ -97,7 +105,8 @@ impl super::View for Sliders {
.clamp_to_range(*clamp_to_range) .clamp_to_range(*clamp_to_range)
.smart_aim(*smart_aim) .smart_aim(*smart_aim)
.orientation(orientation) .orientation(orientation)
.text("f64 demo slider"), .text("f64 demo slider")
.step_by(istep),
); );
ui.label( ui.label(
@ -128,6 +137,14 @@ impl super::View for Sliders {
ui.separator(); ui.separator();
ui.checkbox(use_steps, "Use steps");
ui.label("When enabled, the minimal value change would be restricted to a given step.");
if *use_steps {
ui.add(egui::DragValue::new(step).speed(1.0));
}
ui.separator();
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Slider type:"); ui.label("Slider type:");
ui.radio_value(integer, true, "i32"); ui.radio_value(integer, true, "i32");

View file

@ -64,7 +64,10 @@ impl super::View for TextEdit {
anything_selected, anything_selected,
egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"), egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"),
); );
if ui.input().modifiers.command_only() && ui.input().key_pressed(egui::Key::T) { if ui
.input_mut()
.consume_key(egui::Modifiers::COMMAND, egui::Key::T)
{
if let Some(text_cursor_range) = output.cursor_range { if let Some(text_cursor_range) = output.cursor_range {
use egui::TextBuffer as _; use egui::TextBuffer as _;
let selected_chars = text_cursor_range.as_sorted_char_range(); let selected_chars = text_cursor_range.as_sorted_char_range();

View file

@ -1,3 +1,4 @@
use egui_extras::RetainedImage;
use poll_promise::Promise; use poll_promise::Promise;
struct Resource { struct Resource {
@ -7,7 +8,7 @@ struct Resource {
text: Option<String>, text: Option<String>,
/// If set, the response was an image. /// If set, the response was an image.
texture: Option<egui::TextureHandle>, image: Option<RetainedImage>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>, colored_text: Option<ColoredText>,
@ -17,13 +18,11 @@ impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default(); let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") { let image = if content_type.starts_with("image/") {
load_image(&response.bytes).ok() RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
} else { } else {
None None
}; };
let texture = image.map(|image| ctx.load_texture(&response.url, image));
let text = response.text(); let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned()); let text = text.map(|text| text.to_owned());
@ -31,7 +30,7 @@ impl Resource {
Self { Self {
response, response,
text, text,
texture, image,
colored_text, colored_text,
} }
} }
@ -151,7 +150,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
let Resource { let Resource {
response, response,
text, text,
texture, image,
colored_text, colored_text,
} = resource; } = resource;
@ -198,10 +197,10 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
ui.separator(); ui.separator();
} }
if let Some(texture) = texture { if let Some(image) = image {
let mut size = texture.size_vec2(); let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0); size *= (ui.available_width() / size.x).min(1.0);
ui.image(texture, size); image.show_size(ui, size);
} else if let Some(colored_text) = colored_text { } else if let Some(colored_text) = colored_text {
colored_text.ui(ui); colored_text.ui(ui);
} else if let Some(text) = &text { } else if let Some(text) = &text {
@ -270,16 +269,3 @@ impl ColoredText {
} }
} }
} }
// ----------------------------------------------------------------------------
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View file

@ -172,6 +172,12 @@ impl BackendPanel {
show_integration_name(ui, &frame.info()); show_integration_name(ui, &frame.info());
if let Some(web_info) = &frame.info().web_info {
ui.collapsing("Web info (location)", |ui| {
ui.monospace(format!("{:#?}", web_info.location));
});
}
// For instance: `egui_web` sets `pixels_per_point` every frame to force // For instance: `egui_web` sets `pixels_per_point` every frame to force
// egui to use the same scale as the web zoom factor. // egui to use the same scale as the web zoom factor.
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some(); let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();

View file

@ -117,59 +117,24 @@ impl EasyMarkEditor {
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool { fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
let mut any_change = false; let mut any_change = false;
for event in &ui.input().events { for (key, surrounding) in [
if let Event::Key { (Key::B, "*"), // *bold*
key, (Key::C, "`"), // `code`
pressed: true, (Key::I, "/"), // /italics/
modifiers, (Key::L, "$"), // $subscript$
} = event (Key::R, "^"), // ^superscript^
{ (Key::S, "~"), // ~strikethrough~
if modifiers.command_only() { (Key::U, "_"), // _underline_
match &key { ] {
// toggle *bold* if ui.input_mut().consume_key(egui::Modifiers::COMMAND, key) {
Key::B => { toggle_surrounding(code, ccursor_range, surrounding);
toggle_surrounding(code, ccursor_range, "*"); any_change = true;
any_change = true; };
}
// toggle `code`
Key::C => {
toggle_surrounding(code, ccursor_range, "`");
any_change = true;
}
// toggle /italics/
Key::I => {
toggle_surrounding(code, ccursor_range, "/");
any_change = true;
}
// toggle $lowered$
Key::L => {
toggle_surrounding(code, ccursor_range, "$");
any_change = true;
}
// toggle ^raised^
Key::R => {
toggle_surrounding(code, ccursor_range, "^");
any_change = true;
}
// toggle ~strikethrough~
Key::S => {
toggle_surrounding(code, ccursor_range, "~");
any_change = true;
}
// toggle _underline_
Key::U => {
toggle_surrounding(code, ccursor_range, "_");
any_change = true;
}
_ => {}
}
}
}
} }
any_change any_change
} }
/// E.g. toggle *strong* with `toggle(&mut text, &mut cursor, "*")` /// E.g. toggle *strong* with `toggle_surrounding(&mut text, &mut cursor, "*")`
fn toggle_surrounding( fn toggle_surrounding(
code: &mut dyn TextBuffer, code: &mut dyn TextBuffer,
ccursor_range: &mut CCursorRange, ccursor_range: &mut CCursorRange,

View file

@ -145,10 +145,10 @@ fn test_egui_e2e() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| { let full_output = ctx.run(raw_input.clone(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
let clipped_meshes = ctx.tessellate(shapes); let clipped_meshes = ctx.tessellate(full_output.shapes);
assert!(!clipped_meshes.is_empty()); assert!(!clipped_meshes.is_empty());
} }
} }
@ -164,10 +164,10 @@ fn test_egui_zero_window_size() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| { let full_output = ctx.run(raw_input.clone(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
let clipped_meshes = ctx.tessellate(shapes); let clipped_meshes = ctx.tessellate(full_output.shapes);
assert!(clipped_meshes.is_empty(), "There should be nothing to show"); assert!(clipped_meshes.is_empty(), "There should be nothing to show");
} }
} }

View file

@ -69,7 +69,7 @@ impl epi::App for WrapApp {
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
if let Some(web_info) = frame.info().web_info.as_ref() { if let Some(web_info) = frame.info().web_info.as_ref() {
if let Some(anchor) = web_info.web_location_hash.strip_prefix('#') { if let Some(anchor) = web_info.location.hash.strip_prefix('#') {
self.selected_anchor = anchor.to_owned(); self.selected_anchor = anchor.to_owned();
} }
} }
@ -108,12 +108,19 @@ impl epi::App for WrapApp {
}); });
} }
let mut found_anchor = false;
for (anchor, app) in self.apps.iter_mut() { for (anchor, app) in self.apps.iter_mut() {
if anchor == self.selected_anchor || ctx.memory().everything_is_visible() { if anchor == self.selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame); app.update(ctx, frame);
found_anchor = true;
} }
} }
if !found_anchor {
self.selected_anchor = "demo".into();
}
self.backend_panel.end_of_frame(ctx); self.backend_panel.end_of_frame(ctx);
self.ui_file_drag_and_drop(ctx); self.ui_file_drag_and_drop(ctx);

9
egui_extras/CHANGELOG.md Normal file
View file

@ -0,0 +1,9 @@
# Changelog for egui_extras
All notable changes to the `egui_extras` integration will be noted in this file.
## Unreleased
## 0.17.0 - 2022-02-22
* `RetainedImage`: conventience for loading svg, png, jpeg etc and showing them in egui.

View file

@ -1,29 +1,60 @@
[package] [package]
name = "egui_extras" name = "egui_extras"
version = "0.16.0" version = "0.17.0"
authors = [
"Dominik Rössler <dominik@freshx.de>",
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"René Rössler <rene@freshx.de>",
]
description = "Extra functionality and widgets for the egui GUI library"
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = "1.56"
description = "Extra widgets for egui" homepage = "https://github.com/emilk/egui"
authors = [
"René Rössler <rene@freshx.de>",
"Dominik Rössler <dominik@freshx.de>",
]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
homepage = "https://github.com/emilk/egui/tree/master/egui_extras" readme = "../README.md"
readme = "README.md" repository = "https://github.com/emilk/egui"
repository = "https://github.com/emilk/egui/tree/master/egui_extras" categories = ["gui", "game-development"]
categories = ["gui", "graphics"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[dependencies] [package.metadata.docs.rs]
egui = { version = "0.16.0", path = "../egui", default-features = false } all-features = true
[lib]
[features]
default = []
# Support loading svg images
svg = ["resvg", "tiny-skia", "usvg"]
# Datepicker widget
datepicker = ["chrono"]
# Persistence
persistence = ["serde"]
[dependencies]
egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"single_threaded",
] }
parking_lot = "0.12"
# Optional dependencies:
# Date operations needed for datepicker widget
chrono = { version = "0.4", optional = true } chrono = { version = "0.4", optional = true }
# Add support for loading images with the `image` crate.
# You also need to ALSO opt-in to the image formats you want to support, like so:
# image = { version = "0.24", features = ["jpeg", "png"] }
image = { version = "0.24", optional = true, default-features = false, features = [] }
# svg feature
resvg = { version = "0.22", optional = true }
tiny-skia = { version = "0.6", optional = true }
usvg = { version = "0.22", optional = true }
# feature "persistence": # feature "persistence":
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
[features]
default = ["chrono"]
persistence = ["serde"]

8
egui_extras/README.md Normal file
View file

@ -0,0 +1,8 @@
# egui_extras
[![Latest version](https://img.shields.io/crates/v/egui_extras.svg)](https://crates.io/crates/egui_extras)
[![Documentation](https://docs.rs/egui_extras/badge.svg)](https://docs.rs/egui_extras)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate are for experimental features, and features that require big dependencies that does not belong in `egui`.

177
egui_extras/src/image.rs Normal file
View file

@ -0,0 +1,177 @@
use parking_lot::Mutex;
/// An image to be shown in egui.
///
/// Load once, and save somewhere in your app state.
///
/// Use the `svg` and `image` features to enable more constructors.
pub struct RetainedImage {
debug_name: String,
size: [usize; 2],
/// Cleared once [`Self::texture`] has been loaded.
image: Mutex<egui::ColorImage>,
/// Lazily loaded when we have an egui context.
texture: Mutex<Option<egui::TextureHandle>>,
}
impl RetainedImage {
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
Self {
debug_name: debug_name.into(),
size: image.size,
image: Mutex::new(image),
texture: Default::default(),
}
}
/// Load a (non-svg) image.
///
/// Requires the "image" feature. You must also opt-in to the image formats you need
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
///
/// # Errors
/// On invalid image or unsupported image format.
#[cfg(feature = "image")]
pub fn from_image_bytes(
debug_name: impl Into<String>,
image_bytes: &[u8],
) -> Result<Self, String> {
Ok(Self::from_color_image(
debug_name,
load_image_bytes(image_bytes)?,
))
}
/// Pass in the bytes of an SVG that you've loaded.
///
/// # Errors
/// On invalid image
#[cfg(feature = "svg")]
pub fn from_svg_bytes(debug_name: impl Into<String>, svg_bytes: &[u8]) -> Result<Self, String> {
Ok(Self::from_color_image(
debug_name,
load_svg_bytes(svg_bytes)?,
))
}
/// Pass in the str of an SVG that you've loaded.
///
/// # Errors
/// On invalid image
#[cfg(feature = "svg")]
pub fn from_svg_str(debug_name: impl Into<String>, svg_str: &str) -> Result<Self, String> {
Self::from_svg_bytes(debug_name, svg_str.as_bytes())
}
/// The size of the image data (number of pixels wide/high).
pub fn size(&self) -> [usize; 2] {
self.size
}
/// The size of the image data (number of pixels wide/high).
pub fn size_vec2(&self) -> egui::Vec2 {
let [w, h] = self.size();
egui::vec2(w as f32, h as f32)
}
/// The debug name of the image, e.g. the file name.
pub fn debug_name(&self) -> &str {
&self.debug_name
}
/// The texture if for this image.
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
self.texture
.lock()
.get_or_insert_with(|| {
let image: &mut ColorImage = &mut self.image.lock();
let image = std::mem::take(image);
ctx.load_texture(&self.debug_name, image)
})
.id()
}
/// Show the image with the given maximum size.
pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
let mut desired_size = self.size_vec2();
desired_size *= (max_size.x / desired_size.x).min(1.0);
desired_size *= (max_size.y / desired_size.y).min(1.0);
self.show_size(ui, desired_size)
}
/// Show the image with the original size (one image pixel = one gui point).
pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
self.show_size(ui, self.size_vec2())
}
/// Show the image with the given scale factor (1.0 = original size).
pub fn show_scaled(&self, ui: &mut egui::Ui, scale: f32) -> egui::Response {
self.show_size(ui, self.size_vec2() * scale)
}
/// Show the image with the given size.
pub fn show_size(&self, ui: &mut egui::Ui, desired_size: egui::Vec2) -> egui::Response {
// We need to convert the SVG to a texture to display it:
// Future improvement: tell backend to do mip-mapping of the image to
// make it look smoother when downsized.
ui.image(self.texture_id(ui.ctx()), desired_size)
}
}
// ----------------------------------------------------------------------------
use egui::ColorImage;
/// Load a (non-svg) image.
///
/// Requires the "image" feature. You must also opt-in to the image formats you need
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
///
/// # Errors
/// On invalid image or unsupported image format.
#[cfg(feature = "image")]
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, String> {
let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}
/// Load an SVG and rasterize it into an egui image.
///
/// Requires the "svg" feature.
///
/// # Errors
/// On invalid image
#[cfg(feature = "svg")]
pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result<egui::ColorImage, String> {
let mut opt = usvg::Options::default();
opt.fontdb.load_system_fonts();
let rtree = usvg::Tree::from_data(svg_bytes, &opt.to_ref()).map_err(|err| err.to_string())?;
let pixmap_size = rtree.svg_node().size.to_screen_size();
let [w, h] = [pixmap_size.width(), pixmap_size.height()];
let mut pixmap = tiny_skia::Pixmap::new(w, h)
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?;
resvg::render(
&rtree,
usvg::FitTo::Original,
tiny_skia::Transform::default(),
pixmap.as_mut(),
)
.ok_or_else(|| "Failed to render SVG".to_owned())?;
let image = egui::ColorImage::from_rgba_unmultiplied(
[pixmap.width() as _, pixmap.height() as _],
pixmap.data(),
);
Ok(image)
}

View file

@ -1,5 +1,7 @@
//! `egui_extras`: Widgets for egui which are not in the main egui crate //! This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate are for experimental features, and features that require big dependencies that does not belong in `egui`.
// Forbid warnings in release builds:
#![cfg_attr(not(debug_assertions), deny(warnings))]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn( #![warn(
clippy::all, clippy::all,
@ -84,14 +86,16 @@
mod datepicker; mod datepicker;
mod grid; mod grid;
pub mod image;
mod layout; mod layout;
mod sizing; mod sizing;
mod table; mod table;
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
pub use datepicker::DatePickerButton; pub use crate::datepicker::DatePickerButton;
pub use grid::*; pub use crate::grid::*;
pub(crate) use layout::Layout; pub use crate::image::RetainedImage;
pub use sizing::Size; pub(crate) use crate::layout::Layout;
pub use table::*; pub use crate::sizing::Size;
pub use crate::table::*;

View file

@ -3,6 +3,9 @@ All notable changes to the `egui_glium` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* `EguiGlium::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlium::paint` ([#1110](https://github.com/emilk/egui/pull/1110)). * `EguiGlium::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlium::paint` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Optimize the painter and texture uploading ([#1110](https://github.com/emilk/egui/pull/1110)). * Optimize the painter and texture uploading ([#1110](https://github.com/emilk/egui/pull/1110)).
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)). * Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_glium" name = "egui_glium"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glium library" description = "Bindings for using egui natively using the glium library"
edition = "2021" edition = "2021"
@ -51,12 +51,12 @@ screen_reader = ["egui-winit/screen_reader"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = [ egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"convert_bytemuck", "convert_bytemuck",
"single_threaded", "single_threaded",
] } ] }
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["epi"] } egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false, features = ["epi"] }
epi = { version = "0.16.0", path = "../epi", optional = true } epi = { version = "0.17.0", path = "../epi", optional = true }
ahash = "0.7" ahash = "0.7"
bytemuck = "1.7" bytemuck = "1.7"

View file

@ -67,13 +67,16 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
let (needs_repaint, mut textures_delta, shapes) = let egui::FullOutput {
integration.update(display.gl_window().window()); platform_output,
let clipped_meshes = integration.egui_ctx.tessellate(shapes); needs_repaint,
textures_delta,
shapes,
} = integration.update(display.gl_window().window());
for (id, image_delta) in textures_delta.set { integration.handle_platform_output(display.gl_window().window(), platform_output);
painter.set_texture(&display, id, &image_delta);
} let clipped_meshes = integration.egui_ctx.tessellate(shapes);
// paint: // paint:
{ {
@ -82,20 +85,17 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
let color = integration.app.clear_color(); let color = integration.app.clear_color();
target.clear_color(color[0], color[1], color[2], color[3]); target.clear_color(color[0], color[1], color[2], color[3]);
painter.paint_meshes( painter.paint_and_update_textures(
&display, &display,
&mut target, &mut target,
integration.egui_ctx.pixels_per_point(), integration.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
&textures_delta,
); );
target.finish().unwrap(); target.finish().unwrap();
} }
for id in textures_delta.free.drain(..) {
painter.free_texture(id);
}
{ {
*control_flow = if integration.should_quit() { *control_flow = if integration.should_quit() {
glutin::event_loop::ControlFlow::Exit glutin::event_loop::ControlFlow::Exit

View file

@ -141,38 +141,36 @@ impl EguiGlium {
let raw_input = self let raw_input = self
.egui_winit .egui_winit
.take_egui_input(display.gl_window().window()); .take_egui_input(display.gl_window().window());
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let egui::FullOutput {
let needs_repaint = egui_output.needs_repaint; platform_output,
let textures_delta = self.egui_winit.handle_output( needs_repaint,
textures_delta,
shapes,
} = self.egui_ctx.run(raw_input, run_ui);
self.egui_winit.handle_platform_output(
display.gl_window().window(), display.gl_window().window(),
&self.egui_ctx, &self.egui_ctx,
egui_output, platform_output,
); );
self.shapes = shapes; self.shapes = shapes;
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
needs_repaint needs_repaint
} }
/// Paint the results of the last call to [`Self::run`]. /// Paint the results of the last call to [`Self::run`].
pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) { pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) {
let shapes = std::mem::take(&mut self.shapes); let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta); let textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image_delta) in textures_delta.set {
self.painter.set_texture(display, id, &image_delta);
}
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.painter.paint_meshes( self.painter.paint_and_update_textures(
display, display,
target, target,
self.egui_ctx.pixels_per_point(), self.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
&textures_delta,
); );
for id in textures_delta.free.drain(..) {
self.painter.free_texture(id);
}
} }
} }

View file

@ -65,6 +65,25 @@ impl Painter {
self.max_texture_side self.max_texture_side
} }
pub fn paint_and_update_textures<T: glium::Surface>(
&mut self,
display: &glium::Display,
target: &mut T,
pixels_per_point: f32,
clipped_meshes: Vec<egui::ClippedMesh>,
textures_delta: &egui::TexturesDelta,
) {
for (id, image_delta) in &textures_delta.set {
self.set_texture(display, *id, image_delta);
}
self.paint_meshes(display, target, pixels_per_point, clipped_meshes);
for &id in &textures_delta.free {
self.free_texture(id);
}
}
/// Main entry-point for painting a frame. /// Main entry-point for painting a frame.
/// You should call `target.clear_color(..)` before /// You should call `target.clear_color(..)` before
/// and `target.finish()` after this. /// and `target.finish()` after this.
@ -73,9 +92,9 @@ impl Painter {
display: &glium::Display, display: &glium::Display,
target: &mut T, target: &mut T,
pixels_per_point: f32, pixels_per_point: f32,
cipped_meshes: Vec<egui::ClippedMesh>, clipped_meshes: Vec<egui::ClippedMesh>,
) { ) {
for egui::ClippedMesh(clip_rect, mesh) in cipped_meshes { for egui::ClippedMesh(clip_rect, mesh) in clipped_meshes {
self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh); self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh);
} }
} }

View file

@ -3,11 +3,13 @@ All notable changes to the `egui_glow` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* `EguiGlow::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlow::paint` ([#1110](https://github.com/emilk/egui/pull/1110)). * `EguiGlow::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlow::paint` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Added `set_texture_filter` method to `Painter` ([#1041](https://github.com/emilk/egui/pull/1041)). * Added `set_texture_filter` method to `Painter` ([#1041](https://github.com/emilk/egui/pull/1041)).
* Fix failure to run in Chrome ([#1092](https://github.com/emilk/egui/pull/1092)). * Fix failure to run in Chrome ([#1092](https://github.com/emilk/egui/pull/1092)).
* `EguiGlow::new` now takes `&winit::Window` because there are no reason to use `&glutin::WindowedContext` ([#1151](https://github.com/emilk/egui/pull/1151)). * `EguiGlow::new` and `EguiGlow::paint` now takes `&winit::Window` ([#1151](https://github.com/emilk/egui/pull/1151)).
* `EguiGlow::paint` now takes `&winit::Window` because there are no reason to use `&glutin::WindowedContext` ([#1151](https://github.com/emilk/egui/pull/1151)).
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)). * Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_glow" name = "egui_glow"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glow library" description = "Bindings for using egui natively using the glow library"
edition = "2021" edition = "2021"
@ -55,11 +55,11 @@ winit = ["egui-winit", "glutin"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = [ egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"convert_bytemuck", "convert_bytemuck",
"single_threaded", "single_threaded",
] } ] }
epi = { version = "0.16.0", path = "../epi", optional = true } epi = { version = "0.17.0", path = "../epi", optional = true }
bytemuck = "1.7" bytemuck = "1.7"
glow = "0.11" glow = "0.11"
@ -67,7 +67,7 @@ memoffset = "0.6"
tracing = "0.1" tracing = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["dark-light", "epi"], optional = true } egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false, features = ["dark-light", "epi"], optional = true }
glutin = { version = "0.28.0", optional = true } glutin = { version = "0.28.0", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]

View file

@ -83,13 +83,16 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
let (needs_repaint, mut textures_delta, shapes) = let egui::FullOutput {
integration.update(gl_window.window()); platform_output,
let clipped_meshes = integration.egui_ctx.tessellate(shapes); needs_repaint,
textures_delta,
shapes,
} = integration.update(gl_window.window());
for (id, image_delta) in textures_delta.set { integration.handle_platform_output(gl_window.window(), platform_output);
painter.set_texture(&gl, id, &image_delta);
} let clipped_meshes = integration.egui_ctx.tessellate(shapes);
// paint: // paint:
{ {
@ -100,20 +103,17 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
gl.clear_color(color[0], color[1], color[2], color[3]); gl.clear_color(color[0], color[1], color[2], color[3]);
gl.clear(glow::COLOR_BUFFER_BIT); gl.clear(glow::COLOR_BUFFER_BIT);
} }
painter.paint_meshes( painter.paint_and_update_textures(
&gl, &gl,
gl_window.window().inner_size().into(), gl_window.window().inner_size().into(),
integration.egui_ctx.pixels_per_point(), integration.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
&textures_delta,
); );
gl_window.swap_buffers().unwrap(); gl_window.swap_buffers().unwrap();
} }
for id in textures_delta.free.drain(..) {
painter.free_texture(&gl, id);
}
{ {
*control_flow = if integration.should_quit() { *control_flow = if integration.should_quit() {
winit::event_loop::ControlFlow::Exit winit::event_loop::ControlFlow::Exit

View file

@ -156,11 +156,15 @@ impl EguiGlow {
run_ui: impl FnMut(&egui::Context), run_ui: impl FnMut(&egui::Context),
) -> bool { ) -> bool {
let raw_input = self.egui_winit.take_egui_input(window); let raw_input = self.egui_winit.take_egui_input(window);
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let egui::FullOutput {
let needs_repaint = egui_output.needs_repaint; platform_output,
let textures_delta = self needs_repaint,
.egui_winit textures_delta,
.handle_output(window, &self.egui_ctx, egui_output); shapes,
} = self.egui_ctx.run(raw_input, run_ui);
self.egui_winit
.handle_platform_output(window, &self.egui_ctx, platform_output);
self.shapes = shapes; self.shapes = shapes;
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);

View file

@ -270,6 +270,25 @@ impl Painter {
(width_in_pixels, height_in_pixels) (width_in_pixels, height_in_pixels)
} }
pub fn paint_and_update_textures(
&mut self,
gl: &glow::Context,
inner_size: [u32; 2],
pixels_per_point: f32,
clipped_meshes: Vec<egui::ClippedMesh>,
textures_delta: &egui::TexturesDelta,
) {
for (id, image_delta) in &textures_delta.set {
self.set_texture(gl, *id, image_delta);
}
self.paint_meshes(gl, inner_size, pixels_per_point, clipped_meshes);
for &id in &textures_delta.free {
self.free_texture(gl, id);
}
}
/// Main entry-point for painting a frame. /// Main entry-point for painting a frame.
/// You should call `target.clear_color(..)` before /// You should call `target.clear_color(..)` before
/// and `target.finish()` after this. /// and `target.finish()` after this.

View file

@ -1,15 +1,17 @@
# Changelog for egui_web # Changelog for egui_web
All notable changes to the `egui_web` integration will be noted in this file. All notable changes to the `egui_web` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* The default painter is now glow instead of WebGL ([#1020](https://github.com/emilk/egui/pull/1020)). * The default painter is now glow instead of WebGL ([#1020](https://github.com/emilk/egui/pull/1020)).
* Made the WebGL painter opt-in ([#1020](https://github.com/emilk/egui/pull/1020)). * Made the WebGL painter opt-in ([#1020](https://github.com/emilk/egui/pull/1020)).
* Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)). * Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)).
* Shift-scroll will now result in horizontal scrolling ([#1136](https://github.com/emilk/egui/pull/1136)). * Shift-scroll will now result in horizontal scrolling ([#1136](https://github.com/emilk/egui/pull/1136)).
* Updated `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)). * Updated `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)).
* Panics will now be logged using `console.error`. * Parse and percent-decode the web location query string ([#1258](https://github.com/emilk/egui/pull/1258)).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_web" name = "egui_web"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for compiling egui code to WASM for a web page" description = "Bindings for compiling egui code to WASM for a web page"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -47,19 +47,18 @@ screen_reader = ["tts"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = [ egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"convert_bytemuck", "convert_bytemuck",
"single_threaded", "single_threaded",
"tracing", "tracing",
] } ] }
egui_glow = { version = "0.16.0",path = "../egui_glow", default-features = false, optional = true } egui_glow = { version = "0.17.0",path = "../egui_glow", default-features = false, optional = true }
epi = { version = "0.16.0", path = "../epi" } epi = { version = "0.17.0", path = "../epi" }
bytemuck = "1.7" bytemuck = "1.7"
console_error_panic_hook = "0.1.6"
js-sys = "0.3" js-sys = "0.3"
percent-encoding = "2.1"
tracing = "0.1" tracing = "0.1"
tracing-wasm = "0.2"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"

View file

@ -24,6 +24,6 @@ Check out [eframe_template](https://github.com/emilk/eframe_template) for an exa
* 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. * 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.
The suggested use for `egui_web` is for experiments, personal projects and web games. Using egui for a serious web page is probably a bad idea.
In many ways, `egui_web` 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, `egui_web` 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).
The suggested use for `egui_web` are for web apps where performance and responsiveness are more important than accessability and mobile text editing.

View file

@ -81,6 +81,77 @@ impl epi::backend::RepaintSignal for NeedRepaint {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
fn web_location() -> epi::Location {
let location = web_sys::window().unwrap().location();
let hash = percent_decode(&location.hash().unwrap_or_default());
let query = location
.search()
.unwrap_or_default()
.strip_prefix('?')
.map(percent_decode)
.unwrap_or_default();
let query_map = parse_query_map(&query)
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
epi::Location {
url: percent_decode(&location.href().unwrap_or_default()),
protocol: percent_decode(&location.protocol().unwrap_or_default()),
host: percent_decode(&location.host().unwrap_or_default()),
hostname: percent_decode(&location.hostname().unwrap_or_default()),
port: percent_decode(&location.port().unwrap_or_default()),
hash,
query,
query_map,
origin: percent_decode(&location.origin().unwrap_or_default()),
}
}
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
query
.split('&')
.filter_map(|pair| {
if pair.is_empty() {
None
} else {
Some(if let Some((key, value)) = pair.split_once('=') {
(key, value)
} else {
(pair, "")
})
}
})
.collect()
}
#[test]
fn test_parse_query() {
assert_eq!(parse_query_map(""), BTreeMap::default());
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
assert_eq!(
parse_query_map("foo=bar"),
BTreeMap::from_iter([("foo", "bar")])
);
assert_eq!(
parse_query_map("foo=bar&baz=42"),
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz=42"),
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz&&"),
BTreeMap::from_iter([("foo", ""), ("baz", "")])
);
}
// ----------------------------------------------------------------------------
pub struct AppRunner { pub struct AppRunner {
pub(crate) frame: epi::Frame, pub(crate) frame: epi::Frame,
egui_ctx: egui::Context, egui_ctx: egui::Context,
@ -108,7 +179,7 @@ impl AppRunner {
info: epi::IntegrationInfo { info: epi::IntegrationInfo {
name: painter.name(), name: painter.name(),
web_info: Some(epi::WebInfo { web_info: Some(epi::WebInfo {
web_location_hash: location_hash().unwrap_or_default(), location: web_location(),
}), }),
prefer_dark_mode, prefer_dark_mode,
cpu_usage: None, cpu_usage: None,
@ -143,7 +214,7 @@ impl AppRunner {
textures_delta: Default::default(), textures_delta: Default::default(),
}; };
runner.input.raw.max_texture_side = runner.painter.max_texture_side(); runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
{ {
runner runner
@ -196,14 +267,19 @@ impl AppRunner {
let canvas_size = canvas_size_in_points(self.canvas_id()); let canvas_size = canvas_size_in_points(self.canvas_id());
let raw_input = self.input.new_frame(canvas_size); let raw_input = self.input.new_frame(canvas_size);
let (egui_output, shapes) = self.egui_ctx.run(raw_input, |egui_ctx| { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
self.app.update(egui_ctx, &self.frame); self.app.update(egui_ctx, &self.frame);
}); });
let clipped_meshes = self.egui_ctx.tessellate(shapes); let egui::FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = full_output;
let needs_repaint = egui_output.needs_repaint; self.handle_platform_output(platform_output);
let textures_delta = self.handle_egui_output(egui_output);
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
let clipped_meshes = self.egui_ctx.tessellate(shapes);
{ {
let app_output = self.frame.take_app_output(); let app_output = self.frame.take_app_output();
@ -223,36 +299,32 @@ impl AppRunner {
/// 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_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> { pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> {
let textures_delta = std::mem::take(&mut self.textures_delta); let textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image_delta) in textures_delta.set {
self.painter.set_texture(id, &image_delta);
}
self.painter.clear(self.app.clear_color()); self.painter.clear(self.app.clear_color());
self.painter
.paint_meshes(clipped_meshes, self.egui_ctx.pixels_per_point())?;
for id in textures_delta.free { self.painter.paint_and_update_textures(
self.painter.free_texture(id); clipped_meshes,
} self.egui_ctx.pixels_per_point(),
&textures_delta,
)?;
Ok(()) Ok(())
} }
fn handle_egui_output(&mut self, output: egui::Output) -> egui::TexturesDelta { fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options().screen_reader { if self.egui_ctx.options().screen_reader {
self.screen_reader.speak(&output.events_description()); self.screen_reader
.speak(&platform_output.events_description());
} }
let egui::Output { let egui::PlatformOutput {
cursor_icon, cursor_icon,
open_url, open_url,
copied_text, copied_text,
needs_repaint: _, // handled elsewhere events: _, // already handled
events: _, // already handled
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
textures_delta, } = platform_output;
} = output;
set_cursor_icon(cursor_icon); set_cursor_icon(cursor_icon);
if let Some(open) = open_url { if let Some(open) = open_url {
@ -270,23 +342,15 @@ impl AppRunner {
self.mutable_text_under_cursor = mutable_text_under_cursor; self.mutable_text_under_cursor = mutable_text_under_cursor;
if self.text_cursor_pos != text_cursor_pos { if self.text_cursor_pos != text_cursor_pos {
move_text_cursor(text_cursor_pos, self.canvas_id()); text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
self.text_cursor_pos = text_cursor_pos; self.text_cursor_pos = text_cursor_pos;
} }
textures_delta
} }
} }
/// 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(canvas_id: &str, app: Box<dyn epi::App>) -> Result<AppRunnerRef, JsValue> { pub fn start(canvas_id: &str, app: Box<dyn epi::App>) -> Result<AppRunnerRef, JsValue> {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let mut runner = AppRunner::new(canvas_id, app)?; let mut runner = AppRunner::new(canvas_id, app)?;
runner.warm_up()?; runner.warm_up()?;
start_runner(runner) start_runner(runner)
@ -298,7 +362,7 @@ fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner))); let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner)));
install_canvas_events(&runner_ref)?; install_canvas_events(&runner_ref)?;
install_document_events(&runner_ref)?; install_document_events(&runner_ref)?;
install_text_agent(&runner_ref)?; text_agent::install_text_agent(&runner_ref)?;
repaint_every_ms(&runner_ref, 1000)?; // just in case. TODO: make it a parameter repaint_every_ms(&runner_ref, 1000)?; // just in case. TODO: make it a parameter
paint_and_schedule(runner_ref.clone())?; paint_and_schedule(runner_ref.clone())?;
Ok(runner_ref) Ok(runner_ref)

189
egui_web/src/input.rs Normal file
View file

@ -0,0 +1,189 @@
use crate::{canvas_element, canvas_origin, AppRunner};
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
let rect = canvas.get_bounding_client_rect();
egui::Pos2 {
x: event.client_x() as f32 - rect.left() as f32,
y: event.client_y() as f32 - rect.top() as f32,
}
}
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
match event.button() {
0 => Some(egui::PointerButton::Primary),
1 => Some(egui::PointerButton::Middle),
2 => Some(egui::PointerButton::Secondary),
_ => None,
}
}
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
/// should not jump to a different position. Therefore, we do not calculate the average position
/// of all touches, but we keep using the same touch as long as it is available.
///
/// `touch_id_for_pos` is the `TouchId` of the `Touch` we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> egui::Pos2 {
let touch_for_pos;
if let Some(touch_id_for_pos) = touch_id_for_pos {
// search for the touch we previously used for the position
// (unfortunately, `event.touches()` is not a rust collection):
touch_for_pos = (0..event.touches().length())
.into_iter()
.map(|i| event.touches().get(i).unwrap())
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos);
} else {
touch_for_pos = None;
}
// Use the touch found above or pick the first, or return a default position if there is no
// touch at all. (The latter is not expected as the current method is only called when there is
// at least one touch.)
touch_for_pos
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
pos_from_touch(canvas_origin(canvas_id), &touch)
})
}
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 {
x: touch.page_x() as f32 - canvas_origin.x as f32,
y: touch.page_y() as f32 - canvas_origin.y as f32,
}
}
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
});
}
}
}
/// Web sends 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.
pub fn should_ignore_key(key: &str) -> bool {
let is_function_key = key.starts_with('F') && key.len() > 1;
is_function_key
|| matches!(
key,
"Alt"
| "ArrowDown"
| "ArrowLeft"
| "ArrowRight"
| "ArrowUp"
| "Backspace"
| "CapsLock"
| "ContextMenu"
| "Control"
| "Delete"
| "End"
| "Enter"
| "Esc"
| "Escape"
| "Help"
| "Home"
| "Insert"
| "Meta"
| "NumLock"
| "PageDown"
| "PageUp"
| "Pause"
| "ScrollLock"
| "Shift"
| "Tab"
)
}
/// 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.
pub fn translate_key(key: &str) -> Option<egui::Key> {
match key {
"ArrowDown" => Some(egui::Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp),
"Esc" | "Escape" => Some(egui::Key::Escape),
"Tab" => Some(egui::Key::Tab),
"Backspace" => Some(egui::Key::Backspace),
"Enter" => Some(egui::Key::Enter),
"Space" | " " => Some(egui::Key::Space),
"Help" | "Insert" => Some(egui::Key::Insert),
"Delete" => Some(egui::Key::Delete),
"Home" => Some(egui::Key::Home),
"End" => Some(egui::Key::End),
"PageUp" => Some(egui::Key::PageUp),
"PageDown" => Some(egui::Key::PageDown),
"0" => Some(egui::Key::Num0),
"1" => Some(egui::Key::Num1),
"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),
"b" | "B" => Some(egui::Key::B),
"c" | "C" => Some(egui::Key::C),
"d" | "D" => Some(egui::Key::D),
"e" | "E" => Some(egui::Key::E),
"f" | "F" => Some(egui::Key::F),
"g" | "G" => Some(egui::Key::G),
"h" | "H" => Some(egui::Key::H),
"i" | "I" => Some(egui::Key::I),
"j" | "J" => Some(egui::Key::J),
"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),
_ => None,
}
}
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
egui::Modifiers {
alt: event.alt_key(),
ctrl: event.ctrl_key(),
shift: event.shift_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
mac_cmd: event.meta_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
command: event.ctrl_key() || event.meta_key(),
}
}

View file

@ -17,8 +17,10 @@
pub mod backend; pub mod backend;
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
mod glow_wrapping; mod glow_wrapping;
mod input;
mod painter; mod painter;
pub mod screen_reader; pub mod screen_reader;
mod text_agent;
#[cfg(feature = "webgl")] #[cfg(feature = "webgl")]
pub mod webgl1; pub mod webgl1;
@ -31,14 +33,13 @@ use egui::mutex::Mutex;
pub use wasm_bindgen; pub use wasm_bindgen;
pub use web_sys; pub use web_sys;
use input::*;
pub use painter::Painter; pub use painter::Painter;
use std::cell::Cell;
use std::rc::Rc; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Current time in seconds (since undefined point in time) /// Current time in seconds (since undefined point in time)
@ -89,64 +90,6 @@ pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id)) .unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
} }
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
let rect = canvas.get_bounding_client_rect();
egui::Pos2 {
x: event.client_x() as f32 - rect.left() as f32,
y: event.client_y() as f32 - rect.top() as f32,
}
}
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
match event.button() {
0 => Some(egui::PointerButton::Primary),
1 => Some(egui::PointerButton::Middle),
2 => Some(egui::PointerButton::Secondary),
_ => None,
}
}
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
/// should not jump to a different position. Therefore, we do not calculate the average position
/// of all touches, but we keep using the same touch as long as it is available.
///
/// `touch_id_for_pos` is the `TouchId` of the `Touch` we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> egui::Pos2 {
let touch_for_pos;
if let Some(touch_id_for_pos) = touch_id_for_pos {
// search for the touch we previously used for the position
// (unfortunately, `event.touches()` is not a rust collection):
touch_for_pos = (0..event.touches().length())
.into_iter()
.map(|i| event.touches().get(i).unwrap())
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos);
} else {
touch_for_pos = None;
}
// Use the touch found above or pick the first, or return a default position if there is no
// touch at all. (The latter is not expected as the current method is only called when there is
// at least one touch.)
touch_for_pos
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
pos_from_touch(canvas_origin(canvas_id), &touch)
})
}
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 {
x: touch.page_x() as f32 - canvas_origin.x as f32,
y: touch.page_y() as f32 - canvas_origin.y as f32,
}
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 { fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
let rect = canvas_element(canvas_id) let rect = canvas_element(canvas_id)
.unwrap() .unwrap()
@ -154,21 +97,6 @@ fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
egui::Pos2::new(rect.left() as f32, rect.top() as f32) egui::Pos2::new(rect.left() as f32, rect.top() as f32)
} }
fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
});
}
}
}
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
let canvas = canvas_element(canvas_id).unwrap(); let canvas = canvas_element(canvas_id).unwrap();
let pixels_per_point = native_pixels_per_point(); let pixels_per_point = native_pixels_per_point();
@ -353,108 +281,23 @@ pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
Some(()) Some(())
} }
/// e.g. "#fragment" part of "www.example.com/index.html#fragment" /// e.g. "#fragment" part of "www.example.com/index.html#fragment",
pub fn location_hash() -> Option<String> { ///
web_sys::window()?.location().hash().ok() /// Percent decoded
pub fn location_hash() -> String {
percent_decode(
&web_sys::window()
.unwrap()
.location()
.hash()
.unwrap_or_default(),
)
} }
/// Web sends all keys as strings, so it is up to us to figure out if it is pub fn percent_decode(s: &str) -> String {
/// a real text input or the name of a key. percent_encoding::percent_decode_str(s)
fn should_ignore_key(key: &str) -> bool { .decode_utf8_lossy()
let is_function_key = key.starts_with('F') && key.len() > 1; .to_string()
is_function_key
|| matches!(
key,
"Alt"
| "ArrowDown"
| "ArrowLeft"
| "ArrowRight"
| "ArrowUp"
| "Backspace"
| "CapsLock"
| "ContextMenu"
| "Control"
| "Delete"
| "End"
| "Enter"
| "Esc"
| "Escape"
| "Help"
| "Home"
| "Insert"
| "Meta"
| "NumLock"
| "PageDown"
| "PageUp"
| "Pause"
| "ScrollLock"
| "Shift"
| "Tab"
)
}
/// 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.
pub fn translate_key(key: &str) -> Option<egui::Key> {
match key {
"ArrowDown" => Some(egui::Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp),
"Esc" | "Escape" => Some(egui::Key::Escape),
"Tab" => Some(egui::Key::Tab),
"Backspace" => Some(egui::Key::Backspace),
"Enter" => Some(egui::Key::Enter),
"Space" | " " => Some(egui::Key::Space),
"Help" | "Insert" => Some(egui::Key::Insert),
"Delete" => Some(egui::Key::Delete),
"Home" => Some(egui::Key::Home),
"End" => Some(egui::Key::End),
"PageUp" => Some(egui::Key::PageUp),
"PageDown" => Some(egui::Key::PageDown),
"0" => Some(egui::Key::Num0),
"1" => Some(egui::Key::Num1),
"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),
"b" | "B" => Some(egui::Key::B),
"c" | "C" => Some(egui::Key::C),
"d" | "D" => Some(egui::Key::D),
"e" | "E" => Some(egui::Key::E),
"f" | "F" => Some(egui::Key::F),
"g" | "G" => Some(egui::Key::G),
"h" | "H" => Some(egui::Key::H),
"i" | "I" => Some(egui::Key::I),
"j" | "J" => Some(egui::Key::J),
"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),
_ => None,
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -490,18 +333,6 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
request_animation_frame(runner_ref) request_animation_frame(runner_ref)
} }
fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id(AGENT_ID)
.unwrap()
.dyn_into()
.unwrap()
}
fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
@ -533,7 +364,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
&& !modifiers.command && !modifiers.command
&& !should_ignore_key(&key) && !should_ignore_key(&key)
// When text agent is shown, it sends text event instead. // When text agent is shown, it sends text event instead.
&& text_agent().hidden() && text_agent::text_agent().hidden()
{ {
runner_lock.input.raw.events.push(egui::Event::Text(key)); runner_lock.input.raw.events.push(egui::Event::Text(key));
} }
@ -661,7 +492,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
if let Some(web_info) = &mut frame_lock.info.web_info { if let Some(web_info) = &mut frame_lock.info.web_info {
web_info.web_location_hash = location_hash().unwrap_or_default(); web_info.location.hash = location_hash();
} }
}) as Box<dyn FnMut()>); }) as Box<dyn FnMut()>);
window.add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())?; window.add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())?;
@ -688,117 +519,6 @@ fn repaint_every_ms(runner_ref: &AppRunnerRef, milliseconds: i32) -> Result<(),
Ok(()) Ok(())
} }
fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
egui::Modifiers {
alt: event.alt_key(),
ctrl: event.ctrl_key(),
shift: event.shift_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
mac_cmd: event.meta_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
command: event.ctrl_key() || event.meta_key(),
}
}
///
/// Text event handler,
fn install_text_agent(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
let input = std::rc::Rc::new(input);
input.set_id(AGENT_ID);
let is_composing = Rc::new(Cell::new(false));
{
let style = input.style();
// Transparent
style.set_property("opacity", "0").unwrap();
// Hide under canvas
style.set_property("z-index", "-1").unwrap();
}
// Set size as small as possible, in case user may click on it.
input.set_size(1);
input.set_autofocus(true);
input.set_hidden(true);
{
// When IME is off
let input_clone = input.clone();
let runner_ref = runner_ref.clone();
let is_composing = is_composing.clone();
let on_input = Closure::wrap(Box::new(move |_event: web_sys::InputEvent| {
let text = input_clone.value();
if !text.is_empty() && !is_composing.get() {
input_clone.set_value("");
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.set_true();
}
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("input", on_input.as_ref().unchecked_ref())?;
on_input.forget();
}
{
// When IME is on, handle composition event
let input_clone = input.clone();
let runner_ref = runner_ref.clone();
let on_compositionend = Closure::wrap(Box::new(move |event: web_sys::CompositionEvent| {
let mut runner_lock = runner_ref.0.lock();
let opt_event = match event.type_().as_ref() {
"compositionstart" => {
is_composing.set(true);
input_clone.set_value("");
Some(egui::Event::CompositionStart)
}
"compositionend" => {
is_composing.set(false);
input_clone.set_value("");
event.data().map(egui::Event::CompositionEnd)
}
"compositionupdate" => event.data().map(egui::Event::CompositionUpdate),
s => {
tracing::error!("Unknown composition event type: {:?}", s);
None
}
};
if let Some(event) = opt_event {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.set_true();
}
}) as Box<dyn FnMut(_)>);
let f = on_compositionend.as_ref().unchecked_ref();
input.add_event_listener_with_callback("compositionstart", f)?;
input.add_event_listener_with_callback("compositionupdate", f)?;
input.add_event_listener_with_callback("compositionend", f)?;
on_compositionend.forget();
}
{
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
let on_focusout = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("focusout", on_focusout.as_ref().unchecked_ref())?;
on_focusout.forget();
}
body.append_child(&input)?;
Ok(())
}
fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap(); let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap();
@ -880,7 +600,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
}); });
runner_lock.needs_repaint.set_true(); runner_lock.needs_repaint.set_true();
update_text_agent(&runner_lock); text_agent::update_text_agent(&runner_lock);
} }
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -988,7 +708,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
} }
// Finally, focus or blur text agent to toggle mobile keyboard: // Finally, focus or blur text agent to toggle mobile keyboard:
update_text_agent(&runner_lock); text_agent::update_text_agent(&runner_lock);
}) as Box<dyn FnMut(_)>); }) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget(); closure.forget();
@ -1158,92 +878,6 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
Ok(()) Ok(())
} }
/// Focus or blur text agent to toggle mobile keyboard.
fn update_text_agent(runner: &AppRunner) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
let canvas_style = canvas_element(runner.canvas_id())?.style();
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
if is_already_editing {
input.set_hidden(false);
input.focus().ok()?;
// Move up canvas so that text edit is shown at ~30% of screen height.
// Only on touch screens, when keyboard popups.
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
let window_height = window.inner_height().ok()?.as_f64()? as f32;
let current_rel = latest_touch_pos.y / window_height;
// estimated amount of screen covered by keyboard
let keyboard_fraction = 0.5;
if current_rel > keyboard_fraction {
// below the keyboard
let target_rel = 0.3;
// Note: `delta` is negative, since we are moving the canvas UP
let delta = target_rel - current_rel;
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
let new_pos_percent = (delta * 100.0).round().to_string() + "%";
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", &new_pos_percent).ok()?;
}
}
}
} else {
input.blur().ok()?;
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
}
Some(())
}
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
/// If context is running under mobile device?
fn is_mobile() -> Option<bool> {
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
Some(is_mobile)
}
// Move text agent to text cursor's position, on desktop/laptop,
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
let canvas = canvas_element(canvas_id)?;
let bounding_rect = text_agent().get_bounding_client_rect();
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
// Canvas is translated 50% horizontally in html.
let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?;
style.set_property("top", &(y.to_string() + "px")).ok()?;
style.set_property("left", &(x.to_string() + "px")).ok()
})
} else {
style.set_property("position", "absolute").ok()?;
style.set_property("top", "0px").ok()?;
style.set_property("left", "0px").ok()
}
}
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool { pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794 // See https://github.com/emilk/egui/issues/794

View file

@ -22,4 +22,23 @@ pub trait Painter {
) -> Result<(), JsValue>; ) -> Result<(), JsValue>;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn paint_and_update_textures(
&mut self,
clipped_meshes: Vec<egui::ClippedMesh>,
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> {
for (id, image_delta) in &textures_delta.set {
self.set_texture(*id, image_delta);
}
self.paint_meshes(clipped_meshes, pixels_per_point)?;
for &id in &textures_delta.free {
self.free_texture(id);
}
Ok(())
}
} }

202
egui_web/src/text_agent.rs Normal file
View file

@ -0,0 +1,202 @@
//! The text agent is an `<input>` element used to trigger
//! mobile keyboard and IME input.
use crate::{canvas_element, AppRunner, AppRunnerRef};
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id(AGENT_ID)
.unwrap()
.dyn_into()
.unwrap()
}
/// Text event handler,
pub fn install_text_agent(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
let input = std::rc::Rc::new(input);
input.set_id(AGENT_ID);
let is_composing = Rc::new(Cell::new(false));
{
let style = input.style();
// Transparent
style.set_property("opacity", "0").unwrap();
// Hide under canvas
style.set_property("z-index", "-1").unwrap();
}
// Set size as small as possible, in case user may click on it.
input.set_size(1);
input.set_autofocus(true);
input.set_hidden(true);
{
// When IME is off
let input_clone = input.clone();
let runner_ref = runner_ref.clone();
let is_composing = is_composing.clone();
let on_input = Closure::wrap(Box::new(move |_event: web_sys::InputEvent| {
let text = input_clone.value();
if !text.is_empty() && !is_composing.get() {
input_clone.set_value("");
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.set_true();
}
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("input", on_input.as_ref().unchecked_ref())?;
on_input.forget();
}
{
// When IME is on, handle composition event
let input_clone = input.clone();
let runner_ref = runner_ref.clone();
let on_compositionend = Closure::wrap(Box::new(move |event: web_sys::CompositionEvent| {
let mut runner_lock = runner_ref.0.lock();
let opt_event = match event.type_().as_ref() {
"compositionstart" => {
is_composing.set(true);
input_clone.set_value("");
Some(egui::Event::CompositionStart)
}
"compositionend" => {
is_composing.set(false);
input_clone.set_value("");
event.data().map(egui::Event::CompositionEnd)
}
"compositionupdate" => event.data().map(egui::Event::CompositionUpdate),
s => {
tracing::error!("Unknown composition event type: {:?}", s);
None
}
};
if let Some(event) = opt_event {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.set_true();
}
}) as Box<dyn FnMut(_)>);
let f = on_compositionend.as_ref().unchecked_ref();
input.add_event_listener_with_callback("compositionstart", f)?;
input.add_event_listener_with_callback("compositionupdate", f)?;
input.add_event_listener_with_callback("compositionend", f)?;
on_compositionend.forget();
}
{
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
let on_focusout = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("focusout", on_focusout.as_ref().unchecked_ref())?;
on_focusout.forget();
}
body.append_child(&input)?;
Ok(())
}
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: &AppRunner) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
let canvas_style = canvas_element(runner.canvas_id())?.style();
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
if is_already_editing {
input.set_hidden(false);
input.focus().ok()?;
// Move up canvas so that text edit is shown at ~30% of screen height.
// Only on touch screens, when keyboard popups.
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
let window_height = window.inner_height().ok()?.as_f64()? as f32;
let current_rel = latest_touch_pos.y / window_height;
// estimated amount of screen covered by keyboard
let keyboard_fraction = 0.5;
if current_rel > keyboard_fraction {
// below the keyboard
let target_rel = 0.3;
// Note: `delta` is negative, since we are moving the canvas UP
let delta = target_rel - current_rel;
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
let new_pos_percent = (delta * 100.0).round().to_string() + "%";
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", &new_pos_percent).ok()?;
}
}
}
} else {
input.blur().ok()?;
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
}
Some(())
}
/// If context is running under mobile device?
fn is_mobile() -> Option<bool> {
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
Some(is_mobile)
}
// Move text agent to text cursor's position, on desktop/laptop,
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
let canvas = canvas_element(canvas_id)?;
let bounding_rect = text_agent().get_bounding_client_rect();
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
// Canvas is translated 50% horizontally in html.
let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?;
style.set_property("top", &(y.to_string() + "px")).ok()?;
style.set_property("left", &(x.to_string() + "px")).ok()
})
} else {
style.set_property("position", "absolute").ok()?;
style.set_property("top", "0px").ok()?;
style.set_property("left", "0px").ok()
}
}

View file

@ -1,6 +1,6 @@
[package] [package]
name = "emath" name = "emath"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D math library for GUI work" description = "Minimal 2D math library for GUI work"
edition = "2021" edition = "2021"

View file

@ -1,15 +1,20 @@
# epaint changelog # epaint changelog
All notable changes to the epaint crate will be documented in this file. All notable changes to the epaint crate will be documented in this file.
## Unreleased ## Unreleased
## 0.17.0 - 2022-02-22
* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)): * Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)):
* Replaced `TextStyle` with `FontId` which lets you pick any font size and font family. * Replaced `TextStyle` with `FontId` which lets you pick any font size and font family.
* Replaced `Fonts::font_image` with `font_image_delta` for partial font atlas updates. * Replaced `Fonts::font_image` with `font_image_delta` for partial font atlas updates.
* Made the v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)).
* Added `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)). * Added `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)).
* Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)). * Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)).
* Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)). * Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)).
* Fix anti-aliasing of filled paths with counter-clockwise winding order.
* Improve the anti-aliasing of filled paths with sharp corners, at the cost of these corners sometimes becoming badly extruded instead (see https://github.com/emilk/egui/issues/1226).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29

View file

@ -1,6 +1,6 @@
[package] [package]
name = "epaint" name = "epaint"
version = "0.16.0" version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D graphics library for GUI work" description = "Minimal 2D graphics library for GUI work"
edition = "2021" edition = "2021"
@ -55,7 +55,7 @@ multi_threaded = ["parking_lot"]
[dependencies] [dependencies]
emath = { version = "0.16.0", path = "../emath" } emath = { version = "0.17.0", path = "../emath" }
ab_glyph = "0.2.11" ab_glyph = "0.2.11"
ahash = { version = "0.7", features = ["std"], default-features = false } ahash = { version = "0.7", features = ["std"], default-features = false }
@ -63,7 +63,7 @@ atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_
bytemuck = { version = "1.7.2", features = ["derive"], optional = true } bytemuck = { version = "1.7.2", features = ["derive"], optional = true }
cint = { version = "^0.2.2", optional = true } cint = { version = "^0.2.2", optional = true }
nohash-hasher = "0.2" nohash-hasher = "0.2"
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. parking_lot = { version = "0.12", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
serde = { version = "1", features = ["derive", "rc"], optional = true } serde = { version = "1", features = ["derive", "rc"], optional = true }
[dev-dependencies] [dev-dependencies]

View file

@ -6,9 +6,9 @@ use emath::*;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// How to paint a cubic Bezier curve on screen. /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
/// The definition: [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). ///
/// This implementation is only for cubic Bezier curve, or the Bezier curve of degree 3. /// See also [`QuadraticBezierShape`].
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CubicBezierShape { pub struct CubicBezierShape {
@ -22,30 +22,29 @@ pub struct CubicBezierShape {
} }
impl CubicBezierShape { impl CubicBezierShape {
/// Creates a cubic Bezier curve based on 4 points and stroke. /// Creates a cubic Bézier curve based on 4 points and stroke.
///
/// The first point is the starting point and the last one is the ending point of the curve. /// The first point is the starting point and the last one is the ending point of the curve.
/// The middle points are the control points. /// The middle points are the control points.
/// The number of points must be 4.
pub fn from_points_stroke( pub fn from_points_stroke(
points: Vec<Pos2>, points: [Pos2; 4],
closed: bool, closed: bool,
fill: Color32, fill: Color32,
stroke: impl Into<Stroke>, stroke: impl Into<Stroke>,
) -> Self { ) -> Self {
crate::epaint_assert!(points.len() == 4, "Cubic needs 4 points");
Self { Self {
points: points.try_into().unwrap(), points,
closed, closed,
fill, fill,
stroke: stroke.into(), stroke: stroke.into(),
} }
} }
/// Creates a cubic Bezier curve based on the screen coordinates for the 4 points. /// Transform the curve with the given transform.
pub fn to_screen(&self, to_screen: &RectTransform) -> Self { pub fn transform(&self, transform: &RectTransform) -> Self {
let mut points = [Pos2::default(); 4]; let mut points = [Pos2::default(); 4];
for (i, origin_point) in self.points.iter().enumerate() { for (i, origin_point) in self.points.iter().enumerate() {
points[i] = to_screen * *origin_point; points[i] = transform * *origin_point;
} }
CubicBezierShape { CubicBezierShape {
points, points,
@ -55,12 +54,12 @@ impl CubicBezierShape {
} }
} }
/// Convert the cubic Bezier curve to one or two `PathShapes`. /// Convert the cubic Bézier curve to one or two `PathShapes`.
/// When the curve is closed and it has to intersect with the base line, it will be converted into two shapes. /// When the curve is closed and it has to intersect with the base line, it will be converted into two shapes.
/// Otherwise, it will be converted into one shape. /// Otherwise, it will be converted into one shape.
/// The `tolerance` will be used to control the max distance between the curve and the base line. /// The `tolerance` will be used to control the max distance between the curve and the base line.
/// The `epsilon` is used when comparing two floats. /// The `epsilon` is used when comparing two floats.
pub fn to_pathshapes(&self, tolerance: Option<f32>, epsilon: Option<f32>) -> Vec<PathShape> { pub fn to_path_shapes(&self, tolerance: Option<f32>, epsilon: Option<f32>) -> Vec<PathShape> {
let mut pathshapes = Vec::new(); let mut pathshapes = Vec::new();
let mut points_vec = self.flatten_closed(tolerance, epsilon); let mut points_vec = self.flatten_closed(tolerance, epsilon);
for points in points_vec.drain(..) { for points in points_vec.drain(..) {
@ -74,8 +73,18 @@ impl CubicBezierShape {
} }
pathshapes pathshapes
} }
/// Screen-space bounding rectangle.
pub fn bounding_rect(&self) -> Rect { /// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
self.logical_bounding_rect().expand(self.stroke.width / 2.0)
}
}
/// Logical bounding rectangle (ignoring stroke width)
pub fn logical_bounding_rect(&self) -> Rect {
//temporary solution //temporary solution
let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x { let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x {
(self.points[0].x, self.points[3].x) (self.points[0].x, self.points[3].x)
@ -256,9 +265,9 @@ impl CubicBezierShape {
None None
} }
/// Calculate the point (x,y) at t based on the cubic bezier curve equation. /// Calculate the point (x,y) at t based on the cubic zier curve equation.
/// t is in [0.0,1.0] /// t is in [0.0,1.0]
/// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves) /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves)
/// ///
pub fn sample(&self, t: f32) -> Pos2 { pub fn sample(&self, t: f32) -> Pos2 {
crate::epaint_assert!( crate::epaint_assert!(
@ -278,7 +287,7 @@ impl CubicBezierShape {
result.to_pos2() result.to_pos2()
} }
/// find a set of points that approximate the cubic bezier curve. /// find a set of points that approximate the cubic zier curve.
/// the number of points is determined by the tolerance. /// the number of points is determined by the tolerance.
/// the points may not be evenly distributed in the range [0.0,1.0] (t value) /// the points may not be evenly distributed in the range [0.0,1.0] (t value)
pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> { pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> {
@ -290,7 +299,7 @@ impl CubicBezierShape {
result result
} }
/// find a set of points that approximate the cubic bezier curve. /// find a set of points that approximate the cubic zier curve.
/// the number of points is determined by the tolerance. /// the number of points is determined by the tolerance.
/// the points may not be evenly distributed in the range [0.0,1.0] (t value) /// the points may not be evenly distributed in the range [0.0,1.0] (t value)
/// this api will check whether the curve will cross the base line or not when closed = true. /// this api will check whether the curve will cross the base line or not when closed = true.
@ -358,6 +367,11 @@ impl From<CubicBezierShape> for Shape {
} }
} }
// ----------------------------------------------------------------------------
/// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
///
/// See also [`CubicBezierShape`].
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct QuadraticBezierShape { pub struct QuadraticBezierShape {
@ -371,32 +385,30 @@ pub struct QuadraticBezierShape {
} }
impl QuadraticBezierShape { impl QuadraticBezierShape {
/// create a new quadratic bezier shape based on the 3 points and stroke. /// Create a new quadratic Bézier shape based on the 3 points and stroke.
/// the first point is the starting point and the last one is the ending point of the curve.
/// the middle point is the control points.
/// the points should be in the order [start, control, end]
/// ///
/// The first point is the starting point and the last one is the ending point of the curve.
/// The middle point is the control points.
/// The points should be in the order [start, control, end]
pub fn from_points_stroke( pub fn from_points_stroke(
points: Vec<Pos2>, points: [Pos2; 3],
closed: bool, closed: bool,
fill: Color32, fill: Color32,
stroke: impl Into<Stroke>, stroke: impl Into<Stroke>,
) -> Self { ) -> Self {
crate::epaint_assert!(points.len() == 3, "Quadratic needs 3 points");
QuadraticBezierShape { QuadraticBezierShape {
points: points.try_into().unwrap(), // it's safe to unwrap because we just checked points,
closed, closed,
fill, fill,
stroke: stroke.into(), stroke: stroke.into(),
} }
} }
/// create a new quadratic bezier shape based on the screen coordination for the 3 points. /// Transform the curve with the given transform.
pub fn to_screen(&self, to_screen: &RectTransform) -> Self { pub fn transform(&self, transform: &RectTransform) -> Self {
let mut points = [Pos2::default(); 3]; let mut points = [Pos2::default(); 3];
for (i, origin_point) in self.points.iter().enumerate() { for (i, origin_point) in self.points.iter().enumerate() {
points[i] = to_screen * *origin_point; points[i] = transform * *origin_point;
} }
QuadraticBezierShape { QuadraticBezierShape {
points, points,
@ -406,9 +418,9 @@ impl QuadraticBezierShape {
} }
} }
/// Convert the quadratic Bezier curve to one `PathShape`. /// Convert the quadratic Bézier curve to one `PathShape`.
/// The `tolerance` will be used to control the max distance between the curve and the base line. /// The `tolerance` will be used to control the max distance between the curve and the base line.
pub fn to_pathshape(&self, tolerance: Option<f32>) -> PathShape { pub fn to_path_shape(&self, tolerance: Option<f32>) -> PathShape {
let points = self.flatten(tolerance); let points = self.flatten(tolerance);
PathShape { PathShape {
points, points,
@ -417,8 +429,18 @@ impl QuadraticBezierShape {
stroke: self.stroke, stroke: self.stroke,
} }
} }
/// bounding box of the quadratic bezier shape
pub fn bounding_rect(&self) -> Rect { /// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
self.logical_bounding_rect().expand(self.stroke.width / 2.0)
}
}
/// Logical bounding rectangle (ignoring stroke width)
pub fn logical_bounding_rect(&self) -> Rect {
let (mut min_x, mut max_x) = if self.points[0].x < self.points[2].x { let (mut min_x, mut max_x) = if self.points[0].x < self.points[2].x {
(self.points[0].x, self.points[2].x) (self.points[0].x, self.points[2].x)
} else { } else {
@ -466,9 +488,9 @@ impl QuadraticBezierShape {
} }
} }
/// Calculate the point (x,y) at t based on the quadratic bezier curve equation. /// Calculate the point (x,y) at t based on the quadratic zier curve equation.
/// t is in [0.0,1.0] /// t is in [0.0,1.0]
/// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves) /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves)
/// ///
pub fn sample(&self, t: f32) -> Pos2 { pub fn sample(&self, t: f32) -> Pos2 {
crate::epaint_assert!( crate::epaint_assert!(
@ -486,7 +508,7 @@ impl QuadraticBezierShape {
result.to_pos2() result.to_pos2()
} }
/// find a set of points that approximate the quadratic bezier curve. /// find a set of points that approximate the quadratic zier curve.
/// the number of points is determined by the tolerance. /// the number of points is determined by the tolerance.
/// the points may not be evenly distributed in the range [0.0,1.0] (t value) /// the points may not be evenly distributed in the range [0.0,1.0] (t value)
pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> { pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> {
@ -533,6 +555,8 @@ impl From<QuadraticBezierShape> for Shape {
} }
} }
// ----------------------------------------------------------------------------
// lyon_geom::flatten_cubic.rs // lyon_geom::flatten_cubic.rs
// copied from https://docs.rs/lyon_geom/latest/lyon_geom/ // copied from https://docs.rs/lyon_geom/latest/lyon_geom/
fn flatten_cubic_bezier_with_t<F: FnMut(Pos2, f32)>( fn flatten_cubic_bezier_with_t<F: FnMut(Pos2, f32)>(
@ -567,6 +591,7 @@ fn flatten_cubic_bezier_with_t<F: FnMut(Pos2, f32)>(
callback(point, t); callback(point, t);
}); });
} }
// from lyon_geom::quadratic_bezier.rs // from lyon_geom::quadratic_bezier.rs
// copied from https://docs.rs/lyon_geom/latest/lyon_geom/ // copied from https://docs.rs/lyon_geom/latest/lyon_geom/
struct FlatteningParameters { struct FlatteningParameters {
@ -665,7 +690,7 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape
} }
fn quadratic_for_each_local_extremum<F: FnMut(f32)>(p0: f32, p1: f32, p2: f32, cb: &mut F) { fn quadratic_for_each_local_extremum<F: FnMut(f32)>(p0: f32, p1: f32, p2: f32, cb: &mut F) {
// A quadratic bezier curve can be derived by a linear function: // A quadratic zier curve can be derived by a linear function:
// p(t) = p0 + t(p1 - p0) + t^2(p2 - 2p1 + p0) // p(t) = p0 + t(p1 - p0) + t^2(p2 - 2p1 + p0)
// The derivative is: // The derivative is:
// p'(t) = (p1 - p0) + 2(p2 - 2p1 + p0)t or: // p'(t) = (p1 - p0) + 2(p2 - 2p1 + p0)t or:
@ -685,7 +710,7 @@ fn quadratic_for_each_local_extremum<F: FnMut(f32)>(p0: f32, p1: f32, p2: f32, c
fn cubic_for_each_local_extremum<F: FnMut(f32)>(p0: f32, p1: f32, p2: f32, p3: f32, cb: &mut F) { fn cubic_for_each_local_extremum<F: FnMut(f32)>(p0: f32, p1: f32, p2: f32, p3: f32, cb: &mut F) {
// See www.faculty.idc.ac.il/arik/quality/appendixa.html for an explanation // See www.faculty.idc.ac.il/arik/quality/appendixa.html for an explanation
// A cubic bezier curve can be derivated by the following equation: // A cubic zier curve can be derivated by the following equation:
// B'(t) = 3(1-t)^2(p1-p0) + 6(1-t)t(p2-p1) + 3t^2(p3-p2) or // B'(t) = 3(1-t)^2(p1-p0) + 6(1-t)t(p2-p1) + 3t^2(p3-p2) or
// f(x) = a * x² + b * x + c // f(x) = a * x² + b * x + c
let a = 3.0 * (p3 + 3.0 * (p1 - p2) - p0); let a = 3.0 * (p3 + 3.0 * (p1 - p2) - p0);
@ -748,7 +773,7 @@ mod tests {
fill: Default::default(), fill: Default::default(),
stroke: Default::default(), stroke: Default::default(),
}; };
let bbox = curve.bounding_rect(); let bbox = curve.logical_bounding_rect();
assert!((bbox.min.x - 72.96).abs() < 0.01); assert!((bbox.min.x - 72.96).abs() < 0.01);
assert!((bbox.min.y - 27.78).abs() < 0.01); assert!((bbox.min.y - 27.78).abs() < 0.01);
@ -772,7 +797,7 @@ mod tests {
fill: Default::default(), fill: Default::default(),
stroke: Default::default(), stroke: Default::default(),
}; };
let bbox = curve.bounding_rect(); let bbox = curve.logical_bounding_rect();
assert!((bbox.min.x - 10.0).abs() < 0.01); assert!((bbox.min.x - 10.0).abs() < 0.01);
assert!((bbox.min.y - 10.0).abs() < 0.01); assert!((bbox.min.y - 10.0).abs() < 0.01);
@ -841,7 +866,7 @@ mod tests {
stroke: Default::default(), stroke: Default::default(),
}; };
let bbox = curve.bounding_rect(); let bbox = curve.logical_bounding_rect();
assert_eq!(bbox.min.x, 10.0); assert_eq!(bbox.min.x, 10.0);
assert_eq!(bbox.min.y, 10.0); assert_eq!(bbox.min.y, 10.0);
assert_eq!(bbox.max.x, 270.0); assert_eq!(bbox.max.x, 270.0);
@ -859,7 +884,7 @@ mod tests {
stroke: Default::default(), stroke: Default::default(),
}; };
let bbox = curve.bounding_rect(); let bbox = curve.logical_bounding_rect();
assert_eq!(bbox.min.x, 10.0); assert_eq!(bbox.min.x, 10.0);
assert_eq!(bbox.min.y, 10.0); assert_eq!(bbox.min.y, 10.0);
assert!((bbox.max.x - 206.50).abs() < 0.01); assert!((bbox.max.x - 206.50).abs() < 0.01);
@ -877,7 +902,7 @@ mod tests {
stroke: Default::default(), stroke: Default::default(),
}; };
let bbox = curve.bounding_rect(); let bbox = curve.logical_bounding_rect();
assert!((bbox.min.x - 86.71).abs() < 0.01); assert!((bbox.min.x - 86.71).abs() < 0.01);
assert!((bbox.min.y - 30.0).abs() < 0.01); assert!((bbox.min.y - 30.0).abs() < 0.01);

View file

@ -31,6 +31,20 @@ pub enum Shape {
CubicBezier(CubicBezierShape), CubicBezier(CubicBezierShape),
} }
impl From<Vec<Shape>> for Shape {
#[inline(always)]
fn from(shapes: Vec<Shape>) -> Self {
Self::Vec(shapes)
}
}
impl From<Mesh> for Shape {
#[inline(always)]
fn from(mesh: Mesh) -> Self {
Self::Mesh(mesh)
}
}
/// ## Constructors /// ## Constructors
impl Shape { impl Shape {
/// A line between two points. /// A line between two points.
@ -59,25 +73,25 @@ impl Shape {
/// Turn a line into equally spaced dots. /// Turn a line into equally spaced dots.
pub fn dotted_line( pub fn dotted_line(
points: &[Pos2], path: &[Pos2],
color: impl Into<Color32>, color: impl Into<Color32>,
spacing: f32, spacing: f32,
radius: f32, radius: f32,
) -> Vec<Self> { ) -> Vec<Self> {
let mut shapes = Vec::new(); let mut shapes = Vec::new();
points_from_line(points, spacing, radius, color.into(), &mut shapes); points_from_line(path, spacing, radius, color.into(), &mut shapes);
shapes shapes
} }
/// Turn a line into dashes. /// Turn a line into dashes.
pub fn dashed_line( pub fn dashed_line(
points: &[Pos2], path: &[Pos2],
stroke: impl Into<Stroke>, stroke: impl Into<Stroke>,
dash_length: f32, dash_length: f32,
gap_length: f32, gap_length: f32,
) -> Vec<Self> { ) -> Vec<Self> {
let mut shapes = Vec::new(); let mut shapes = Vec::new();
dashes_from_line(points, stroke.into(), dash_length, gap_length, &mut shapes); dashes_from_line(path, stroke.into(), dash_length, gap_length, &mut shapes);
shapes shapes
} }
@ -94,6 +108,8 @@ impl Shape {
} }
/// A convex polygon with a fill and optional stroke. /// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline] #[inline]
pub fn convex_polygon( pub fn convex_polygon(
points: Vec<Pos2>, points: Vec<Pos2>,
@ -154,6 +170,34 @@ impl Shape {
crate::epaint_assert!(mesh.is_valid()); crate::epaint_assert!(mesh.is_valid());
Self::Mesh(mesh) Self::Mesh(mesh)
} }
/// The visual bounding rectangle (includes stroke widths)
pub fn visual_bounding_rect(&self) -> Rect {
match self {
Self::Noop => Rect::NOTHING,
Self::Vec(shapes) => {
let mut rect = Rect::NOTHING;
for shape in shapes {
rect = rect.union(shape.visual_bounding_rect());
}
rect
}
Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(),
Self::LineSegment { points, stroke } => {
if stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_two_pos(points[0], points[1]).expand(stroke.width / 2.0)
}
}
Self::Path(path_shape) => path_shape.visual_bounding_rect(),
Self::Rect(rect_shape) => rect_shape.visual_bounding_rect(),
Self::Text(text_shape) => text_shape.visual_bounding_rect(),
Self::Mesh(mesh) => mesh.calc_bounds(),
Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(),
Self::CubicBezier(bezier) => bezier.visual_bounding_rect(),
}
}
} }
/// ## Inspection and transforms /// ## Inspection and transforms
@ -244,6 +288,18 @@ impl CircleShape {
stroke: stroke.into(), stroke: stroke.into(),
} }
} }
/// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_center_size(
self.center,
Vec2::splat(self.radius + self.stroke.width / 2.0),
)
}
}
} }
impl From<CircleShape> for Shape { impl From<CircleShape> for Shape {
@ -259,6 +315,7 @@ impl From<CircleShape> for Shape {
#[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 PathShape { pub struct PathShape {
/// Filled paths should prefer clockwise order.
pub points: Vec<Pos2>, pub points: Vec<Pos2>,
/// If true, connect the first and last of the points together. /// If true, connect the first and last of the points together.
/// This is required if `fill != TRANSPARENT`. /// This is required if `fill != TRANSPARENT`.
@ -294,6 +351,8 @@ impl PathShape {
} }
/// A convex polygon with a fill and optional stroke. /// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline] #[inline]
pub fn convex_polygon( pub fn convex_polygon(
points: Vec<Pos2>, points: Vec<Pos2>,
@ -308,10 +367,14 @@ impl PathShape {
} }
} }
/// Screen-space bounding rectangle. /// The visual bounding rectangle (includes stroke width)
#[inline] #[inline]
pub fn bounding_rect(&self) -> Rect { pub fn visual_bounding_rect(&self) -> Rect {
Rect::from_points(&self.points).expand(self.stroke.width) if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_points(&self.points).expand(self.stroke.width / 2.0)
}
} }
} }
@ -360,10 +423,14 @@ impl RectShape {
} }
} }
/// Screen-space bounding rectangle. /// The visual bounding rectangle (includes stroke width)
#[inline] #[inline]
pub fn bounding_rect(&self) -> Rect { pub fn visual_bounding_rect(&self) -> Rect {
self.rect.expand(self.stroke.width) if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
self.rect.expand(self.stroke.width / 2.0)
}
} }
} }
@ -439,6 +506,7 @@ impl Rounding {
/// How to paint some text on screen. /// How to paint some text on screen.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextShape { pub struct TextShape {
/// Top left corner of the first character. /// Top left corner of the first character.
pub pos: Pos2, pub pos: Pos2,
@ -455,7 +523,7 @@ pub struct TextShape {
/// This will NOT replace background color nor strikethrough/underline color. /// This will NOT replace background color nor strikethrough/underline color.
pub override_text_color: Option<Color32>, pub override_text_color: Option<Color32>,
/// Rotate text by this many radians clock-wise. /// Rotate text by this many radians clockwise.
/// The pivot is `pos` (the upper left corner of the text). /// The pivot is `pos` (the upper left corner of the text).
pub angle: f32, pub angle: f32,
} }
@ -472,9 +540,9 @@ impl TextShape {
} }
} }
/// Screen-space bounding rectangle. /// The visual bounding rectangle
#[inline] #[inline]
pub fn bounding_rect(&self) -> Rect { pub fn visual_bounding_rect(&self) -> Rect {
self.galley.mesh_bounds.translate(self.pos.to_vec2()) self.galley.mesh_bounds.translate(self.pos.to_vec2())
} }
} }
@ -490,16 +558,15 @@ impl From<TextShape> for Shape {
/// Creates equally spaced filled circles from a line. /// Creates equally spaced filled circles from a line.
fn points_from_line( fn points_from_line(
line: &[Pos2], path: &[Pos2],
spacing: f32, spacing: f32,
radius: f32, radius: f32,
color: Color32, color: Color32,
shapes: &mut Vec<Shape>, shapes: &mut Vec<Shape>,
) { ) {
let mut position_on_segment = 0.0; let mut position_on_segment = 0.0;
line.windows(2).for_each(|window| { path.windows(2).for_each(|window| {
let start = window[0]; let (start, end) = (window[0], window[1]);
let end = window[1];
let vector = end - start; let vector = end - start;
let segment_length = vector.length(); let segment_length = vector.length();
while position_on_segment < segment_length { while position_on_segment < segment_length {
@ -513,7 +580,7 @@ fn points_from_line(
/// Creates dashes from a line. /// Creates dashes from a line.
fn dashes_from_line( fn dashes_from_line(
line: &[Pos2], path: &[Pos2],
stroke: Stroke, stroke: Stroke,
dash_length: f32, dash_length: f32,
gap_length: f32, gap_length: f32,
@ -521,9 +588,8 @@ fn dashes_from_line(
) { ) {
let mut position_on_segment = 0.0; let mut position_on_segment = 0.0;
let mut drawing_dash = false; let mut drawing_dash = false;
line.windows(2).for_each(|window| { path.windows(2).for_each(|window| {
let start = window[0]; let (start, end) = (window[0], window[1]);
let end = window[1];
let vector = end - start; let vector = end - start;
let segment_length = vector.length(); let segment_length = vector.length();

View file

@ -133,9 +133,20 @@ impl Path {
let normal = (n0 + n1) / 2.0; let normal = (n0 + n1) / 2.0;
let length_sq = normal.length_sq(); let length_sq = normal.length_sq();
// We can't just cut off corners for filled shapes like this,
// because the feather will both expand and contract the corner along the provided normals
// to make sure it doesn't grow, and the shrinking will make the inner points cross each other.
//
// A better approach is to shrink the vertices in by half the feather-width here
// and then only expand during feathering.
//
// See https://github.com/emilk/egui/issues/1226
const CUT_OFF_SHARP_CORNERS: bool = false;
let right_angle_length_sq = 0.5; let right_angle_length_sq = 0.5;
let sharper_than_a_right_angle = length_sq < right_angle_length_sq; let sharper_than_a_right_angle = length_sq < right_angle_length_sq;
if sharper_than_a_right_angle { if CUT_OFF_SHARP_CORNERS && sharper_than_a_right_angle {
// cut off the sharp corner // cut off the sharp corner
let center_normal = normal.normalized(); let center_normal = normal.normalized();
let n0c = (n0 + center_normal) / 2.0; let n0c = (n0 + center_normal) / 2.0;
@ -172,8 +183,12 @@ impl Path {
} }
/// The path is taken to be closed (i.e. returning to the start again). /// The path is taken to be closed (i.e. returning to the start again).
pub fn fill(&self, color: Color32, options: &TessellationOptions, out: &mut Mesh) { ///
fill_closed_path(&self.0, color, options, out); /// Calling this may reverse the vertices in the path if they are wrong winding order.
///
/// The preferred winding order is clockwise.
pub fn fill(&mut self, color: Color32, options: &TessellationOptions, out: &mut Mesh) {
fill_closed_path(&mut self.0, color, options, out);
} }
} }
@ -196,10 +211,10 @@ pub mod path {
let min = rect.min; let min = rect.min;
let max = rect.max; let max = rect.max;
path.reserve(4); path.reserve(4);
path.push(pos2(min.x, min.y)); path.push(pos2(min.x, min.y)); // left top
path.push(pos2(max.x, min.y)); path.push(pos2(max.x, min.y)); // right top
path.push(pos2(max.x, max.y)); path.push(pos2(max.x, max.y)); // right bottom
path.push(pos2(min.x, max.y)); path.push(pos2(min.x, max.y)); // left bottom
} else { } else {
add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0);
add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0); add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0);
@ -346,9 +361,27 @@ impl TessellationOptions {
} }
} }
fn cw_signed_area(path: &[PathPoint]) -> f64 {
if let Some(last) = path.last() {
let mut previous = last.pos;
let mut area = 0.0;
for p in path {
area += (previous.x * p.pos.y - p.pos.x * previous.y) as f64;
previous = p.pos;
}
area
} else {
0.0
}
}
/// Tessellate the given convex area into a polygon. /// Tessellate the given convex area into a polygon.
///
/// Calling this may reverse the vertices in the path if they are wrong winding order.
///
/// The preferred winding order is clockwise.
fn fill_closed_path( fn fill_closed_path(
path: &[PathPoint], path: &mut [PathPoint],
color: Color32, color: Color32,
options: &TessellationOptions, options: &TessellationOptions,
out: &mut Mesh, out: &mut Mesh,
@ -359,14 +392,26 @@ fn fill_closed_path(
let n = path.len() as u32; let n = path.len() as u32;
if options.anti_alias { if options.anti_alias {
if cw_signed_area(path) < 0.0 {
// Wrong winding order - fix:
path.reverse();
for point in path.iter_mut() {
point.normal = -point.normal;
}
}
out.reserve_triangles(3 * n as usize); out.reserve_triangles(3 * n as usize);
out.reserve_vertices(2 * n as usize); out.reserve_vertices(2 * n as usize);
let color_outer = Color32::TRANSPARENT; let color_outer = Color32::TRANSPARENT;
let idx_inner = out.vertices.len() as u32; let idx_inner = out.vertices.len() as u32;
let idx_outer = idx_inner + 1; let idx_outer = idx_inner + 1;
// The fill:
for i in 2..n { for i in 2..n {
out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i); out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i);
} }
// The feathering:
let mut i0 = n - 1; let mut i0 = n - 1;
for i1 in 0..n { for i1 in 0..n {
let p1 = &path[i1 as usize]; let p1 = &path[i1 as usize];
@ -748,7 +793,7 @@ impl Tessellator {
let clip_rect = self.clip_rect; let clip_rect = self.clip_rect;
if options.coarse_tessellation_culling if options.coarse_tessellation_culling
&& !quadratic_shape.bounding_rect().intersects(clip_rect) && !quadratic_shape.visual_bounding_rect().intersects(clip_rect)
{ {
return; return;
} }
@ -771,7 +816,8 @@ impl Tessellator {
) { ) {
let options = &self.options; let options = &self.options;
let clip_rect = self.clip_rect; let clip_rect = self.clip_rect;
if options.coarse_tessellation_culling && !cubic_shape.bounding_rect().intersects(clip_rect) if options.coarse_tessellation_culling
&& !cubic_shape.visual_bounding_rect().intersects(clip_rect)
{ {
return; return;
} }
@ -825,7 +871,7 @@ impl Tessellator {
} }
if self.options.coarse_tessellation_culling if self.options.coarse_tessellation_culling
&& !path_shape.bounding_rect().intersects(self.clip_rect) && !path_shape.visual_bounding_rect().intersects(self.clip_rect)
{ {
return; return;
} }
@ -1026,16 +1072,20 @@ pub fn tessellate_shapes(
if options.debug_paint_clip_rects { if options.debug_paint_clip_rects {
for ClippedMesh(clip_rect, mesh) in &mut clipped_meshes { for ClippedMesh(clip_rect, mesh) in &mut clipped_meshes {
tessellator.clip_rect = Rect::EVERYTHING; if mesh.texture_id == TextureId::default() {
tessellator.tessellate_shape( tessellator.clip_rect = Rect::EVERYTHING;
tex_size, tessellator.tessellate_shape(
Shape::rect_stroke( tex_size,
*clip_rect, Shape::rect_stroke(
0.0, *clip_rect,
Stroke::new(2.0, Color32::from_rgb(150, 255, 150)), 0.0,
), Stroke::new(2.0, Color32::from_rgb(150, 255, 150)),
mesh, ),
); mesh,
);
} else {
// TODO: create a new `ClippedMesh` just for the painted clip rectangle
}
} }
} }

View file

@ -9,6 +9,7 @@ use std::collections::BTreeSet;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct UvRect { pub struct UvRect {
/// X/Y offset for nice rendering (unit: points). /// X/Y offset for nice rendering (unit: points).
pub offset: Vec2, pub offset: Vec2,
@ -56,6 +57,7 @@ impl Default for GlyphInfo {
/// A specific font with a size. /// A specific font with a size.
/// The interface uses points as the unit for everything. /// The interface uses points as the unit for everything.
pub struct FontImpl { pub struct FontImpl {
name: String,
ab_glyph_font: ab_glyph::FontArc, ab_glyph_font: ab_glyph::FontArc,
/// Maximum character height /// Maximum character height
scale_in_pixels: u32, scale_in_pixels: u32,
@ -71,24 +73,28 @@ impl FontImpl {
pub fn new( pub fn new(
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32, pixels_per_point: f32,
name: String,
ab_glyph_font: ab_glyph::FontArc, ab_glyph_font: ab_glyph::FontArc,
scale_in_pixels: u32, scale_in_pixels: u32,
y_offset: f32, y_offset_points: f32,
) -> FontImpl { ) -> FontImpl {
assert!(scale_in_pixels > 0); assert!(scale_in_pixels > 0);
assert!(pixels_per_point > 0.0); assert!(pixels_per_point > 0.0);
let height_in_points = scale_in_pixels as f32 / pixels_per_point; let height_in_points = scale_in_pixels as f32 / pixels_per_point;
// TODO: use v_metrics for line spacing ? // TODO: use these font metrics?
// let v = rusttype_font.v_metrics(Scale::uniform(scale_in_pixels)); // use ab_glyph::ScaleFont as _;
// let height_in_pixels = v.ascent - v.descent + v.line_gap; // let scaled = ab_glyph_font.as_scaled(scale_in_pixels as f32);
// let height_in_points = height_in_pixels / pixels_per_point; // dbg!(scaled.ascent());
// dbg!(scaled.descent());
// dbg!(scaled.line_gap());
// Round to closest pixel: // Round to closest pixel:
let y_offset = (y_offset * pixels_per_point).round() / pixels_per_point; let y_offset = (y_offset_points * pixels_per_point).round() / pixels_per_point;
Self { Self {
name,
ab_glyph_font, ab_glyph_font,
scale_in_pixels, scale_in_pixels,
height_in_points, height_in_points,
@ -99,22 +105,32 @@ impl FontImpl {
} }
} }
fn ignore_character(&self, chr: char) -> bool {
if self.name == "emoji-icon-font" {
// HACK: https://github.com/emilk/egui/issues/1284 https://github.com/jslegers/emoji-icon-font/issues/18
// Don't show the wrong fullwidth capital letters:
if '' <= chr && chr <= '' {
return true;
}
}
matches!(
chr,
// Strip out a religious symbol with secondary nefarious interpretation:
'\u{534d}' | '\u{5350}' |
// Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
)
}
/// An un-ordered iterator over all supported characters. /// An un-ordered iterator over all supported characters.
fn characters(&self) -> impl Iterator<Item = char> + '_ { fn characters(&self) -> impl Iterator<Item = char> + '_ {
use ab_glyph::Font as _; use ab_glyph::Font as _;
self.ab_glyph_font self.ab_glyph_font
.codepoint_ids() .codepoint_ids()
.map(|(_, chr)| chr) .map(|(_, chr)| chr)
.filter(|chr| { .filter(|&chr| !self.ignore_character(chr))
!matches!(
chr,
// Strip out a religious symbol with secondary nefarious interpretation:
'\u{534d}' | '\u{5350}' |
// Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
)
})
} }
/// `\n` will result in `None` /// `\n` will result in `None`
@ -125,9 +141,9 @@ impl FontImpl {
} }
} }
// Add new character: if self.ignore_character(c) {
use ab_glyph::Font as _; return None;
let glyph_id = self.ab_glyph_font.glyph_id(c); }
if c == '\t' { if c == '\t' {
if let Some(space) = self.glyph_info(' ') { if let Some(space) = self.glyph_info(' ') {
@ -140,6 +156,10 @@ impl FontImpl {
} }
} }
// Add new character:
use ab_glyph::Font as _;
let glyph_id = self.ab_glyph_font.glyph_id(c);
if glyph_id.0 == 0 { if glyph_id.0 == 0 {
if invisible_char(c) { if invisible_char(c) {
// hack // hack
@ -147,7 +167,7 @@ impl FontImpl {
self.glyph_info_cache.write().insert(c, glyph_info); self.glyph_info_cache.write().insert(c, glyph_info);
Some(glyph_info) Some(glyph_info)
} else { } else {
None None // unsupported character
} }
} else { } else {
let glyph_info = allocate_glyph( let glyph_info = allocate_glyph(

View file

@ -120,6 +120,9 @@ pub struct FontData {
/// Which font face in the file to use. /// Which font face in the file to use.
/// When in doubt, use `0`. /// When in doubt, use `0`.
pub index: u32, pub index: u32,
/// Extra scale and vertical tweak to apply to all text of this font.
pub tweak: FontTweak,
} }
impl FontData { impl FontData {
@ -127,6 +130,7 @@ impl FontData {
Self { Self {
font: std::borrow::Cow::Borrowed(font), font: std::borrow::Cow::Borrowed(font),
index: 0, index: 0,
tweak: Default::default(),
} }
} }
@ -134,10 +138,52 @@ impl FontData {
Self { Self {
font: std::borrow::Cow::Owned(font), font: std::borrow::Cow::Owned(font),
index: 0, index: 0,
tweak: Default::default(),
}
}
pub fn tweak(self, tweak: FontTweak) -> Self {
Self { tweak, ..self }
}
}
// ----------------------------------------------------------------------------
/// Extra scale and vertical tweak to apply to all text of a certain font.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FontTweak {
/// Scale the font by this much.
///
/// Default: `1.0` (no scaling).
pub scale: f32,
/// Shift font downwards by this fraction of the font size (in points).
///
/// A positive value shifts the text downwards.
/// A negative value shifts it upwards.
///
/// Example value: `-0.2`.
pub y_offset_factor: f32,
/// Shift font downwards by this amount of logical points.
///
/// Example value: `2.0`.
pub y_offset: f32,
}
impl Default for FontTweak {
fn default() -> Self {
Self {
scale: 1.0,
y_offset_factor: -0.2, // makes the default fonts look more centered in buttons and such
y_offset: 0.0,
} }
} }
} }
// ----------------------------------------------------------------------------
fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc { fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc {
match &data.font { match &data.font {
std::borrow::Cow::Borrowed(bytes) => { std::borrow::Cow::Borrowed(bytes) => {
@ -220,10 +266,17 @@ impl Default for FontDefinitions {
"NotoEmoji-Regular".to_owned(), "NotoEmoji-Regular".to_owned(),
FontData::from_static(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")), FontData::from_static(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")),
); );
// Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>: // Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>:
font_data.insert( font_data.insert(
"emoji-icon-font".to_owned(), "emoji-icon-font".to_owned(),
FontData::from_static(include_bytes!("../../fonts/emoji-icon-font.ttf")), FontData::from_static(include_bytes!("../../fonts/emoji-icon-font.ttf")).tweak(
FontTweak {
scale: 0.8, // make it smaller
y_offset_factor: 0.07, // move it down slightly
y_offset: 0.0,
},
),
); );
families.insert( families.insert(
@ -603,7 +656,7 @@ impl GalleyCache {
struct FontImplCache { struct FontImplCache {
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32, pixels_per_point: f32,
ab_glyph_fonts: BTreeMap<String, ab_glyph::FontArc>, ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>,
/// Map font pixel sizes and names to the cached `FontImpl`. /// Map font pixel sizes and names to the cached `FontImpl`.
cache: ahash::AHashMap<(u32, String), Arc<FontImpl>>, cache: ahash::AHashMap<(u32, String), Arc<FontImpl>>,
@ -617,7 +670,11 @@ impl FontImplCache {
) -> Self { ) -> Self {
let ab_glyph_fonts = font_data let ab_glyph_fonts = font_data
.iter() .iter()
.map(|(name, font_data)| (name.clone(), ab_glyph_font_from_font_data(name, font_data))) .map(|(name, font_data)| {
let tweak = font_data.tweak;
let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
(name.clone(), (tweak, ab_glyph))
})
.collect(); .collect();
Self { Self {
@ -638,35 +695,29 @@ impl FontImplCache {
} }
pub fn font_impl(&mut self, scale_in_pixels: u32, font_name: &str) -> Arc<FontImpl> { pub fn font_impl(&mut self, scale_in_pixels: u32, font_name: &str) -> Arc<FontImpl> {
let scale_in_pixels = if font_name == "emoji-icon-font" { let (tweak, ab_glyph_font) = self
(scale_in_pixels as f32 * 0.8).round() as u32 // TODO: remove font scale HACK! .ab_glyph_fonts
} else { .get(font_name)
scale_in_pixels .unwrap_or_else(|| panic!("No font data found for {:?}", font_name))
}; .clone();
let y_offset = if font_name == "emoji-icon-font" { let scale_in_pixels = (scale_in_pixels as f32 * tweak.scale).round() as u32;
let y_offset_points = {
let scale_in_points = scale_in_pixels as f32 / self.pixels_per_point; let scale_in_points = scale_in_pixels as f32 / self.pixels_per_point;
scale_in_points * 0.29375 // TODO: remove font alignment hack scale_in_points * tweak.y_offset_factor
} else { } + tweak.y_offset;
0.0
};
let y_offset = y_offset - 3.0; // Tweaked to make text look centered in buttons and text edit fields
self.cache self.cache
.entry((scale_in_pixels, font_name.to_owned())) .entry((scale_in_pixels, font_name.to_owned()))
.or_insert_with(|| { .or_insert_with(|| {
let ab_glyph_font = self
.ab_glyph_fonts
.get(font_name)
.unwrap_or_else(|| panic!("No font data found for {:?}", font_name))
.clone();
Arc::new(FontImpl::new( Arc::new(FontImpl::new(
self.atlas.clone(), self.atlas.clone(),
self.pixels_per_point, self.pixels_per_point,
font_name.to_owned(),
ab_glyph_font, ab_glyph_font,
scale_in_pixels, scale_in_pixels,
y_offset, y_offset_points,
)) ))
}) })
.clone() .clone()

View file

@ -10,7 +10,7 @@ mod text_layout_types;
pub const TAB_SIZE: usize = 4; pub const TAB_SIZE: usize = 4;
pub use { pub use {
fonts::{FontData, FontDefinitions, FontFamily, FontId, Fonts, FontsImpl}, fonts::{FontData, FontDefinitions, FontFamily, FontId, FontTweak, Fonts, FontsImpl},
text_layout::layout, text_layout::layout,
text_layout_types::*, text_layout_types::*,
}; };

View file

@ -261,6 +261,7 @@ impl TextFormat {
/// ///
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`]; /// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Galley { pub struct Galley {
/// The job that this galley is the result of. /// The job that this galley is the result of.
/// Contains the original string and style sections. /// Contains the original string and style sections.
@ -294,6 +295,7 @@ pub struct Galley {
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Row { pub struct Row {
/// One for each `char`. /// One for each `char`.
pub glyphs: Vec<Glyph>, pub glyphs: Vec<Glyph>,
@ -316,6 +318,7 @@ pub struct Row {
/// The tessellated output of a row. /// The tessellated output of a row.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RowVisuals { pub struct RowVisuals {
/// The tessellated text, using non-normalized (texel) UV coordinates. /// The tessellated text, using non-normalized (texel) UV coordinates.
/// That is, you need to divide the uv coordinates by the texture size. /// That is, you need to divide the uv coordinates by the texture size.
@ -341,6 +344,7 @@ impl Default for RowVisuals {
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Glyph { pub struct Glyph {
pub chr: char, pub chr: char,
/// Relative to the galley position. /// Relative to the galley position.

View file

@ -152,13 +152,51 @@ pub struct TexturesDelta {
/// New or changed textures. Apply before painting. /// New or changed textures. Apply before painting.
pub set: AHashMap<TextureId, ImageDelta>, pub set: AHashMap<TextureId, ImageDelta>,
/// Texture to free after painting. /// Textures to free after painting.
pub free: Vec<TextureId>, pub free: Vec<TextureId>,
} }
impl TexturesDelta { impl TexturesDelta {
pub fn is_empty(&self) -> bool {
self.set.is_empty() && self.free.is_empty()
}
pub fn append(&mut self, mut newer: TexturesDelta) { pub fn append(&mut self, mut newer: TexturesDelta) {
self.set.extend(newer.set.into_iter()); self.set.extend(newer.set.into_iter());
self.free.append(&mut newer.free); self.free.append(&mut newer.free);
} }
pub fn clear(&mut self) {
self.set.clear();
self.free.clear();
}
}
impl std::fmt::Debug for TexturesDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug_struct = f.debug_struct("TexturesDelta");
if !self.set.is_empty() {
let mut string = String::new();
for (tex_id, delta) in &self.set {
let size = delta.image.size();
if let Some(pos) = delta.pos {
string += &format!(
"{:?} partial ([{} {}] - [{} {}]), ",
tex_id,
pos[0],
pos[1],
pos[0] + size[0],
pos[1] + size[1]
);
} else {
string += &format!("{:?} full {}x{}, ", tex_id, size[0], size[1]);
}
}
debug_struct.field("set", &string);
}
if !self.free.is_empty() {
debug_struct.field("free", &self.free);
}
debug_struct.finish()
}
} }

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