Merge remote-tracking branch 'egui/master' into dynamic-grid
This commit is contained in:
commit
1cd2a3c984
107 changed files with 4529 additions and 3858 deletions
16
.github/workflows/rust.yml
vendored
16
.github/workflows/rust.yml
vendored
|
@ -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
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,3 +1,5 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.formatOnSave": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
|
|
|
@ -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`.
|
||||
|
|
79
CHANGELOG.md
79
CHANGELOG.md
|
@ -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`.
|
||||
* Made the v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)).
|
||||
* Plot:
|
||||
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)).
|
||||
* Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)).
|
||||
* Added plot pointer coordinates with `Plot::coordinates_formatter`. ([#1235](https://github.com/emilk/egui/pull/1235)).
|
||||
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
|
||||
* `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)).
|
||||
* `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`.
|
||||
* Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)).
|
||||
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
|
||||
* Added `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 linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
|
||||
* Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)).
|
||||
* 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
133
CODE_OF_CONDUCT.md
Normal 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
1595
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
37
README.md
37
README.md
|
@ -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.
|
||||
|
|
|
@ -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.
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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] = [
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -969,7 +1014,7 @@ impl Ui {
|
|||
.inner
|
||||
}
|
||||
|
||||
/// Add a single[`Widget`] that is possibly disabled, i.e. greyed out and non-interactive.
|
||||
/// Add a single [`Widget`] that is possibly disabled, i.e. greyed out and non-interactive.
|
||||
///
|
||||
/// If you call `add_enabled` from within an already disabled `Ui`,
|
||||
/// the widget will always be disabled, even if the `enabled` argument is true.
|
||||
|
@ -1024,7 +1069,7 @@ impl Ui {
|
|||
})
|
||||
}
|
||||
|
||||
/// Add a single[`Widget`] that is possibly invisible.
|
||||
/// Add a single [`Widget`] that is possibly invisible.
|
||||
///
|
||||
/// An invisible widget still takes up the same space as if it were visible.
|
||||
///
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()))
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -105,6 +105,9 @@ impl super::View for ContextMenus {
|
|||
});
|
||||
});
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::__egui_github_link_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()),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
9
egui_extras/CHANGELOG.md
Normal 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.
|
|
@ -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
8
egui_extras/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# egui_extras
|
||||
|
||||
[](https://crates.io/crates/egui_extras)
|
||||
[](https://docs.rs/egui_extras)
|
||||

|
||||

|
||||
|
||||
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
177
egui_extras/src/image.rs
Normal 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)
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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)).
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)).
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
189
egui_web/src/input.rs
Normal 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(),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
202
egui_web/src/text_agent.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 Bé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 Bé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 Bé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 Bé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 Bé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 Bé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 Bé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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 'S' <= chr && chr <= 'Y' {
|
||||
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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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::*,
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue