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
override: true
- 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:
name: cargo doc web
@ -153,3 +153,17 @@ jobs:
steps:
- uses: actions/checkout@v2
- 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
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.
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).
### `egui_extras`
This adds additional features on top of `egui`.
### `epi`
Depends only on `egui`.
Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`.

View file

@ -1,59 +1,102 @@
# 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!
## Unreleased
## 0.17.0 - 2022-02-22 - Improved font selection and image handling
### Added ⭐
* 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`.
* Easily change text styles with `Style::text_styles`.
* Added `Ui::text_style_height`.
* Added `TextStyle::resolve`.
* `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)).
* 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)).
* 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 `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 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)).
* `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 `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
* Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)).
* 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 `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 🔧
* ⚠️ `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.
* 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)).
* `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)).
* Renamed `Ui::visible` to `Ui::is_visible`.
* 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)).
* `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 `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)).
* `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 🐛
* 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)).
* 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.
* 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 🙏
* [AlexxxRu](https://github.com/alexxxru): [#1108](https://github.com/emilk/egui/pull/1108).
* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050).
* [juancampa](https://github.com/juancampa): [#1147](https://github.com/emilk/egui/pull/1147).
* [4JX](https://github.com/4JX)
* [55nknown](https://github.com/55nknown)
* [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`
@ -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-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
* 2018-12-23: [initial commit](https://github.com/emilk/egui/commit/856bbf4dae4a69693a0324da34e8b0dd3754dfdf)
* 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

1595
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -148,7 +148,7 @@ Light Theme:
## 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.
An integration needs to do the following each frame:
@ -160,7 +160,7 @@ An integration needs to do the following each frame:
### 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:
@ -192,7 +192,7 @@ Missing an integration for the thing you're working on? Create one, it's easy!
### 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
let mut egui_ctx = egui::CtxRef::default();
@ -201,20 +201,19 @@ let mut egui_ctx = egui::CtxRef::default();
loop {
// Gather input (mouse, touches, keyboard, screen size, etc):
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
});
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(clipped_meshes);
my_integration.free_egui_textures(&output.textures_delta.free);
my_integration.paint(&full_output.textures_delta, clipped_meshes);
my_integration.set_cursor_icon(output.cursor_icon);
if !output.copied_text.is_empty() {
my_integration.set_clipboard_text(output.copied_text);
let platform_output = full_output.platform_output;
my_integration.set_cursor_icon(platform_output.cursor_icon);
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`.
### 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?
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 = [
{ 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 = [
{ 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);
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:
wasm_bindgen.start("the_canvas_id");
console.debug("egui app started.");
console.debug("app started.");
document.getElementById("center_text").remove();
}
function on_wasm_error(error) {
console.error("Failed to start egui: " + error);
console.error("Failed to start: " + error);
document.getElementById("center_text").innerHTML = `
<p>
An error occurred loading egui
An error occurred during loading:
</p>
<p style="font-family:Courier New">
${error}

View file

@ -5,6 +5,9 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
## 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)).
* 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)).
@ -12,8 +15,10 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
* Fix horizontal scrolling direction on Linux.
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
* 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)).
* 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

View file

@ -1,6 +1,6 @@
[package]
name = "eframe"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021"
@ -49,27 +49,24 @@ screen_reader = [
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false }
epi = { version = "0.16.0", path = "../epi" }
egui = { version = "0.17.0", path = "../egui", default-features = false }
epi = { version = "0.17.0", path = "../epi" }
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.16.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_glow = { version = "0.16.0", path = "../egui_glow", default-features = false, features = ["clipboard", "epi", "links", "winit"], optional = true }
egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false }
egui_glium = { version = "0.17.0", path = "../egui_glium", default-features = false, features = ["clipboard", "epi", "links"], optional = true }
egui_glow = { version = "0.17.0", path = "../egui_glow", default-features = false, features = ["clipboard", "epi", "links", "winit"], optional = true }
# web:
[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]
# For examples:
egui_extras = { path = "../egui_extras", features = ["image", "svg"] }
ehttp = "0.2"
image = { version = "0.24", default-features = false, features = ["jpeg", "png"] }
poll-promise = "0.1"
rfd = "0.7"
# svg.rs example:
resvg = "0.20"
tiny-skia = "0.6"
usvg = "0.20"
rfd = "0.8"

View file

@ -1,6 +1,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi};
use egui_extras::RetainedImage;
use poll_promise::Promise;
fn main() {
@ -11,7 +12,7 @@ fn main() {
#[derive(Default)]
struct MyApp {
/// `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 {
@ -24,14 +25,13 @@ impl epi::App for MyApp {
// Begin download.
// 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.
let ctx = ctx.clone();
let frame = frame.clone();
let (sender, promise) = Promise::new();
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
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
let texture = response.and_then(|response| parse_response(&ctx, response));
sender.send(texture); // send the results back to the UI thread.
});
promise
});
@ -43,24 +43,17 @@ impl epi::App for MyApp {
Some(Err(err)) => {
ui.colored_label(egui::Color32::RED, err); // something went wrong
}
Some(Ok(texture)) => {
let mut size = texture.size_vec2();
size *= (ui.available_width() / size.x).min(1.0);
size *= (ui.available_height() / size.y).min(1.0);
ui.image(texture, size);
Some(Ok(image)) => {
image.show_max_size(ui, ui.available_size());
}
});
}
}
fn parse_response(
ctx: &egui::Context,
response: ehttp::Response,
) -> Result<egui::TextureHandle, String> {
fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> {
let content_type = response.content_type().unwrap_or_default();
if content_type.starts_with("image/") {
let image = load_image(&response.bytes).map_err(|err| err.to_string())?;
Ok(ctx.load_texture("my-image", image))
RetainedImage::from_image_bytes(&response.url, &response.bytes)
} else {
Err(format!(
"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) {
let Self { name, age } = self;
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
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() {
*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:

View file

@ -1,10 +1,22 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi};
use egui_extras::RetainedImage;
#[derive(Default)]
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 {
@ -13,17 +25,15 @@ impl epi::App for MyApp {
}
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| {
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.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();
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.
//!
//! 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};
/// 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 {
svg_image: SvgImage,
svg_image: egui_extras::RetainedImage,
}
impl Default for MyApp {
fn default() -> 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
All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
## 0.17.0 - 2022-02-22
* 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))
* 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)).
* 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

View file

@ -1,6 +1,6 @@
[package]
name = "egui-winit"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition = "2021"
@ -43,17 +43,17 @@ convert_bytemuck = ["egui/convert_bytemuck"]
[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"] }
tracing = "0.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 }
dark-light = { version = "0.2.1", optional = true } # detect dark mode system preference
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.5", optional = true }
webbrowser = { version = "0.6", optional = true }
# feature screen_reader
tts = { version = "0.20", optional = true }

View file

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

View file

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

View file

@ -1,8 +1,8 @@
[package]
name = "egui"
version = "0.16.1"
version = "0.17.0"
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"
rust-version = "1.56"
homepage = "https://github.com/emilk/egui"
@ -58,7 +58,7 @@ multi_threaded = ["epaint/multi_threaded"]
[dependencies]
epaint = { version = "0.16.0", path = "../epaint", default-features = false }
epaint = { version = "0.17.0", path = "../epaint", default-features = false }
ahash = "0.7"
nohash-hasher = "0.2"

View file

@ -204,7 +204,7 @@ impl Area {
let state = ctx.memory().areas.get(id).cloned();
let is_new = state.is_none();
if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place}
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
}
let mut state = state.unwrap_or_else(|| State {
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
@ -212,6 +212,7 @@ impl Area {
interactable,
});
state.pos = new_pos.unwrap_or(state.pos);
state.interactable = interactable;
if let Some((anchor, offset)) = anchor {
if is_new {

View file

@ -1,51 +1,8 @@
//! Frame container
use crate::{layers::ShapeIdx, *};
use crate::{layers::ShapeIdx, style::Margin, *};
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`].
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[must_use = "You should call .show()"]

View file

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

View file

@ -9,6 +9,8 @@
//! 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.
//!
//! You must never open one top-level panel from within another panel. Add one panel, then the next.
//!
//! Always add any [`CentralPanel`] last.
//!
//! Add your [`Window`]:s after any top-level panels.

View file

@ -81,6 +81,7 @@ pub struct ScrollArea {
has_bar: [bool; 2],
auto_shrink: [bool; 2],
max_size: Vec2,
min_scrolled_size: Vec2,
always_show_scroll: bool,
id_source: Option<Id>,
offset_x: Option<f32>,
@ -123,6 +124,7 @@ impl ScrollArea {
has_bar,
auto_shrink: [true; 2],
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
always_show_scroll: false,
id_source: None,
offset_x: None,
@ -152,6 +154,28 @@ impl ScrollArea {
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 `true`, the scroll bar will always be displayed even if not needed.
pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
@ -288,6 +312,7 @@ impl ScrollArea {
has_bar,
auto_shrink,
max_size,
min_scrolled_size,
always_show_scroll,
id_source,
offset_x,
@ -329,27 +354,39 @@ impl ScrollArea {
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 mut inner_child_max_size = inner_size;
let mut content_max_size = inner_size;
if true {
// 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 {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if has_bar[d] {
inner_child_max_size[d] = f32::INFINITY;
content_max_size[d] = f32::INFINITY;
}
}
}
let mut content_ui = ui.child_ui(
Rect::from_min_size(inner_rect.min - state.offset, inner_child_max_size),
*ui.layout(),
);
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
// Nice handling of forced resizing beyond the possible:
@ -489,18 +526,38 @@ impl Prepared {
// We take the scroll target so only this ScrollArea will use it:
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
if let Some((scroll, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
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 delta = if let Some(align) = align {
let center_factor = align.to_factor();
let min = content_ui.min_rect().min[d];
let visible_range = min..=min + content_ui.clip_rect().size()[d];
let offset = scroll - lerp(visible_range, center_factor);
let mut spacing = ui.spacing().item_spacing[d];
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);
state.offset[d] = offset + spacing;
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)]
use crate::{
animation_manager::AnimationManager, data::output::Output, frame_state::FrameState,
input_state::*, layers::GraphicLayers, memory::Options, TextureHandle, *,
animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
input_state::*, layers::GraphicLayers, memory::Options, output::FullOutput, TextureHandle, *,
};
use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *};
@ -42,7 +42,7 @@ struct ContextImpl {
// The output of a frame:
graphics: GraphicLayers,
output: Output,
output: PlatformOutput,
paint_stats: PaintStats,
@ -79,7 +79,7 @@ impl ContextImpl {
/// Load fonts unless already loaded.
fn update_fonts_mut(&mut self) {
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() {
let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions);
@ -108,7 +108,7 @@ impl ContextImpl {
/// Your handle to 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`] uses refcounting internally).
@ -121,14 +121,14 @@ impl ContextImpl {
/// # Example:
///
/// ``` no_run
/// # fn handle_output(_: egui::Output) {}
/// # fn paint(_: Vec<egui::ClippedMesh>) {}
/// # fn handle_platform_output(_: egui::PlatformOutput) {}
/// # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedMesh>) {}
/// let mut ctx = egui::Context::default();
///
/// // Game loop:
/// loop {
/// 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| {
/// ui.label("Hello world!");
/// if ui.button("Click me").clicked() {
@ -136,9 +136,9 @@ impl ContextImpl {
/// }
/// });
/// });
/// let clipped_meshes = ctx.tessellate(shapes); // create triangles to paint
/// handle_output(output);
/// paint(clipped_meshes);
/// handle_platform_output(full_output.platform_output);
/// let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint
/// paint(full_output.textures_delta, clipped_meshes);
/// }
/// ```
#[derive(Clone)]
@ -185,19 +185,15 @@ impl Context {
///
/// // Each frame:
/// 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| {
/// ui.label("Hello egui!");
/// });
/// });
/// // handle output, paint shapes
/// // handle full_output
/// ```
#[must_use]
pub fn run(
&self,
new_input: RawInput,
run_ui: impl FnOnce(&Context),
) -> (Output, Vec<ClippedShape>) {
pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Context)) -> FullOutput {
self.begin_frame(new_input);
run_ui(self);
self.end_frame()
@ -217,8 +213,8 @@ impl Context {
/// ui.label("Hello egui!");
/// });
///
/// let (output, shapes) = ctx.end_frame();
/// // handle output, paint shapes
/// let full_output = ctx.end_frame();
/// // handle full_output
/// ```
pub fn begin_frame(&self, new_input: RawInput) {
self.write().begin_frame_mut(new_input);
@ -463,8 +459,13 @@ impl Context {
}
/// What egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output().cursor_icon = egui::CursorIcon::Progress;
/// ```
#[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, Output> {
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
RwLockWriteGuard::map(self.write(), |c| &mut c.output)
}
@ -719,14 +720,13 @@ impl Context {
impl Context {
/// 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]
pub fn end_frame(&self) -> (Output, Vec<ClippedShape>) {
pub fn end_frame(&self) -> FullOutput {
if self.input().wants_repaint() {
self.request_repaint();
}
let textures_delta;
{
let ctx_impl = &mut *self.write();
ctx_impl
@ -742,20 +742,26 @@ impl Context {
.set(TextureId::default(), font_image_delta);
}
ctx_impl
.output
.textures_delta
.append(ctx_impl.tex_manager.0.write().take_delta());
}
textures_delta = ctx_impl.tex_manager.0.write().take_delta();
};
let mut output: Output = std::mem::take(&mut self.output());
if self.read().repaint_requests > 0 {
let platform_output: PlatformOutput = std::mem::take(&mut self.output());
let needs_repaint = if self.read().repaint_requests > 0 {
self.write().repaint_requests -= 1;
output.needs_repaint = true;
}
true
} else {
false
};
let shapes = self.drain_paint_lists();
(output, shapes)
FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
}
}
fn drain_paint_lists(&self) -> Vec<ClippedShape> {
@ -889,6 +895,17 @@ impl Context {
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 {
let pointer_pos = self.input().pointer.interact_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`.
///
/// 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.
/// 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 {
screen_rect: None,
pixels_per_point: None,
max_texture_side: 2048,
max_texture_side: None,
time: None,
predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(),
@ -89,7 +89,7 @@ impl RawInput {
RawInput {
screen_rect: self.screen_rect.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(),
predicted_dt: self.predicted_dt,
modifiers: self.modifiers,
@ -115,7 +115,7 @@ impl RawInput {
self.screen_rect = screen_rect.or(self.screen_rect);
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.predicted_dt = predicted_dt; // use latest dt
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`]).
Text(String),
/// A key was pressed or released.
Key {
key: Key,
/// Was it pressed or released?
pressed: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
},
/// The mouse or touch moved to a new place.
PointerMoved(Pos2),
/// A mouse button was pressed or released (or a touch started or stopped).
PointerButton {
/// Where is the pointer?
pos: Pos2,
/// What mouse button? For touches, use [`PointerButton::Primary`].
button: PointerButton,
/// Was it the button/touch pressed this frame, or released?
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,
},
/// 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
/// to lift-up
id: TouchId,
/// One of: start move end cancel.
phase: TouchPhase,
/// Position of the touch (or where the touch was last detected)
pos: Pos2,
@ -244,18 +254,24 @@ pub enum PointerButton {
pub const NUM_POINTER_BUTTONS: usize = 3;
/// 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))]
pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac).
pub alt: bool,
/// Either of the control keys are down.
/// When checking for keyboard shortcuts, consider using [`Self::command`] instead.
pub ctrl: bool,
/// Either of the shift keys are down.
pub shift: bool,
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
pub mac_cmd: bool,
/// 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`).
/// 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 {
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)]
pub fn is_none(&self) -> bool {
self == &Self::default()
@ -274,6 +340,7 @@ impl Modifiers {
!self.is_none()
}
/// Is shift the only pressed button?
#[inline(always)]
pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command)
@ -284,6 +351,66 @@ impl Modifiers {
pub fn command_only(&self) -> bool {
!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.
@ -382,7 +509,7 @@ impl RawInput {
.on_hover_text(
"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 {
ui.label(format!("time: {:.3} s", time));
} else {

View file

@ -2,11 +2,56 @@
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.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Output {
pub struct PlatformOutput {
/// Set the cursor to this icon.
pub cursor_icon: CursorIcon,
@ -18,14 +63,6 @@ pub struct Output {
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
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.
pub events: Vec<OutputEvent>,
@ -35,12 +72,9 @@ pub struct Output {
/// Screen-space position of text edit cursor (used for IME).
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.
/// If egui is running in a browser, the same tab will be reused.
pub fn open_url(&mut self, url: impl ToString) {
@ -70,11 +104,9 @@ impl Output {
cursor_icon,
open_url,
copied_text,
needs_repaint,
mut events,
mutable_text_under_cursor,
text_cursor_pos,
textures_delta,
} = newer;
self.cursor_icon = cursor_icon;
@ -84,11 +116,9 @@ impl Output {
if !copied_text.is_empty() {
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.mutable_text_under_cursor = mutable_text_under_cursor;
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)
@ -129,7 +159,7 @@ impl OpenUrl {
/// 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>.
#[derive(Clone, Copy, Debug, PartialEq)]

View file

@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use crate::*;
/// 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.
pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
/// horizontal, vertical
pub(crate) scroll_target: [Option<(f32, Align)>; 2],
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
}
impl Default for FrameState {
@ -40,7 +42,7 @@ impl Default for FrameState {
used_by_panels: Rect::NAN,
tooltip_rect: None,
scroll_delta: Vec2::ZERO,
scroll_target: [None; 2],
scroll_target: [None, None],
}
}
}
@ -63,7 +65,7 @@ impl FrameState {
*used_by_panels = Rect::NOTHING;
*tooltip_rect = None;
*scroll_delta = input.scroll_delta;
*scroll_target = [None; 2];
*scroll_target = [None, None];
}
/// 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.
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.
pub time: f64,
@ -82,6 +87,7 @@ impl Default for InputState {
zoom_factor_delta: 1.0,
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
pixels_per_point: 1.0,
max_texture_side: 2048,
time: 0.0,
unstable_dt: 1.0 / 6.0,
predicted_dt: 1.0 / 6.0,
@ -134,6 +140,7 @@ impl InputState {
zoom_factor_delta,
screen_rect,
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,
unstable_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()
}
/// 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?
pub fn key_pressed(&self, desired_key: Key) -> bool {
self.num_presses(desired_key) > 0
@ -692,6 +721,7 @@ impl InputState {
zoom_factor_delta,
screen_rect,
pixels_per_point,
max_texture_side,
time,
unstable_dt,
predicted_dt,
@ -724,9 +754,13 @@ impl InputState {
ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta));
ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!(
"{:?} physical pixels for each logical point",
"{} physical pixels for each logical 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 since previous frame: {:.1} ms",

View file

@ -146,7 +146,7 @@ impl Widget for &mut epaint::TessellationOptions {
epsilon: _,
} = self;
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(
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
.logarithmic(true)

View file

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

View file

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

View file

@ -105,7 +105,7 @@ pub struct Options {
pub tessellation_options: epaint::TessellationOptions,
/// This does not at all change the behavior of egui,
/// but is a signal to any backend that we want the [`crate::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.
pub screen_reader: bool,
@ -329,6 +329,12 @@ impl Memory {
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 {
self.interaction.focus.id_previous_frame == Some(id)
}
@ -516,13 +522,13 @@ impl Areas {
if state.interactable {
// Allow us to resize by dragging just outside the window:
rect = rect.expand(resize_interact_radius_side);
}
if rect.contains(pos) {
return Some(*layer);
}
}
}
}
}
None
}

View file

@ -126,7 +126,7 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id)
.order(Order::Foreground)
.fixed_pos(pos)
.interactable(false)
.interactable(true)
.drag_bounds(Rect::EVERYTHING);
let inner_response = area.show(ctx, |ui| {
ui.scope(|ui| {

View file

@ -1,5 +1,5 @@
use crate::{
emath::{lerp, Align, Pos2, Rect, Vec2},
emath::{Align, Pos2, Rect, Vec2},
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
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| {
@ -451,18 +455,15 @@ impl Response {
/// for i in 0..1000 {
/// let response = ui.button("Scroll to me");
/// 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) {
let scroll_target = lerp(self.rect.x_range(), align.to_factor());
self.ctx.frame_state().scroll_target[0] = Some((scroll_target, align));
let scroll_target = lerp(self.rect.y_range(), align.to_factor());
self.ctx.frame_state().scroll_target[1] = Some((scroll_target, align));
pub fn scroll_to_me(&self, align: Option<Align>) {
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
}
/// For accessibility.
@ -509,6 +510,8 @@ impl Response {
impl Response {
/// A logical "or" operation.
/// 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 {
assert!(self.ctx == other.ctx);
crate::egui_assert!(

View file

@ -2,7 +2,7 @@
#![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 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.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -564,7 +607,7 @@ impl Visuals {
selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
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),
window_rounding: Rounding::same(6.0),
window_shadow: Shadow::big_dark(),
@ -585,9 +628,9 @@ impl Visuals {
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
faint_bg_color: Color32::from_gray(240),
extreme_bg_color: Color32::from_gray(250),
code_bg_color: Color32::from_gray(200),
faint_bg_color: Color32::from_gray(245),
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
code_bg_color: Color32::from_gray(230),
window_shadow: Shadow::big_light(),
popup_shadow: Shadow::small_light(),
..Self::dark()
@ -666,21 +709,21 @@ impl Widgets {
pub fn light() -> Self {
Self {
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
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),
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(215), // button background
bg_fill: Color32::from_gray(230), // button background
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),
expansion: 0.0,
},
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
fg_stroke: Stroke::new(1.5, Color32::BLACK),
rounding: Rounding::same(3.0),

View file

@ -338,6 +338,21 @@ impl Ui {
self.ctx().input()
}
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input_mut()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings
/// like for [`Self::input()`].
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
/// # });
/// ```
#[inline]
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
self.ctx().input_mut()
}
/// The [`Memory`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().memory()`.
#[inline]
@ -351,10 +366,10 @@ impl Ui {
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()`.
#[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, Output> {
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
self.ctx().output()
}
@ -889,7 +904,36 @@ impl Ui {
(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;
@ -901,15 +945,16 @@ impl Ui {
/// }
///
/// 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();
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));
}
}
}

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 {
#[inline]
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);
ui.painter().add(Shape::convex_polygon(
vec![
pos2(x - r, rect.bottom()),
pos2(x + r, rect.bottom()),
pos2(x, rect.center().y),
pos2(x, rect.center().y), // tip
pos2(x + r, rect.bottom()), // right bottom
pos2(x - r, rect.bottom()), // left bottom
],
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) {
let area_response = Area::new(popup_id)
.order(Order::Foreground)
.default_pos(button_response.rect.max)
.current_pos(button_response.rect.max)
.show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = 210.0;
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)
};
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 mut response = if is_kb_editing {

View file

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

View file

@ -1,6 +1,6 @@
//! Simple plotting library.
use std::{cell::RefCell, rc::Rc};
use std::{cell::RefCell, ops::RangeInclusive, rc::Rc};
use crate::*;
use epaint::ahash::AHashSet;
@ -20,12 +20,44 @@ mod items;
mod legend;
mod transform;
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>;
type AxisFormatterFn = dyn Fn(f64) -> String;
type LabelFormatterFn = dyn Fn(&str, &Value) -> String;
type LabelFormatter = Option<Box<LabelFormatterFn>>;
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
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.
@ -146,7 +178,8 @@ pub struct Plot {
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2],
legend_config: Option<Legend>,
show_background: bool,
@ -177,7 +210,8 @@ impl Plot {
show_x: true,
show_y: true,
custom_label_func: None,
label_formatter: None,
coordinates_formatter: None,
axis_formatters: [None, None], // [None; 2] requires Copy
legend_config: None,
show_background: true,
@ -284,7 +318,7 @@ impl Plot {
/// });
/// let line = Line::new(Values::from_values_iter(sin));
/// Plot::new("my_plot").view_aspect(2.0)
/// .custom_label_func(|name, value| {
/// .label_formatter(|name, value| {
/// if !name.is_empty() {
/// format!("{}: {:.*}%", name, 1, value.y).to_string()
/// } else {
@ -294,34 +328,50 @@ impl Plot {
/// .show(ui, |plot_ui| plot_ui.line(line));
/// # });
/// ```
pub fn custom_label_func(
pub fn label_formatter(
mut self,
custom_label_func: impl Fn(&str, &Value) -> String + 'static,
label_formatter: impl Fn(&str, &Value) -> String + 'static,
) -> Self {
self.custom_label_func = Some(Box::new(custom_label_func));
self.label_formatter = Some(Box::new(label_formatter));
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.
///
/// 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
/// 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
}
/// 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.
///
/// 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
/// 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
}
@ -388,7 +438,8 @@ impl Plot {
view_aspect,
mut show_x,
mut show_y,
custom_label_func,
label_formatter,
coordinates_formatter,
axis_formatters,
legend_config,
show_background,
@ -630,7 +681,8 @@ impl Plot {
items,
show_x,
show_y,
custom_label_func,
label_formatter,
coordinates_formatter,
axis_formatters,
show_axes,
transform: transform.clone(),
@ -849,7 +901,8 @@ struct PreparedPlot {
items: Vec<Box<dyn PlotItem>>,
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
@ -877,7 +930,24 @@ impl PreparedPlot {
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>) {
@ -888,6 +958,11 @@ impl PreparedPlot {
} = self;
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());
@ -947,7 +1022,7 @@ impl PreparedPlot {
let color = color_from_alpha(ui, text_alpha);
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main)
formatter(value_main, &axis_range)
} else {
emath::round_to_decimals(value_main, 5).to_string() // hack
};
@ -982,7 +1057,7 @@ impl PreparedPlot {
transform,
show_x,
show_y,
custom_label_func,
label_formatter,
items,
..
} = self;
@ -1012,10 +1087,10 @@ impl PreparedPlot {
};
if let Some((item, elem)) = closest {
item.on_hover(elem, shapes, &plot, custom_label_func);
item.on_hover(elem, shapes, &plot, label_formatter);
} else {
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]
}
pub(crate) fn range_y(&self) -> RangeInclusive<f64> {
self.min[1]..=self.max[1]
}
pub(crate) fn make_x_symmetrical(&mut self) {
let x_abs = self.min[0].abs().max(self.max[0].abs());
self.min[0] = -x_abs;

View file

@ -71,6 +71,8 @@ pub struct Slider<'a> {
suffix: String,
text: String,
text_color: Option<Color32>,
/// Sets the minimal step of the widget value
step: Option<f64>,
min_decimals: usize,
max_decimals: Option<usize>,
}
@ -113,6 +115,7 @@ impl<'a> Slider<'a> {
suffix: Default::default(),
text: Default::default(),
text_color: None,
step: None,
min_decimals: 0,
max_decimals: None,
}
@ -199,6 +202,16 @@ impl<'a> Slider<'a> {
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".
/// 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.
@ -255,6 +268,9 @@ impl<'a> Slider<'a> {
if let Some(max_decimals) = self.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);
}
@ -284,10 +300,10 @@ impl<'a> Slider<'a> {
impl<'a> Slider<'a> {
/// 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 {
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, perpendicular),
SliderOrientation::Vertical => vec2(perpendicular, ui.spacing().slider_width),
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
};
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_position = self.position_from_value(prev_value, position_range.clone());
let new_position = prev_position + kb_step;
let new_value = if self.smart_aim {
let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = ui.input().aim_radius();
emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()),
self.value_from_position(
new_position - aim_radius,
position_range.clone(),
),
self.value_from_position(
new_position + aim_radius,
position_range.clone(),
),
)
} else {
self.value_from_position(new_position, position_range.clone())
}
_ => self.value_from_position(new_position, position_range.clone()),
};
self.set_value(new_value);
}
@ -429,19 +453,20 @@ impl<'a> Slider<'a> {
}
}
fn label_ui(&mut self, ui: &mut Ui) {
if !self.text.is_empty() {
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));
}
}
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) {
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
// If `DragValue` is controlled from the keyboard and `step` is defined, set speed to `step`
let change = ui.input().num_presses(Key::ArrowUp) as i32
+ ui.input().num_presses(Key::ArrowRight) as i32
- 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,
_ => self.current_gradient(&position_range),
};
let mut value = self.get_value();
ui.add(
let response = ui.add(
DragValue::new(&mut value)
.speed(self.current_gradient(&position_range))
.speed(speed)
.clamp_range(self.clamp_range())
.min_decimals(self.min_decimals)
.max_decimals_opt(self.max_decimals)
@ -451,6 +476,7 @@ impl<'a> Slider<'a> {
if value != self.get_value() {
self.set_value(value);
}
response
}
/// delta(value) / delta(points)
@ -466,21 +492,35 @@ impl<'a> Slider<'a> {
}
fn add_contents(&mut self, ui: &mut Ui) -> Response {
let perpendicular = ui
let thickness = ui
.text_style_height(&TextStyle::Body)
.at_least(ui.spacing().interact_size.y);
let slider_response = self.allocate_slider_space(ui, perpendicular);
self.slider_ui(ui, &slider_response);
let mut response = self.allocate_slider_space(ui, thickness);
self.slider_ui(ui, &response);
if self.show_value {
let position_range = self.position_range(&slider_response.rect);
self.value_ui(ui, position_range);
let position_range = self.position_range(&response.rect);
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() {
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);
}
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) {
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
// 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_end(
let cursor_pos = paint_cursor_end(
ui,
row_height,
&painter,
@ -570,15 +578,14 @@ impl<'t> TextEdit<'t> {
&cursor_range.primary,
);
if response.changed || selection_changed {
ui.scroll_to_rect(cursor_pos, None); // keep cursor in view
}
if interactive && text.is_mutable() {
// egui_web uses `text_cursor_pos` when showing IME,
// so only set it when text is editable and visible!
ui.ctx().output().text_cursor_pos = Some(
galley
.pos_from_cursor(&cursor_range.primary)
.translate(response.rect.min.to_vec2())
.left_top(),
);
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
}
}
}
@ -586,14 +593,6 @@ impl<'t> TextEdit<'t> {
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 {
response.widget_info(|| {
WidgetInfo::text_edit(
@ -887,7 +886,7 @@ fn paint_cursor_end(
pos: Pos2,
galley: &Galley,
cursor: &Cursor,
) {
) -> Rect {
let stroke = ui.visuals().selection.stroke;
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
@ -915,6 +914,8 @@ fn paint_cursor_end(
(width, stroke.color),
);
}
cursor_pos
}
// ----------------------------------------------------------------------------
@ -1091,11 +1092,31 @@ fn on_key_press(
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,
}
}
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 {
Key::ArrowLeft => {
if modifiers.alt || modifiers.ctrl {

View file

@ -1,6 +1,6 @@
[package]
name = "egui_demo_app"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
@ -23,10 +23,14 @@ syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
[dependencies]
eframe = { version = "0.16.0", path = "../eframe" }
eframe = { version = "0.17.0", path = "../eframe" }
# 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"
[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")]
#[wasm_bindgen]
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();
eframe::start_web(canvas_id, Box::new(app))
}

View file

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

View file

@ -13,10 +13,10 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// The most end-to-end benchmark.
c.bench_function("demo_with_tessellate__realistic", |b| {
b.iter(|| {
let (_output, shapes) = ctx.run(RawInput::default(), |ctx| {
let full_output = ctx.run(RawInput::default(), |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);
});
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) {
use crate::syntax_highlighting::code_view_ui;
ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!());
});
code_view_ui(
ui,
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 {
fn default() -> Self {
Self::from_demos(vec![
Box::new(super::paint_bezier::PaintBezier::default()),
Box::new(super::code_editor::CodeEditor::default()),
Box::new(super::code_example::CodeExample::default()),
Box::new(super::context_menu::ContextMenus::default()),
@ -26,7 +27,6 @@ impl Default for Demos {
Box::new(super::MiscDemoWindow::default()),
Box::new(super::multi_touch::MultiTouch::default()),
Box::new(super::painting::Painting::default()),
Box::new(super::paint_bezier::PaintBezier::default()),
Box::new(super::plot_demo::PlotDemo::default()),
Box::new(super::scrolling::Scrolling::default()),
Box::new(super::sliders::Sliders::default()),

View file

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

View file

@ -1,250 +1,167 @@
use egui::emath::RectTransform;
use egui::epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape};
use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape};
use egui::*;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct PaintBezier {
/// Current bezier curve degree, it can be 3, 4.
bezier: usize,
/// Track the bezier degree before change in order to clean the remaining points.
degree_backup: usize,
/// Points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes'
points: Vec<Pos2>,
/// Track last points set in order to draw auxiliary lines.
backup_points: Vec<Pos2>,
/// Quadratic shapes already drawn.
q_shapes: Vec<QuadraticBezierShape>,
/// Cubic shapes already drawn.
/// Since `Shape` can't be 'serialized', we can't use Shape as variable type.
c_shapes: Vec<CubicBezierShape>,
/// Bézier curve degree, it can be 3, 4.
degree: usize,
/// The control points. The [`Self::degree`] first of them are used.
control_points: [Pos2; 4],
/// Stroke for Bézier curve.
stroke: Stroke,
/// Fill for Bézier curve.
fill: Color32,
/// Stroke for auxiliary lines.
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,
}
impl Default for PaintBezier {
fn default() -> Self {
Self {
bezier: 4, // default bezier degree, a cubic bezier curve
degree_backup: 4,
points: Default::default(),
backup_points: Default::default(),
q_shapes: Default::default(),
c_shapes: Default::default(),
aux_stroke: Stroke::new(1.0, Color32::RED),
degree: 4,
control_points: [
pos2(50.0, 50.0),
pos2(60.0, 250.0),
pos2(200.0, 200.0),
pos2(250.0, 50.0),
],
stroke: Stroke::new(1.0, Color32::LIGHT_BLUE),
fill: Default::default(),
closed: false,
show_bounding_box: false,
bounding_box_stroke: Stroke::new(1.0, Color32::LIGHT_GREEN),
fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25),
aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)),
}
}
}
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.collapsing("Colors", |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label("Fill color:");
ui.color_edit_button_srgba(&mut self.fill);
});
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
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");
});
ui.separator();
ui.vertical(|ui| {
{
ui.collapsing("Global tessellation options", |ui| {
let mut tessellation_options = *(ui.ctx().tessellation_options());
let tessellation_options = &mut tessellation_options;
tessellation_options.ui(ui);
let mut new_tessellation_options = ui.ctx().tessellation_options();
*new_tessellation_options = *tessellation_options;
}
ui.checkbox(&mut self.show_bounding_box, "Bounding Box");
});
ui.separator();
ui.vertical(|ui| {
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
ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");
ui.radio_value(&mut self.degree, 4, "Cubic Bézier");
ui.label("Move the points by dragging them.");
ui.small("Only convex curves can be accurately filled.");
}
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
let (mut response, painter) =
ui.allocate_painter(ui.available_size_before_wrap(), Sense::click());
let (response, painter) =
ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover());
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,
);
let from_screen = to_screen.inverse();
if response.clicked() {
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() {
let control_point_radius = 8.0;
let mut control_point_shapes = vec![];
for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) {
let size = Vec2::splat(2.0 * control_point_radius);
let point_in_screen = to_screen.transform_pos(*point);
let point_rect = Rect::from_center_size(point_in_screen, size);
let point_id = response.id.with(i);
let point_response = ui.interact(point_rect, point_id, Sense::drag());
*point += point_response.drag_delta();
*point = to_screen.from().clamp(*point);
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 quadratic = QuadraticBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.q_shapes.push(quadratic);
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 cubic = CubicBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.c_shapes.push(cubic);
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!();
}
}
}
};
response.mark_changed();
}
}
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() {
painter.extend(build_auxiliary_line(
&self.points,
&to_screen,
&self.aux_stroke,
));
} else if !self.backup_points.is_empty() {
painter.extend(build_auxiliary_line(
&self.backup_points,
&to_screen,
&self.aux_stroke,
));
}
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
painter.extend(control_point_shapes);
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 {
fn name(&self) -> &'static str {
"✔ Bezier Curve"
" Bézier Curve"
}
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)
.default_size(vec2(512.0, 512.0))
.vscroll(false)
.resizable(false)
.default_size([300.0, 350.0])
.show(ctx, |ui| self.ui(ui));
}
}
impl super::View for PaintBezier {
fn ui(&mut self, ui: &mut Ui) {
// ui.vertical_centered(|ui| {
// ui.add(crate::__egui_github_link_file!());
// });
ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!());
});
self.ui_control(ui);
Frame::dark_canvas(ui.style()).show(ui, |ui| {

View file

@ -2,8 +2,9 @@ use std::f64::consts::TAU;
use egui::*;
use plot::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Corner, HLine, Legend, Line, LineStyle,
MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value, Values,
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value,
Values,
};
#[derive(PartialEq)]
@ -14,6 +15,7 @@ struct LineDemo {
circle_center: Pos2,
square: bool,
proportional: bool,
coordinates: bool,
line_style: LineStyle,
}
@ -26,6 +28,7 @@ impl Default for LineDemo {
circle_center: Pos2::new(0.0, 0.0),
square: false,
proportional: true,
coordinates: true,
line_style: LineStyle::Solid,
}
}
@ -41,6 +44,7 @@ impl LineDemo {
square,
proportional,
line_style,
coordinates,
..
} = self;
@ -76,6 +80,8 @@ impl LineDemo {
.on_hover_text("Always keep the viewport square.");
ui.checkbox(proportional, "Proportional data 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")
.selected_text(line_style.to_string())
@ -151,6 +157,9 @@ impl Widget for &mut LineDemo {
if self.proportional {
plot = plot.data_aspect(1.0);
}
if self.coordinates {
plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default());
}
plot.show(ui, |plot_ui| {
plot_ui.line(self.circle());
plot_ui.line(self.sin());
@ -595,7 +604,7 @@ impl ChartsDemo {
.name("Set 4")
.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) {
// Only label full days from 0 to 4
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;
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)]
struct ScrollTo {
track_item: usize,
tack_item_align: Align,
tack_item_align: Option<Align>,
offset: f32,
}
@ -155,7 +155,7 @@ impl Default for ScrollTo {
fn default() -> Self {
Self {
track_item: 25,
tack_item_align: Align::Center,
tack_item_align: Some(Align::Center),
offset: 0.0,
}
}
@ -180,13 +180,16 @@ impl super::View for ScrollTo {
ui.horizontal(|ui| {
ui.label("Item align:");
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();
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();
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();
});
@ -213,7 +216,7 @@ impl super::View for ScrollTo {
let (current_scroll, max_scroll) = scroll_area
.show(ui, |ui| {
if scroll_top {
ui.scroll_to_cursor(Align::TOP);
ui.scroll_to_cursor(Some(Align::TOP));
}
ui.vertical(|ui| {
for item in 1..=50 {
@ -228,7 +231,7 @@ impl super::View for ScrollTo {
});
if scroll_bottom {
ui.scroll_to_cursor(Align::BOTTOM);
ui.scroll_to_cursor(Some(Align::BOTTOM));
}
let margin = ui.visuals().clip_rect_margin;

View file

@ -11,6 +11,8 @@ pub struct Sliders {
pub logarithmic: bool,
pub clamp_to_range: bool,
pub smart_aim: bool,
pub step: f64,
pub use_steps: bool,
pub integer: bool,
pub vertical: bool,
pub value: f64,
@ -24,6 +26,8 @@ impl Default for Sliders {
logarithmic: true,
clamp_to_range: false,
smart_aim: true,
step: 10.0,
use_steps: false,
integer: false,
vertical: false,
value: 10.0,
@ -55,6 +59,8 @@ impl super::View for Sliders {
logarithmic,
clamp_to_range,
smart_aim,
step,
use_steps,
integer,
vertical,
value,
@ -79,6 +85,7 @@ impl super::View for Sliders {
SliderOrientation::Horizontal
};
let istep = if *use_steps { *step } else { 0.0 };
if *integer {
let mut value_i32 = *value as i32;
ui.add(
@ -87,7 +94,8 @@ impl super::View for Sliders {
.clamp_to_range(*clamp_to_range)
.smart_aim(*smart_aim)
.orientation(orientation)
.text("i32 demo slider"),
.text("i32 demo slider")
.step_by(istep),
);
*value = value_i32 as f64;
} else {
@ -97,7 +105,8 @@ impl super::View for Sliders {
.clamp_to_range(*clamp_to_range)
.smart_aim(*smart_aim)
.orientation(orientation)
.text("f64 demo slider"),
.text("f64 demo slider")
.step_by(istep),
);
ui.label(
@ -128,6 +137,14 @@ impl super::View for Sliders {
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.label("Slider type:");
ui.radio_value(integer, true, "i32");

View file

@ -64,7 +64,10 @@ impl super::View for TextEdit {
anything_selected,
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 {
use egui::TextBuffer as _;
let selected_chars = text_cursor_range.as_sorted_char_range();

View file

@ -1,3 +1,4 @@
use egui_extras::RetainedImage;
use poll_promise::Promise;
struct Resource {
@ -7,7 +8,7 @@ struct Resource {
text: Option<String>,
/// 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").
colored_text: Option<ColoredText>,
@ -17,13 +18,11 @@ impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
load_image(&response.bytes).ok()
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
} else {
None
};
let texture = image.map(|image| ctx.load_texture(&response.url, image));
let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
@ -31,7 +30,7 @@ impl Resource {
Self {
response,
text,
texture,
image,
colored_text,
}
}
@ -151,7 +150,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
let Resource {
response,
text,
texture,
image,
colored_text,
} = resource;
@ -198,10 +197,10 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
ui.separator();
}
if let Some(texture) = texture {
let mut size = texture.size_vec2();
if let Some(image) = image {
let mut size = image.size_vec2();
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 {
colored_text.ui(ui);
} 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());
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
// 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();

View file

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

View file

@ -145,10 +145,10 @@ fn test_egui_e2e() {
const NUM_FRAMES: usize = 5;
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);
});
let clipped_meshes = ctx.tessellate(shapes);
let clipped_meshes = ctx.tessellate(full_output.shapes);
assert!(!clipped_meshes.is_empty());
}
}
@ -164,10 +164,10 @@ fn test_egui_zero_window_size() {
const NUM_FRAMES: usize = 5;
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);
});
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");
}
}

View file

@ -69,7 +69,7 @@ impl epi::App for WrapApp {
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
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();
}
}
@ -108,12 +108,19 @@ impl epi::App for WrapApp {
});
}
let mut found_anchor = false;
for (anchor, app) in self.apps.iter_mut() {
if anchor == self.selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame);
found_anchor = true;
}
}
if !found_anchor {
self.selected_anchor = "demo".into();
}
self.backend_panel.end_of_frame(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]
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"
rust-version = "1.56"
description = "Extra widgets for egui"
authors = [
"René Rössler <rene@freshx.de>",
"Dominik Rössler <dominik@freshx.de>",
]
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/emilk/egui/tree/master/egui_extras"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/egui_extras"
categories = ["gui", "graphics"]
keywords = ["egui", "gui", "gamedev"]
readme = "../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false }
[package.metadata.docs.rs]
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 }
# 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":
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)]
#![warn(
clippy::all,
@ -84,14 +86,16 @@
mod datepicker;
mod grid;
pub mod image;
mod layout;
mod sizing;
mod table;
#[cfg(feature = "chrono")]
pub use datepicker::DatePickerButton;
pub use crate::datepicker::DatePickerButton;
pub use grid::*;
pub(crate) use layout::Layout;
pub use sizing::Size;
pub use table::*;
pub use crate::grid::*;
pub use crate::image::RetainedImage;
pub(crate) use crate::layout::Layout;
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
## 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)).
* 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)).

View file

@ -1,6 +1,6 @@
[package]
name = "egui_glium"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glium library"
edition = "2021"
@ -51,12 +51,12 @@ screen_reader = ["egui-winit/screen_reader"]
[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",
"single_threaded",
] }
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["epi"] }
epi = { version = "0.16.0", path = "../epi", optional = true }
egui-winit = { version = "0.17.0", path = "../egui-winit", default-features = false, features = ["epi"] }
epi = { version = "0.17.0", path = "../epi", optional = true }
ahash = "0.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));
}
let (needs_repaint, mut textures_delta, shapes) =
integration.update(display.gl_window().window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
let egui::FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = integration.update(display.gl_window().window());
for (id, image_delta) in textures_delta.set {
painter.set_texture(&display, id, &image_delta);
}
integration.handle_platform_output(display.gl_window().window(), platform_output);
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
// paint:
{
@ -82,20 +85,17 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
let color = integration.app.clear_color();
target.clear_color(color[0], color[1], color[2], color[3]);
painter.paint_meshes(
painter.paint_and_update_textures(
&display,
&mut target,
integration.egui_ctx.pixels_per_point(),
clipped_meshes,
&textures_delta,
);
target.finish().unwrap();
}
for id in textures_delta.free.drain(..) {
painter.free_texture(id);
}
{
*control_flow = if integration.should_quit() {
glutin::event_loop::ControlFlow::Exit

View file

@ -141,38 +141,36 @@ impl EguiGlium {
let raw_input = self
.egui_winit
.take_egui_input(display.gl_window().window());
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui);
let needs_repaint = egui_output.needs_repaint;
let textures_delta = self.egui_winit.handle_output(
let egui::FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = self.egui_ctx.run(raw_input, run_ui);
self.egui_winit.handle_platform_output(
display.gl_window().window(),
&self.egui_ctx,
egui_output,
platform_output,
);
self.shapes = shapes;
self.textures_delta.append(textures_delta);
needs_repaint
}
/// Paint the results of the last call to [`Self::run`].
pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) {
let shapes = std::mem::take(&mut self.shapes);
let mut 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 textures_delta = std::mem::take(&mut self.textures_delta);
let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.painter.paint_meshes(
self.painter.paint_and_update_textures(
display,
target,
self.egui_ctx.pixels_per_point(),
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
}
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.
/// You should call `target.clear_color(..)` before
/// and `target.finish()` after this.
@ -73,9 +92,9 @@ impl Painter {
display: &glium::Display,
target: &mut T,
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);
}
}

View file

@ -3,11 +3,13 @@ All notable changes to the `egui_glow` integration will be noted in this file.
## 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)).
* 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)).
* `EguiGlow::new` now takes `&winit::Window` because there are no reason to use `&glutin::WindowedContext` ([#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)).
* `EguiGlow::new` and `EguiGlow::paint` now takes `&winit::Window` ([#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)).

View file

@ -1,6 +1,6 @@
[package]
name = "egui_glow"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glow library"
edition = "2021"
@ -55,11 +55,11 @@ winit = ["egui-winit", "glutin"]
[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",
"single_threaded",
] }
epi = { version = "0.16.0", path = "../epi", optional = true }
epi = { version = "0.17.0", path = "../epi", optional = true }
bytemuck = "1.7"
glow = "0.11"
@ -67,7 +67,7 @@ memoffset = "0.6"
tracing = "0.1"
[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 }
[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));
}
let (needs_repaint, mut textures_delta, shapes) =
integration.update(gl_window.window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
let egui::FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = integration.update(gl_window.window());
for (id, image_delta) in textures_delta.set {
painter.set_texture(&gl, id, &image_delta);
}
integration.handle_platform_output(gl_window.window(), platform_output);
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
// 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(glow::COLOR_BUFFER_BIT);
}
painter.paint_meshes(
painter.paint_and_update_textures(
&gl,
gl_window.window().inner_size().into(),
integration.egui_ctx.pixels_per_point(),
clipped_meshes,
&textures_delta,
);
gl_window.swap_buffers().unwrap();
}
for id in textures_delta.free.drain(..) {
painter.free_texture(&gl, id);
}
{
*control_flow = if integration.should_quit() {
winit::event_loop::ControlFlow::Exit

View file

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

View file

@ -270,6 +270,25 @@ impl Painter {
(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.
/// You should call `target.clear_color(..)` before
/// and `target.finish()` after this.

View file

@ -1,15 +1,17 @@
# Changelog for egui_web
All notable changes to the `egui_web` integration will be noted in this file.
## Unreleased
## 0.17.0 - 2022-02-22
* 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)).
* 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)).
* 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

View file

@ -1,6 +1,6 @@
[package]
name = "egui_web"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for compiling egui code to WASM for a web page"
license = "MIT OR Apache-2.0"
@ -47,19 +47,18 @@ screen_reader = ["tts"]
[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",
"single_threaded",
"tracing",
] }
egui_glow = { version = "0.16.0",path = "../egui_glow", default-features = false, optional = true }
epi = { version = "0.16.0", path = "../epi" }
egui_glow = { version = "0.17.0",path = "../egui_glow", default-features = false, optional = true }
epi = { version = "0.17.0", path = "../epi" }
bytemuck = "1.7"
console_error_panic_hook = "0.1.6"
js-sys = "0.3"
percent-encoding = "2.1"
tracing = "0.1"
tracing-wasm = "0.2"
wasm-bindgen = "0.2"
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.
* 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).
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(crate) frame: epi::Frame,
egui_ctx: egui::Context,
@ -108,7 +179,7 @@ impl AppRunner {
info: epi::IntegrationInfo {
name: painter.name(),
web_info: Some(epi::WebInfo {
web_location_hash: location_hash().unwrap_or_default(),
location: web_location(),
}),
prefer_dark_mode,
cpu_usage: None,
@ -143,7 +214,7 @@ impl AppRunner {
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
@ -196,14 +267,19 @@ impl AppRunner {
let canvas_size = canvas_size_in_points(self.canvas_id());
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);
});
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;
let textures_delta = self.handle_egui_output(egui_output);
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
let clipped_meshes = self.egui_ctx.tessellate(shapes);
{
let app_output = self.frame.take_app_output();
@ -223,36 +299,32 @@ impl AppRunner {
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> {
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
.paint_meshes(clipped_meshes, self.egui_ctx.pixels_per_point())?;
for id in textures_delta.free {
self.painter.free_texture(id);
}
self.painter.paint_and_update_textures(
clipped_meshes,
self.egui_ctx.pixels_per_point(),
&textures_delta,
)?;
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 {
self.screen_reader.speak(&output.events_description());
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::Output {
let egui::PlatformOutput {
cursor_icon,
open_url,
copied_text,
needs_repaint: _, // handled elsewhere
events: _, // already handled
mutable_text_under_cursor,
text_cursor_pos,
textures_delta,
} = output;
} = platform_output;
set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
@ -270,23 +342,15 @@ impl AppRunner {
self.mutable_text_under_cursor = mutable_text_under_cursor;
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;
}
textures_delta
}
}
/// Install event listeners to register different input events
/// and start running the given app.
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)?;
runner.warm_up()?;
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)));
install_canvas_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
paint_and_schedule(runner_ref.clone())?;
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;
#[cfg(feature = "glow")]
mod glow_wrapping;
mod input;
mod painter;
pub mod screen_reader;
mod text_agent;
#[cfg(feature = "webgl")]
pub mod webgl1;
@ -31,14 +33,13 @@ use egui::mutex::Mutex;
pub use wasm_bindgen;
pub use web_sys;
use input::*;
pub use painter::Painter;
use std::cell::Cell;
use std::rc::Rc;
use std::collections::BTreeMap;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
// ----------------------------------------------------------------------------
/// 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))
}
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 {
let rect = canvas_element(canvas_id)
.unwrap()
@ -154,21 +97,6 @@ fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
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 {
let canvas = canvas_element(canvas_id).unwrap();
let pixels_per_point = native_pixels_per_point();
@ -353,108 +281,23 @@ pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
Some(())
}
/// e.g. "#fragment" part of "www.example.com/index.html#fragment"
pub fn location_hash() -> Option<String> {
web_sys::window()?.location().hash().ok()
}
/// 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.
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"
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
///
/// Percent decoded
pub fn location_hash() -> String {
percent_decode(
&web_sys::window()
.unwrap()
.location()
.hash()
.unwrap_or_default(),
)
}
/// 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 percent_decode(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8_lossy()
.to_string()
}
// ----------------------------------------------------------------------------
@ -490,18 +333,6 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
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> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
@ -533,7 +364,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
&& !modifiers.command
&& !should_ignore_key(&key)
// 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));
}
@ -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
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()>);
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(())
}
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> {
use wasm_bindgen::JsCast;
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();
update_text_agent(&runner_lock);
text_agent::update_text_agent(&runner_lock);
}
event.stop_propagation();
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:
update_text_agent(&runner_lock);
text_agent::update_text_agent(&runner_lock);
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
@ -1158,92 +878,6 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
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 {
// See https://github.com/emilk/egui/issues/794

View file

@ -22,4 +22,23 @@ pub trait Painter {
) -> Result<(), JsValue>;
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]
name = "emath"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D math library for GUI work"
edition = "2021"

View file

@ -1,15 +1,20 @@
# epaint changelog
All notable changes to the epaint crate will be documented in this file.
## Unreleased
## 0.17.0 - 2022-02-22
* 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 `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 `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)).
* 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

View file

@ -1,6 +1,6 @@
[package]
name = "epaint"
version = "0.16.0"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D graphics library for GUI work"
edition = "2021"
@ -55,7 +55,7 @@ multi_threaded = ["parking_lot"]
[dependencies]
emath = { version = "0.16.0", path = "../emath" }
emath = { version = "0.17.0", path = "../emath" }
ab_glyph = "0.2.11"
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 }
cint = { version = "^0.2.2", optional = true }
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 }
[dev-dependencies]

View file

@ -6,9 +6,9 @@ use emath::*;
// ----------------------------------------------------------------------------
/// How to paint a cubic Bezier curve on screen.
/// 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.
/// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
///
/// See also [`QuadraticBezierShape`].
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CubicBezierShape {
@ -22,30 +22,29 @@ pub struct 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 middle points are the control points.
/// The number of points must be 4.
pub fn from_points_stroke(
points: Vec<Pos2>,
points: [Pos2; 4],
closed: bool,
fill: Color32,
stroke: impl Into<Stroke>,
) -> Self {
crate::epaint_assert!(points.len() == 4, "Cubic needs 4 points");
Self {
points: points.try_into().unwrap(),
points,
closed,
fill,
stroke: stroke.into(),
}
}
/// Creates a cubic Bezier curve based on the screen coordinates for the 4 points.
pub fn to_screen(&self, to_screen: &RectTransform) -> Self {
/// Transform the curve with the given transform.
pub fn transform(&self, transform: &RectTransform) -> Self {
let mut points = [Pos2::default(); 4];
for (i, origin_point) in self.points.iter().enumerate() {
points[i] = to_screen * *origin_point;
points[i] = transform * *origin_point;
}
CubicBezierShape {
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.
/// 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 `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 points_vec = self.flatten_closed(tolerance, epsilon);
for points in points_vec.drain(..) {
@ -74,8 +73,18 @@ impl CubicBezierShape {
}
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
let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x {
(self.points[0].x, self.points[3].x)
@ -256,9 +265,9 @@ impl CubicBezierShape {
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]
/// [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 {
crate::epaint_assert!(
@ -278,7 +287,7 @@ impl CubicBezierShape {
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 points may not be evenly distributed in the range [0.0,1.0] (t value)
pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> {
@ -290,7 +299,7 @@ impl CubicBezierShape {
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 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.
@ -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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct QuadraticBezierShape {
@ -371,32 +385,30 @@ pub struct QuadraticBezierShape {
}
impl QuadraticBezierShape {
/// create a new quadratic bezier 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]
/// 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]
pub fn from_points_stroke(
points: Vec<Pos2>,
points: [Pos2; 3],
closed: bool,
fill: Color32,
stroke: impl Into<Stroke>,
) -> Self {
crate::epaint_assert!(points.len() == 3, "Quadratic needs 3 points");
QuadraticBezierShape {
points: points.try_into().unwrap(), // it's safe to unwrap because we just checked
points,
closed,
fill,
stroke: stroke.into(),
}
}
/// create a new quadratic bezier shape based on the screen coordination for the 3 points.
pub fn to_screen(&self, to_screen: &RectTransform) -> Self {
/// Transform the curve with the given transform.
pub fn transform(&self, transform: &RectTransform) -> Self {
let mut points = [Pos2::default(); 3];
for (i, origin_point) in self.points.iter().enumerate() {
points[i] = to_screen * *origin_point;
points[i] = transform * *origin_point;
}
QuadraticBezierShape {
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.
pub fn to_pathshape(&self, tolerance: Option<f32>) -> PathShape {
pub fn to_path_shape(&self, tolerance: Option<f32>) -> PathShape {
let points = self.flatten(tolerance);
PathShape {
points,
@ -417,8 +429,18 @@ impl QuadraticBezierShape {
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 {
(self.points[0].x, self.points[2].x)
} 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]
/// [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 {
crate::epaint_assert!(
@ -486,7 +508,7 @@ impl QuadraticBezierShape {
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 points may not be evenly distributed in the range [0.0,1.0] (t value)
pub fn flatten(&self, tolerance: Option<f32>) -> Vec<Pos2> {
@ -533,6 +555,8 @@ impl From<QuadraticBezierShape> for Shape {
}
}
// ----------------------------------------------------------------------------
// lyon_geom::flatten_cubic.rs
// copied from https://docs.rs/lyon_geom/latest/lyon_geom/
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);
});
}
// from lyon_geom::quadratic_bezier.rs
// copied from https://docs.rs/lyon_geom/latest/lyon_geom/
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) {
// 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)
// The derivative is:
// 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) {
// 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
// f(x) = a * x² + b * x + c
let a = 3.0 * (p3 + 3.0 * (p1 - p2) - p0);
@ -748,7 +773,7 @@ mod tests {
fill: 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.y - 27.78).abs() < 0.01);
@ -772,7 +797,7 @@ mod tests {
fill: 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.y - 10.0).abs() < 0.01);
@ -841,7 +866,7 @@ mod tests {
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.y, 10.0);
assert_eq!(bbox.max.x, 270.0);
@ -859,7 +884,7 @@ mod tests {
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.y, 10.0);
assert!((bbox.max.x - 206.50).abs() < 0.01);
@ -877,7 +902,7 @@ mod tests {
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.y - 30.0).abs() < 0.01);

View file

@ -31,6 +31,20 @@ pub enum Shape {
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
impl Shape {
/// A line between two points.
@ -59,25 +73,25 @@ impl Shape {
/// Turn a line into equally spaced dots.
pub fn dotted_line(
points: &[Pos2],
path: &[Pos2],
color: impl Into<Color32>,
spacing: f32,
radius: f32,
) -> Vec<Self> {
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
}
/// Turn a line into dashes.
pub fn dashed_line(
points: &[Pos2],
path: &[Pos2],
stroke: impl Into<Stroke>,
dash_length: f32,
gap_length: f32,
) -> Vec<Self> {
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
}
@ -94,6 +108,8 @@ impl Shape {
}
/// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline]
pub fn convex_polygon(
points: Vec<Pos2>,
@ -154,6 +170,34 @@ impl Shape {
crate::epaint_assert!(mesh.is_valid());
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
@ -244,6 +288,18 @@ impl CircleShape {
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 {
@ -259,6 +315,7 @@ impl From<CircleShape> for Shape {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PathShape {
/// Filled paths should prefer clockwise order.
pub points: Vec<Pos2>,
/// If true, connect the first and last of the points together.
/// This is required if `fill != TRANSPARENT`.
@ -294,6 +351,8 @@ impl PathShape {
}
/// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline]
pub fn convex_polygon(
points: Vec<Pos2>,
@ -308,10 +367,14 @@ impl PathShape {
}
}
/// Screen-space bounding rectangle.
/// The visual bounding rectangle (includes stroke width)
#[inline]
pub fn bounding_rect(&self) -> Rect {
Rect::from_points(&self.points).expand(self.stroke.width)
pub fn visual_bounding_rect(&self) -> Rect {
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]
pub fn bounding_rect(&self) -> Rect {
self.rect.expand(self.stroke.width)
pub fn visual_bounding_rect(&self) -> Rect {
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.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextShape {
/// Top left corner of the first character.
pub pos: Pos2,
@ -455,7 +523,7 @@ pub struct TextShape {
/// This will NOT replace background color nor strikethrough/underline color.
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).
pub angle: f32,
}
@ -472,9 +540,9 @@ impl TextShape {
}
}
/// Screen-space bounding rectangle.
/// The visual bounding rectangle
#[inline]
pub fn bounding_rect(&self) -> Rect {
pub fn visual_bounding_rect(&self) -> Rect {
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.
fn points_from_line(
line: &[Pos2],
path: &[Pos2],
spacing: f32,
radius: f32,
color: Color32,
shapes: &mut Vec<Shape>,
) {
let mut position_on_segment = 0.0;
line.windows(2).for_each(|window| {
let start = window[0];
let end = window[1];
path.windows(2).for_each(|window| {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();
while position_on_segment < segment_length {
@ -513,7 +580,7 @@ fn points_from_line(
/// Creates dashes from a line.
fn dashes_from_line(
line: &[Pos2],
path: &[Pos2],
stroke: Stroke,
dash_length: f32,
gap_length: f32,
@ -521,9 +588,8 @@ fn dashes_from_line(
) {
let mut position_on_segment = 0.0;
let mut drawing_dash = false;
line.windows(2).for_each(|window| {
let start = window[0];
let end = window[1];
path.windows(2).for_each(|window| {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();

View file

@ -133,9 +133,20 @@ impl Path {
let normal = (n0 + n1) / 2.0;
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 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
let center_normal = normal.normalized();
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).
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 max = rect.max;
path.reserve(4);
path.push(pos2(min.x, min.y));
path.push(pos2(max.x, min.y));
path.push(pos2(max.x, max.y));
path.push(pos2(min.x, max.y));
path.push(pos2(min.x, min.y)); // left top
path.push(pos2(max.x, min.y)); // right top
path.push(pos2(max.x, max.y)); // right bottom
path.push(pos2(min.x, max.y)); // left bottom
} else {
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);
@ -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.
///
/// 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(
path: &[PathPoint],
path: &mut [PathPoint],
color: Color32,
options: &TessellationOptions,
out: &mut Mesh,
@ -359,14 +392,26 @@ fn fill_closed_path(
let n = path.len() as u32;
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_vertices(2 * n as usize);
let color_outer = Color32::TRANSPARENT;
let idx_inner = out.vertices.len() as u32;
let idx_outer = idx_inner + 1;
// The fill:
for i in 2..n {
out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i);
}
// The feathering:
let mut i0 = n - 1;
for i1 in 0..n {
let p1 = &path[i1 as usize];
@ -748,7 +793,7 @@ impl Tessellator {
let clip_rect = self.clip_rect;
if options.coarse_tessellation_culling
&& !quadratic_shape.bounding_rect().intersects(clip_rect)
&& !quadratic_shape.visual_bounding_rect().intersects(clip_rect)
{
return;
}
@ -771,7 +816,8 @@ impl Tessellator {
) {
let options = &self.options;
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;
}
@ -825,7 +871,7 @@ impl Tessellator {
}
if self.options.coarse_tessellation_culling
&& !path_shape.bounding_rect().intersects(self.clip_rect)
&& !path_shape.visual_bounding_rect().intersects(self.clip_rect)
{
return;
}
@ -1026,6 +1072,7 @@ pub fn tessellate_shapes(
if options.debug_paint_clip_rects {
for ClippedMesh(clip_rect, mesh) in &mut clipped_meshes {
if mesh.texture_id == TextureId::default() {
tessellator.clip_rect = Rect::EVERYTHING;
tessellator.tessellate_shape(
tex_size,
@ -1036,6 +1083,9 @@ pub fn tessellate_shapes(
),
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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct UvRect {
/// X/Y offset for nice rendering (unit: points).
pub offset: Vec2,
@ -56,6 +57,7 @@ impl Default for GlyphInfo {
/// A specific font with a size.
/// The interface uses points as the unit for everything.
pub struct FontImpl {
name: String,
ab_glyph_font: ab_glyph::FontArc,
/// Maximum character height
scale_in_pixels: u32,
@ -71,24 +73,28 @@ impl FontImpl {
pub fn new(
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
name: String,
ab_glyph_font: ab_glyph::FontArc,
scale_in_pixels: u32,
y_offset: f32,
y_offset_points: f32,
) -> FontImpl {
assert!(scale_in_pixels > 0);
assert!(pixels_per_point > 0.0);
let height_in_points = scale_in_pixels as f32 / pixels_per_point;
// TODO: use v_metrics for line spacing ?
// let v = rusttype_font.v_metrics(Scale::uniform(scale_in_pixels));
// let height_in_pixels = v.ascent - v.descent + v.line_gap;
// let height_in_points = height_in_pixels / pixels_per_point;
// TODO: use these font metrics?
// use ab_glyph::ScaleFont as _;
// let scaled = ab_glyph_font.as_scaled(scale_in_pixels as f32);
// dbg!(scaled.ascent());
// dbg!(scaled.descent());
// dbg!(scaled.line_gap());
// 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 {
name,
ab_glyph_font,
scale_in_pixels,
height_in_points,
@ -99,14 +105,16 @@ impl FontImpl {
}
}
/// An un-ordered iterator over all supported characters.
fn characters(&self) -> impl Iterator<Item = char> + '_ {
use ab_glyph::Font as _;
self.ab_glyph_font
.codepoint_ids()
.map(|(_, chr)| chr)
.filter(|chr| {
!matches!(
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}' |
@ -114,7 +122,15 @@ impl FontImpl {
// Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
)
})
}
/// An un-ordered iterator over all supported characters.
fn characters(&self) -> impl Iterator<Item = char> + '_ {
use ab_glyph::Font as _;
self.ab_glyph_font
.codepoint_ids()
.map(|(_, chr)| chr)
.filter(|&chr| !self.ignore_character(chr))
}
/// `\n` will result in `None`
@ -125,9 +141,9 @@ impl FontImpl {
}
}
// Add new character:
use ab_glyph::Font as _;
let glyph_id = self.ab_glyph_font.glyph_id(c);
if self.ignore_character(c) {
return None;
}
if c == '\t' {
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 invisible_char(c) {
// hack
@ -147,7 +167,7 @@ impl FontImpl {
self.glyph_info_cache.write().insert(c, glyph_info);
Some(glyph_info)
} else {
None
None // unsupported character
}
} else {
let glyph_info = allocate_glyph(

View file

@ -120,6 +120,9 @@ pub struct FontData {
/// Which font face in the file to use.
/// When in doubt, use `0`.
pub index: u32,
/// Extra scale and vertical tweak to apply to all text of this font.
pub tweak: FontTweak,
}
impl FontData {
@ -127,6 +130,7 @@ impl FontData {
Self {
font: std::borrow::Cow::Borrowed(font),
index: 0,
tweak: Default::default(),
}
}
@ -134,10 +138,52 @@ impl FontData {
Self {
font: std::borrow::Cow::Owned(font),
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 {
match &data.font {
std::borrow::Cow::Borrowed(bytes) => {
@ -220,10 +266,17 @@ impl Default for FontDefinitions {
"NotoEmoji-Regular".to_owned(),
FontData::from_static(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")),
);
// Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>:
font_data.insert(
"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(
@ -603,7 +656,7 @@ impl GalleyCache {
struct FontImplCache {
atlas: Arc<Mutex<TextureAtlas>>,
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`.
cache: ahash::AHashMap<(u32, String), Arc<FontImpl>>,
@ -617,7 +670,11 @@ impl FontImplCache {
) -> Self {
let ab_glyph_fonts = font_data
.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();
Self {
@ -638,35 +695,29 @@ impl FontImplCache {
}
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" {
(scale_in_pixels as f32 * 0.8).round() as u32 // TODO: remove font scale HACK!
} else {
scale_in_pixels
};
let y_offset = if font_name == "emoji-icon-font" {
let scale_in_points = scale_in_pixels as f32 / self.pixels_per_point;
scale_in_points * 0.29375 // TODO: remove font alignment hack
} else {
0.0
};
let y_offset = y_offset - 3.0; // Tweaked to make text look centered in buttons and text edit fields
self.cache
.entry((scale_in_pixels, font_name.to_owned()))
.or_insert_with(|| {
let ab_glyph_font = self
let (tweak, ab_glyph_font) = self
.ab_glyph_fonts
.get(font_name)
.unwrap_or_else(|| panic!("No font data found for {:?}", font_name))
.clone();
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;
scale_in_points * tweak.y_offset_factor
} + tweak.y_offset;
self.cache
.entry((scale_in_pixels, font_name.to_owned()))
.or_insert_with(|| {
Arc::new(FontImpl::new(
self.atlas.clone(),
self.pixels_per_point,
font_name.to_owned(),
ab_glyph_font,
scale_in_pixels,
y_offset,
y_offset_points,
))
})
.clone()

View file

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

View file

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

View file

@ -152,13 +152,51 @@ pub struct TexturesDelta {
/// New or changed textures. Apply before painting.
pub set: AHashMap<TextureId, ImageDelta>,
/// Texture to free after painting.
/// Textures to free after painting.
pub free: Vec<TextureId>,
}
impl TexturesDelta {
pub fn is_empty(&self) -> bool {
self.set.is_empty() && self.free.is_empty()
}
pub fn append(&mut self, mut newer: TexturesDelta) {
self.set.extend(newer.set.into_iter());
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