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

This commit is contained in:
René Rössler 2022-02-07 10:03:48 +01:00
commit 0f385f6beb
179 changed files with 8173 additions and 3350 deletions

View file

@ -20,7 +20,7 @@ eframe = { git = "https://github.com/emilk/egui", branch = "master" }
-->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
<!-- A clear and concise description of what the bug is. An image is good, a gif or movie is better! -->
**To Reproduce**
Steps to reproduce the behavior:

View file

@ -147,3 +147,9 @@ jobs:
- run: sudo apt-get update && sudo apt-get install libspeechd-dev
- run: rustup target add wasm32-unknown-unknown
- run: cargo doc -p egui_web --target wasm32-unknown-unknown --lib --no-deps --all-features
cargo-deny:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1

View file

@ -42,7 +42,7 @@ Puts an egui app inside a native window on your laptop. Paints the triangles tha
### `eframe`
A wrapper around `egui_web` + `egui_glium`, so you can compile the same app for either web or native.
The demo that you can see at <https://emilk.github.io/egui/index.html> is using `eframe` to host the `egui`. The demo code is found in:
The demo that you can see at <https://www.egui.rs> is using `eframe` to host the `egui`. The demo code is found in:
### `egui_demo_lib`
Depends on `egui` + `epi`.

View file

@ -8,13 +8,51 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased
### Added ⭐
* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)):
* You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`.
* Easily change text styles with `Style::text_styles`.
* Added `Ui::text_style_height`.
* Added `TextStyle::resolve`.
* `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Added `Ui::add_visible` and `Ui::add_visible_ui`.
* Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)).
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
* 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))
### 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`.
* 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)).
* `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)).
### Fixed 🐛
* Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043))
* 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)).
* Calling `Context::set_pixels_per_point` before the first frame will now work.
### 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).
## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame`
@ -334,7 +372,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
### Changed 🔧
* Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority.
* Widgets will now always line break at `\n` characters.
* Widgets will now more intelligently choose wether or not to wrap text.
* Widgets will now more intelligently choose whether or not to wrap text.
* `mouse` has been renamed `pointer` everywhere (to make it clear it includes touches too).
* Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`.
* `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead.
@ -517,6 +555,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
* Optimization: coarse culling in the tessellator
* CHANGED: switch argument order of `ui.checkbox` and `ui.radio`
## 0.1.4 - 2020-09-08
This is when I started the CHANGELOG.md, after almost two years of development. Better late than never.
@ -528,3 +567,13 @@ This is when I started the CHANGELOG.md, after almost two years of development.
* Regions: resizing, vertical scrolling, collapsing headers (sections)
* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons.
* Tooltips on hover
## Earlier:
* 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)
* 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

View file

@ -16,7 +16,7 @@ There is an `egui` discord at <https://discord.gg/vbuv9Xan65>.
## Filing an issue
[Issues](https://github.com/emilk/egui/issues) are for bug reports and feature requests. Issues are not for asking questions (use [Discussions](https://github.com/emilk/egui/discussions) for that).
[Issues](https://github.com/emilk/egui/issues) are for bug reports and feature requests. Issues are not for asking questions (use [Discussions](https://github.com/emilk/egui/discussions) or [Discord](https://discord.gg/vbuv9Xan65) for that).
Always make sure there is not already a similar issue to the one you are creating.
@ -36,6 +36,8 @@ When you feel the PR is ready to go, do a self-review of the code, and then open
Please keep pull requests small and focused.
Don't worry about having many small commits in the PR - they will be squashed to one commit once merged.
Do not include the `.js` and `.wasm` build artifacts generated for building for web.
`git` is not great at storing large files like these, so we only commit a new web demo after a new egui release.
@ -59,6 +61,8 @@ While using an immediate mode gui is simple, implementing one is a lot more tric
* read some code before writing your own
* follow the `egui` code style
* add blank lines around all `fn`, `struct`, `enum`, etc.
* `// Comment like this`, not `//like this`
* write idiomatic rust
* avoid `unsafe`
* avoid code that can cause panics

1340
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,3 +25,6 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
# opt-level = 3 # unecessarily large wasm for no performance gain
# debug = true # include debug symbols, useful when profiling wasm
[patch.crates-io]
ureq = { git = "https://github.com/emilk/ureq/", branch = "opt-in-webpki-roots" } # See https://github.com/algesten/ureq/pull/479 / https://github.com/algesten/ureq/issues/478

130
README.md
View file

@ -9,6 +9,7 @@
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq)
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
@ -18,43 +19,19 @@ egui can be used anywhere you can draw textured triangles, which means you can e
Sections:
* [Example](#example)
* [Quick start](#quick-start)
* [Demo](#demo)
* [Goals](#goals)
* [Who is egui for?](#who-is-egui-for)
* [State / features](#state)
* [How it works](#how-it-works)
* [Integrations](#integrations)
* [Why immediate mode](#why-immediate-mode)
* [FAQ](#faq)
* [Other](#other)
* [Credits](#credits)
## Quick start
If you just want to write a GUI application in Rust (for the web or for native), go to <https://github.com/emilk/eframe_template/> and follow the instructions there! The official docs are at <https://docs.rs/egui>. For inspiration, check out the [the egui web demo](https://emilk.github.io/egui/index.html) and follow the links in it to its source code. There is also an excellent tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
## Demo
[Click to run egui web demo](https://emilk.github.io/egui/index.html) (works in any browser with WASM and WebGL support). Uses [`egui_web`](https://github.com/emilk/egui/tree/master/egui_web).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
### Example
## Example
``` rust
ui.heading("My egui Application");
@ -71,6 +48,30 @@ ui.label(format!("Hello '{}', age {}", name, age));
<img src="media/demo.gif">
## Quick start
If you just want to write a GUI application in Rust (for the web or for native), go to <https://github.com/emilk/eframe_template/> and follow the instructions there! The official docs are at <https://docs.rs/egui>. For inspiration, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code. There is also an excellent tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
## Demo
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`egui_web`](https://github.com/emilk/egui/tree/master/egui_web).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
## Goals
* The easiest to use GUI library
@ -145,72 +146,75 @@ Light Theme:
<img src="media/light_theme.png" width="50%">
## How it works
Loop:
* Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Run application code (Immediate Mode GUI)
* Tell egui to tessellate the frame graphics to a triangle mesh
* Render the triangle mesh with your favorite graphics API (see [OpenGL example](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs)) or use `eframe`, the egui framework crate.
## Integrations
egui is build 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.
The integration needs to do two things:
* **IO**: Supply egui with input (mouse position, keyboard presses, …) and handle egui output (cursor changes, copy-paste integration, …).
* **Painting**: Render the textured triangles that egui outputs.
An integration needs to do the following each frame:
### Official
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Run the application code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs))
There are three official egui integrations made for apps:
### 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).
These are the official egui integrations:
* [`egui_web`](https://github.com/emilk/egui/tree/master/egui_web) for making a web app. Compiles to WASM, renders with WebGL. [Click to run the egui demo](https://emilk.github.io/egui/index.html).
* [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
* [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for compiling native apps with [Glow](https://github.com/grovesNL/glow).
* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [`winit`](https://github.com/rust-windowing/winit). `egui-winit` is used by `egui_glium` and `egui_glow`.
* [`egui_web`](https://github.com/emilk/egui/tree/master/egui_web) for making a web app. Compiles to WASM, renders with WebGL. [Click to run the egui demo](https://www.egui.rs/#demo).
* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit). `egui-winit` is used by `egui_glium` and `egui_glow`.
If you making an app, consider using [`eframe`](https://github.com/emilk/egui/tree/master/eframe), a framework which allows you to write code that works on both the web (`egui_web`) and native (using `egui_glium`).
### 3rd party
### 3rd party integrations
* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/).
* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/).
* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw).
* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad).
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad).
* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2).
* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash).
* [`egui_wgpu_backend`](https://crates.io/crates/egui_wgpu_backend) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad).
* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad).
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash).
* [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs).
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) for the [ggez](https://ggez.rs/) game framework.
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [`godot-rust`](https://github.com/godot-rust/godot-rust).
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
* [`nannou_egui`](https://github.com/AlexEne/nannou_egui) for [nannou](https://nannou.cc).
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
* [`egui_wgpu_backend`](https://crates.io/crates/egui_wgpu_backend) for [`wgpu`](https://crates.io/crates/wgpu) (WebGPU API).
* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/).
Missing an integration for the thing you're working on? Create one, it is easy!
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/):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), 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:
``` rust
let mut egui_ctx = egui::CtxRef::default();
// Game loop:
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| {
my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here
});
let clipped_meshes = egui_ctx.tessellate(shapes); // create triangles to paint
let clipped_meshes = egui_ctx.tessellate(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.set_cursor_icon(output.cursor_icon);
// Also see `egui::Output` for more
if !output.copied_text.is_empty() {
my_integration.set_clipboard_text(output.copied_text);
}
// See `egui::Output` for more
}
```
@ -231,11 +235,12 @@ For a reference OpenGL backend, see [the `egui_glium` painter](https://github.co
* egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
* Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
* Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
* Otherwise: remember to decode gamma in the fragment shader.
* Decode the gamma of the incoming vertex colors in your vertex shader.
* Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
* Otherwise: gamma-encode the colors before you write them again.
* egui prefers linear color spaces for all blending so:
* Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
* Otherwise: remember to decode gamma in the fragment shader.
* Decode the gamma of the incoming vertex colors in your vertex shader.
* Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
* Otherwise: gamma-encode the colors before you write them again.
## Why immediate mode
@ -301,7 +306,7 @@ Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::se
Yes! You can customize the colors, spacing and sizes of everything. By default egui comes with a dark and a light theme.
### What about accessibility, such as screen readers?
There is experimental support for a screen reader. In [the web demo](https://emilk.github.io/egui/index.html) you can enable it in the "Backend" tab.
There is experimental support for a screen reader. In [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
Read more at <https://github.com/emilk/egui/issues/167>.
@ -365,10 +370,13 @@ Notable contributions by:
* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685).
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
* The flattening algorithm for the cubic bezier curve and quadratic bezier curve is from [lyon_geom](https://docs.rs/lyon_geom/latest/lyon_geom/)
Default fonts:
* `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License

68
deny.toml Normal file
View file

@ -0,0 +1,68 @@
# https://embarkstudios.github.io/cargo-deny/
targets = [
{ triple = "aarch64-apple-darwin" },
{ triple = "aarch64-linux-android" },
{ triple = "x86_64-apple-darwin" },
{ triple = "x86_64-pc-windows-msvc" },
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "x86_64-unknown-linux-musl" },
]
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
ignore = [
"RUSTSEC-2020-0071", # https://rustsec.org/advisories/RUSTSEC-2020-0071 - chrono/time: Potential segfault in the time crate
"RUSTSEC-2020-0159", # https://rustsec.org/advisories/RUSTSEC-2020-0159 - chrono/time: Potential segfault in localtime_r invocations
"RUSTSEC-2021-0019", # https://rustsec.org/advisories/RUSTSEC-2021-0019 - xcb - is being worked on: https://github.com/rust-x-bindings/rust-xcb/issues/107
]
[bans]
multiple-versions = "deny"
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
deny = [
{ name = "openssl" }, # prefer rustls
{ name = "openssl-sys" }, # prefer rustls
]
skip = [
{ name = "time" }, # old version pulled in by unmaintianed crate 'chrono'
]
skip-tree = [
{ name = "eframe", version = "0.16.0" },
]
[licenses]
unlicensed = "deny"
allow-osi-fsf-free = "neither"
confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text
copyleft = "deny"
allow = [
"Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
"Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
"ISC", # https://tldrlegal.com/license/-isc-license
"MIT", # https://tldrlegal.com/license/mit-license
"OpenSSL", # https://www.openssl.org/source/license.html
"Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
]
[[licenses.clarify]]
name = "webpki"
expression = "ISC"
license-files = [
{ path = "LICENSE", hash = 0x001c7e6c }
]
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 }
]

1
docs/CNAME Normal file
View file

@ -0,0 +1 @@
www.egui.rs

View file

@ -1,3 +1,3 @@
This folder contains the files required for the egui web demo hosted at <https://emilk.github.io/egui/>.
This folder contains the files required for the egui web demo hosted at <https://www.egui.rs/>.
The reason the folder is called "docs" is because that is the name that GitHub requires in order to host a web page from the `master` branch of a repository.

View file

@ -3,7 +3,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<head>
<meta http-equiv="refresh" content="0; url = https://emilk.github.io/egui/#http" />
<meta http-equiv="refresh" content="0; url = https://www.egui.rs/#http" />
</head>
<body>

View file

@ -46,7 +46,7 @@
transform: translate(-50%, 0%);
}
.loading {
.centered {
margin-right: auto;
margin-left: auto;
display: block;
@ -54,9 +54,10 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
@ -94,8 +95,10 @@
<body>
<!-- The WASM code will resize the canvas dynamically -->
<canvas id="the_canvas_id"></canvas>
<div class="loading" id="loading">
Loading…&nbsp;&nbsp;
<div class="centered" id="center_text">
<p style="font-size:16px">
Loading…
</p>
<div class="lds-dual-ring"></div>
</div>
@ -119,18 +122,33 @@
// We'll defer our execution until the wasm is ready to go.
// Here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done.
console.debug("loading wasm…");
wasm_bindgen("./egui_demo_app_bg.wasm")
.then(on_wasm_loaded)
.catch(console.error);
.catch(on_wasm_error);
function on_wasm_loaded() {
console.log("loaded wasm, starting egui app…");
console.debug("wasm loaded. starting egui app…");
// This call installs a bunch of callbacks and then returns:
wasm_bindgen.start("the_canvas_id");
console.log("egui app started.");
document.getElementById("loading").remove();
console.debug("egui app started.");
document.getElementById("center_text").remove();
}
function on_wasm_error(error) {
console.error("Failed to start egui: " + error);
document.getElementById("center_text").innerHTML = `
<p>
An error occurred loading egui
</p>
<p style="font-family:Courier New">
${error}
</p>
<p style="font-size:14px">
Make sure you use a modern browser with WebGL and WASM enabled.
</p>`;
}
</script>
</body>

View file

@ -1,12 +1,19 @@
# Changelog for eframe
All notable changes to the `eframe` and `epi` crates.
NOTE: [`egui_web`](egui_web/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.md), [`egui_glium`](egui_glium/CHANGELOG.md), and [`egui_glow`](egui_glow/CHANGELOG.md) have their own changelogs!
NOTE: [`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
* 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)).
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Fix horizontal scrolling direction on Linux.
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
* Added `NativeOptions::initial_window_pos`.
* 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)).
## 0.16.0 - 2021-12-29

View file

@ -23,23 +23,6 @@ all-features = true
[lib]
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false }
epi = { version = "0.16.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 }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
egui_web = { version = "0.16.0", path = "../egui_web", default-features = false, features = ["glow"] }
[dev-dependencies]
image = { version = "0.23", default-features = false, features = ["png"] }
rfd = "0.6"
[features]
default = ["default_fonts", "egui_glow"]
@ -57,9 +40,31 @@ persistence = [
"epi/persistence",
]
# experimental support for a screen reader
# enable screen reader support (requires `ctx.options().screen_reader = true;`)
screen_reader = [
# we cannot touch egui_glium or egui_glow here due to https://github.com/rust-lang/cargo/issues/8832
"egui-winit/screen_reader",
"egui_web/screen_reader",
]
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false }
epi = { version = "0.16.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 }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
egui_web = { version = "0.16.0", path = "../egui_web", default-features = false, features = ["glow"] }
[dev-dependencies]
ehttp = "0.2"
image = { version = "0.24", default-features = false, features = ["jpeg", "png"] }
poll-promise = "0.1"
rfd = "0.7"

View file

@ -0,0 +1,49 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi};
#[derive(Default)]
struct MyApp {
can_exit: bool,
is_exiting: bool,
}
impl epi::App for MyApp {
fn name(&self) -> &str {
"Confirm exit"
}
fn on_exit_event(&mut self) -> bool {
self.is_exiting = true;
self.can_exit
}
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Try to close the window");
});
if self.is_exiting {
egui::Window::new("Do you want to quit?")
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("Yes!").clicked() {
self.can_exit = true;
frame.quit();
}
if ui.button("Not yet").clicked() {
self.is_exiting = false;
}
});
});
}
}
}
fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options);
}

View file

@ -19,7 +19,7 @@ impl epi::App for MyApp {
fn setup(
&mut self,
ctx: &egui::CtxRef,
ctx: &egui::Context,
_frame: &epi::Frame,
_storage: Option<&dyn epi::Storage>,
) {
@ -35,14 +35,14 @@ impl epi::App for MyApp {
// Put my font first (highest priority) for proportional text:
fonts
.fonts_for_family
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "my_font".to_owned());
// Put my font as last fallback for monospace:
fonts
.fonts_for_family
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.push("my_font".to_owned());
@ -51,7 +51,7 @@ impl epi::App for MyApp {
ctx.set_fonts(fonts);
}
fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) {
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("egui using custom fonts");
ui.text_edit_multiline(&mut self.text);

View file

@ -0,0 +1,81 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi};
use poll_promise::Promise;
fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options);
}
#[derive(Default)]
struct MyApp {
/// `None` when download hasn't started yet.
promise: Option<Promise<ehttp::Result<egui::TextureHandle>>>,
}
impl epi::App for MyApp {
fn name(&self) -> &str {
"Download and show an image with eframe/egui"
}
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
let promise = self.promise.get_or_insert_with(|| {
// 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| {
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
});
egui::CentralPanel::default().show(ctx, |ui| match promise.ready() {
None => {
ui.add(egui::Spinner::new()); // still loading
}
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);
}
});
}
}
fn parse_response(
ctx: &egui::Context,
response: ehttp::Response,
) -> Result<egui::TextureHandle, 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))
} else {
Err(format!(
"Expected image, found content-type {:?}",
content_type
))
}
}
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View file

@ -13,7 +13,7 @@ impl epi::App for MyApp {
"Native file dialogs and drag-and-drop files"
}
fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) {
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("Drag-and-drop files onto the window!");
@ -57,7 +57,7 @@ impl epi::App for MyApp {
}
impl MyApp {
fn detect_files_being_dropped(&mut self, ctx: &egui::CtxRef) {
fn detect_files_being_dropped(&mut self, ctx: &egui::Context) {
use egui::*;
// Preview hovering files:
@ -82,7 +82,7 @@ impl MyApp {
screen_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Heading,
TextStyle::Heading.resolve(&ctx.style()),
Color32::WHITE,
);
}

View file

@ -21,7 +21,7 @@ impl epi::App for MyApp {
"My egui App"
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
let Self { name, age } = self;
egui::CentralPanel::default().show(ctx, |ui| {

View file

@ -4,7 +4,7 @@ use eframe::{egui, epi};
#[derive(Default)]
struct MyApp {
texture: Option<(egui::Vec2, egui::TextureId)>,
texture: Option<egui::TextureHandle>,
}
impl epi::App for MyApp {
@ -12,31 +12,18 @@ impl epi::App for MyApp {
"Show an image with eframe/egui"
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
if self.texture.is_none() {
// Load the image:
let image_data = include_bytes!("rust-logo-256x256.png");
use image::GenericImageView;
let image = image::load_from_memory(image_data).expect("Failed to load image");
let image_buffer = image.to_rgba8();
let size = [image.width() as usize, image.height() as usize];
let pixels = image_buffer.into_vec();
let image = epi::Image::from_rgba_unmultiplied(size, &pixels);
// Allocate a texture:
let texture = frame.alloc_texture(image);
let size = egui::Vec2::new(size[0] as f32, size[1] as f32);
self.texture = Some((size, texture));
}
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| {
if let Some((size, texture)) = self.texture {
ui.heading("This is an image:");
ui.image(texture, size);
ui.heading("This is an image:");
ui.image(texture, texture.size_vec2());
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(texture, size));
}
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(texture, texture.size_vec2()));
});
}
}
@ -45,3 +32,14 @@ fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options);
}
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View file

@ -26,7 +26,7 @@
//! "My egui App"
//! }
//!
//! fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
//! fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
//! egui::CentralPanel::default().show(ctx, |ui| {
//! ui.heading("Hello World!");
//! });
@ -129,7 +129,7 @@ pub fn start_web(canvas_id: &str, app: Box<dyn epi::App>) -> Result<(), wasm_bin
/// "My egui App"
/// }
///
/// fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
/// fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("Hello World!");
/// });
@ -160,7 +160,7 @@ pub fn run_native(app: Box<dyn epi::App>, native_options: epi::NativeOptions) ->
/// "My egui App"
/// }
///
/// fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
/// fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("Hello World!");
/// });

View file

@ -4,7 +4,12 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
* 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))
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)).
* Fixed `enable_drag` for Windows. Now called only once just after left click ([#1108](https://github.com/emilk/egui/pull/1108)).
## 0.16.0 - 2021-12-29

View file

@ -21,22 +21,9 @@ include = [
[package.metadata.docs.rs]
all-features = true
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = ["single_threaded"] }
instant = { version = "0.1", features = ["wasm-bindgen"] }
winit = "0.26"
epi = { version = "0.16.0", path = "../epi", optional = true }
copypasta = { version = "0.7", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.5", optional = true }
# feature screen_reader
tts = { version = "0.19", optional = true }
[features]
default = ["clipboard", "links"]
default = ["clipboard", "dark-light", "links"]
# enable cut/copy/paste to OS clipboard.
# if disabled a clipboard will be simulated so you can still copy/paste within the egui app.
@ -48,8 +35,25 @@ links = ["webbrowser"]
# experimental support for a screen reader
screen_reader = ["tts"]
persistence = ["egui/serialize", "serde"]
persistence = ["egui/serialize", "serde"] # can't add epi/persistence here because of https://github.com/rust-lang/cargo/issues/8832
serialize = ["egui/serialize", "serde"]
# implement bytemuck on most types.
convert_bytemuck = ["egui/convert_bytemuck"]
[dependencies]
egui = { version = "0.16.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 }
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 }
# feature screen_reader
tts = { version = "0.20", optional = true }

View file

@ -29,7 +29,7 @@ impl Clipboard {
match clipboard.get_contents() {
Ok(contents) => Some(contents),
Err(err) => {
eprintln!("Paste error: {}", err);
tracing::error!("Paste error: {}", err);
None
}
}
@ -46,7 +46,7 @@ impl Clipboard {
if let Some(clipboard) = &mut self.copypasta {
use copypasta::ClipboardProvider as _;
if let Err(err) = clipboard.set_contents(text) {
eprintln!("Copy/Cut error: {}", err);
tracing::error!("Copy/Cut error: {}", err);
}
}
@ -62,7 +62,7 @@ fn init_copypasta() -> Option<copypasta::ClipboardContext> {
match copypasta::ClipboardContext::new() {
Ok(clipboard) => Some(clipboard),
Err(err) => {
eprintln!("Failed to initialize clipboard: {}", err);
tracing::error!("Failed to initialize clipboard: {}", err);
None
}
}

View file

@ -1,29 +1,62 @@
use egui::Vec2;
use winit::dpi::LogicalSize;
pub fn points_to_size(points: Vec2) -> LogicalSize<f64> {
winit::dpi::LogicalSize {
width: points.x as f64,
height: points.y as f64,
}
}
pub fn window_builder(
native_options: &epi::NativeOptions,
window_settings: &Option<crate::WindowSettings>,
) -> winit::window::WindowBuilder {
let window_icon = native_options.icon_data.clone().and_then(load_icon);
let epi::NativeOptions {
always_on_top,
maximized,
decorated,
drag_and_drop_support,
icon_data,
initial_window_pos,
initial_window_size,
min_window_size,
max_window_size,
resizable,
transparent,
} = native_options;
let window_icon = icon_data.clone().and_then(load_icon);
let mut window_builder = winit::window::WindowBuilder::new()
.with_always_on_top(native_options.always_on_top)
.with_maximized(native_options.maximized)
.with_decorations(native_options.decorated)
.with_resizable(native_options.resizable)
.with_transparent(native_options.transparent)
.with_always_on_top(*always_on_top)
.with_maximized(*maximized)
.with_decorations(*decorated)
.with_resizable(*resizable)
.with_transparent(*transparent)
.with_window_icon(window_icon);
window_builder =
window_builder_drag_and_drop(window_builder, native_options.drag_and_drop_support);
if let Some(min_size) = *min_window_size {
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
}
if let Some(max_size) = *max_window_size {
window_builder = window_builder.with_max_inner_size(points_to_size(max_size));
}
let initial_size_points = native_options.initial_window_size;
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
if let Some(window_settings) = window_settings {
window_builder = window_settings.initialize_window(window_builder);
} else if let Some(initial_size_points) = initial_size_points {
window_builder = window_builder.with_inner_size(winit::dpi::LogicalSize {
width: initial_size_points.x as f64,
height: initial_size_points.y as f64,
});
} else {
if let Some(pos) = *initial_window_pos {
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
if let Some(initial_window_size) = *initial_window_size {
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
}
}
window_builder
@ -55,10 +88,9 @@ pub fn handle_app_output(
window: &winit::window::Window,
current_pixels_per_point: f32,
app_output: epi::backend::AppOutput,
) -> epi::backend::TexAllocationData {
) {
let epi::backend::AppOutput {
quit: _,
tex_allocation_data,
window_size,
window_title,
decorated,
@ -86,8 +118,6 @@ pub fn handle_app_output(
if drag_window {
let _ = window.drag_window();
}
tex_allocation_data
}
// ----------------------------------------------------------------------------
@ -191,30 +221,34 @@ impl Persistence {
pub struct EpiIntegration {
frame: epi::Frame,
persistence: crate::epi::Persistence,
pub egui_ctx: egui::CtxRef,
pub egui_ctx: egui::Context,
egui_winit: crate::State,
pub app: Box<dyn epi::App>,
/// When set, it is time to quit
quit: bool,
can_drag_window: bool,
}
impl EpiIntegration {
pub fn new(
integration_name: &'static str,
max_texture_side: usize,
window: &winit::window::Window,
repaint_signal: std::sync::Arc<dyn epi::backend::RepaintSignal>,
persistence: crate::epi::Persistence,
app: Box<dyn epi::App>,
) -> Self {
let egui_ctx = egui::CtxRef::default();
let egui_ctx = egui::Context::default();
*egui_ctx.memory() = persistence.load_memory().unwrap_or_default();
let prefer_dark_mode = prefer_dark_mode();
let frame = epi::Frame::new(epi::backend::FrameData {
info: epi::IntegrationInfo {
name: integration_name,
web_info: None,
prefer_dark_mode: None, // TODO: figure out system default
prefer_dark_mode,
cpu_usage: None,
native_pixels_per_point: Some(crate::native_pixels_per_point(window)),
},
@ -222,13 +256,20 @@ impl EpiIntegration {
repaint_signal,
});
if prefer_dark_mode == Some(true) {
egui_ctx.set_visuals(egui::Visuals::dark());
} else {
egui_ctx.set_visuals(egui::Visuals::light());
}
let mut slf = Self {
frame,
persistence,
egui_ctx,
egui_winit: crate::State::new(window),
egui_winit: crate::State::new(max_texture_side, window),
app,
quit: false,
can_drag_window: false,
};
slf.setup(window);
@ -243,17 +284,19 @@ impl EpiIntegration {
self.app
.setup(&self.egui_ctx, &self.frame, self.persistence.storage());
let app_output = self.frame.take_app_output();
self.quit |= app_output.quit;
let tex_alloc_data =
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
self.frame.lock().output.tex_allocation_data = tex_alloc_data; // Do it later
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);
}
fn warm_up(&mut self, window: &winit::window::Window) {
let saved_memory = self.egui_ctx.memory().clone();
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
let (_, tex_alloc_data, _) = self.update(window);
self.frame.lock().output.tex_allocation_data = tex_alloc_data; // handle it next frame
let (_, textures_delta, _) = self.update(window);
self.egui_ctx.output().textures_delta = textures_delta; // 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();
}
@ -264,8 +307,19 @@ impl EpiIntegration {
}
pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) {
use winit::event::WindowEvent;
self.quit |= matches!(event, WindowEvent::CloseRequested | WindowEvent::Destroyed);
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
WindowEvent::CloseRequested => self.quit = self.app.on_exit_event(),
WindowEvent::Destroyed => self.quit = true,
WindowEvent::MouseInput {
button: MouseButton::Left,
state: ElementState::Pressed,
..
} => self.can_drag_window = true,
_ => {}
}
self.egui_winit.on_event(&self.egui_ctx, event);
}
@ -273,11 +327,7 @@ impl EpiIntegration {
pub fn update(
&mut self,
window: &winit::window::Window,
) -> (
bool,
epi::backend::TexAllocationData,
Vec<egui::epaint::ClippedShape>,
) {
) -> (bool, egui::TexturesDelta, Vec<egui::epaint::ClippedShape>) {
let frame_start = instant::Instant::now();
let raw_input = self.egui_winit.take_egui_input(window);
@ -286,18 +336,24 @@ impl EpiIntegration {
});
let needs_repaint = egui_output.needs_repaint;
self.egui_winit
let textures_delta = self
.egui_winit
.handle_output(window, &self.egui_ctx, egui_output);
let app_output = self.frame.take_app_output();
self.quit |= app_output.quit;
let tex_allocation_data =
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
let mut app_output = self.frame.take_app_output();
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
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, tex_allocation_data, shapes)
(needs_repaint, textures_delta, shapes)
}
pub fn maybe_autosave(&mut self, window: &winit::window::Window) {
@ -311,3 +367,16 @@ impl EpiIntegration {
.save(&mut *self.app, &self.egui_ctx, window);
}
}
#[cfg(feature = "dark-light")]
fn prefer_dark_mode() -> Option<bool> {
match dark_light::detect() {
dark_light::Mode::Dark => Some(true),
dark_light::Mode::Light => Some(false),
}
}
#[cfg(not(feature = "dark-light"))]
fn prefer_dark_mode() -> Option<bool> {
None
}

View file

@ -129,17 +129,22 @@ pub struct State {
}
impl State {
/// Initialize with the native `pixels_per_point` (dpi scaling).
pub fn new(window: &winit::window::Window) -> Self {
Self::from_pixels_per_point(native_pixels_per_point(window))
/// Initialize with:
/// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE`
/// * the native `pixels_per_point` (dpi scaling).
pub fn new(max_texture_side: usize, window: &winit::window::Window) -> Self {
Self::from_pixels_per_point(max_texture_side, native_pixels_per_point(window))
}
/// Initialize with a given dpi scaling.
pub fn from_pixels_per_point(pixels_per_point: f32) -> Self {
/// Initialize with:
/// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE`
/// * the given `pixels_per_point` (dpi scaling).
pub fn from_pixels_per_point(max_texture_side: usize, pixels_per_point: f32) -> Self {
Self {
start_time: instant::Instant::now(),
egui_input: egui::RawInput {
pixels_per_point: Some(pixels_per_point),
max_texture_side,
..Default::default()
},
pointer_pos_in_points: None,
@ -453,17 +458,19 @@ impl State {
egui::vec2(delta.x as f32, delta.y as f32) / self.pixels_per_point()
}
};
if cfg!(target_os = "macos") {
delta.x *= -1.0; // until https://github.com/rust-windowing/winit/pull/2105 is merged and released
}
if cfg!(target_os = "windows") {
delta.x *= -1.0; // until https://github.com/rust-windowing/winit/pull/2101 is merged and released
}
delta.x *= -1.0; // Winit has inverted hscroll. Remove this line when we update winit after https://github.com/rust-windowing/winit/pull/2105 is merged and released
if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command {
// Treat as zoom instead:
let factor = (delta.y / 200.0).exp();
self.egui_input.events.push(egui::Event::Zoom(factor));
} else if self.egui_input.modifiers.shift {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
self.egui_input
.events
.push(egui::Event::Scroll(egui::vec2(delta.x + delta.y, 0.0)));
} else {
self.egui_input.events.push(egui::Event::Scroll(delta));
}
@ -484,7 +491,7 @@ impl State {
if let Some(contents) = self.clipboard.get() {
self.egui_input
.events
.push(egui::Event::Text(contents.replace("\r\n", "\n")));
.push(egui::Event::Paste(contents.replace("\r\n", "\n")));
}
}
}
@ -512,26 +519,39 @@ impl State {
window: &winit::window::Window,
egui_ctx: &egui::Context,
output: egui::Output,
) {
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
if egui_ctx.memory().options.screen_reader {
) -> egui::TexturesDelta {
if egui_ctx.options().screen_reader {
self.screen_reader.speak(&output.events_description());
}
self.set_cursor_icon(window, output.cursor_icon);
let egui::Output {
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;
if let Some(open) = output.open_url {
open_url(&open.url);
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
}
if !output.copied_text.is_empty() {
self.clipboard.set(output.copied_text);
if !copied_text.is_empty() {
self.clipboard.set(copied_text);
}
if let Some(egui::Pos2 { x, y }) = output.text_cursor_pos {
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) {
@ -554,15 +574,15 @@ impl State {
}
}
fn open_url(_url: &str) {
fn open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) {
eprintln!("Failed to open url: {}", err);
tracing::warn!("Failed to open url: {}", err);
}
#[cfg(not(feature = "webbrowser"))]
{
eprintln!("Cannot open url - feature \"links\" not enabled.");
tracing::warn!("Cannot open url - feature \"links\" not enabled.");
}
}

View file

@ -15,11 +15,11 @@ impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::Tts::default() {
Ok(screen_reader) => {
eprintln!("Initialized screen reader.");
tracing::debug!("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
eprintln!("Failed to load screen reader: {}", err);
tracing::warn!("Failed to load screen reader: {}", err);
None
}
};
@ -38,10 +38,10 @@ impl ScreenReader {
return;
}
if let Some(tts) = &mut self.tts {
eprintln!("Speaking: {:?}", text);
tracing::debug!("Speaking: {:?}", text);
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
eprintln!("Failed to read: {}", err);
tracing::warn!("Failed to read: {}", err);
}
}
}

View file

@ -23,13 +23,6 @@ all-features = true
[lib]
[dependencies]
epaint = { version = "0.16.0", path = "../epaint", default-features = false }
ahash = "0.7"
nohash-hasher = "0.2"
ron = { version = "0.7", optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
[features]
default = ["default_fonts", "single_threaded"]
@ -37,6 +30,9 @@ default = ["default_fonts", "single_threaded"]
# add compatibility with https://crates.io/crates/cint
cint = ["epaint/cint"]
# implement bytemuck on most types.
convert_bytemuck = ["epaint/convert_bytemuck"]
# If set, egui will use `include_bytes!` to bundle some fonts.
# If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
@ -55,10 +51,20 @@ persistence = ["serde", "epaint/serialize", "ron"]
# implement serde on most types.
serialize = ["serde", "epaint/serialize"]
# implement bytemuck on most types.
convert_bytemuck = ["epaint/convert_bytemuck"]
# multi_threaded is only needed if you plan to use the same egui::Context
# from multiple threads. It comes with a minor performance impact.
single_threaded = ["epaint/single_threaded"]
multi_threaded = ["epaint/multi_threaded"]
[dependencies]
epaint = { version = "0.16.0", path = "../epaint", default-features = false }
ahash = "0.7"
nohash-hasher = "0.2"
# Optional:
ron = { version = "0.7", optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
# egui doesn't log much, but when it does, it uses `tracing`
tracing = { version = "0.1", optional = true }

View file

@ -1,5 +1,5 @@
There are no stand-alone egui examples, because egui is not stand-alone!
There are plenty of examples in [the online demo](https://emilk.github.io/egui/). You can find the source code for it at <https://github.com/emilk/egui/tree/master/egui_demo_lib>.
There are plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/egui_demo_lib>.
If you are using `eframe`, check out [the `eframe` examples](https://github.com/emilk/egui/tree/master/eframe/examples) and [the `eframe` template repository](https://github.com/emilk/eframe_template/).

View file

@ -3,6 +3,7 @@ use crate::{emath::remap_clamp, Id, IdMap, InputState};
#[derive(Clone, Default)]
pub(crate) struct AnimationManager {
bools: IdMap<BoolAnim>,
values: IdMap<ValueAnim>,
}
#[derive(Clone, Debug)]
@ -12,6 +13,14 @@ struct BoolAnim {
toggle_time: f64,
}
#[derive(Clone, Debug)]
struct ValueAnim {
from_value: f32,
to_value: f32,
/// when did `value` last toggle?
toggle_time: f64,
}
impl AnimationManager {
/// See `Context::animate_bool` for documentation
pub fn animate_bool(
@ -56,4 +65,47 @@ impl AnimationManager {
}
}
}
pub fn animate_value(
&mut self,
input: &InputState,
animation_time: f32,
id: Id,
value: f32,
) -> f32 {
match self.values.get_mut(&id) {
None => {
self.values.insert(
id,
ValueAnim {
from_value: value,
to_value: value,
toggle_time: -f64::INFINITY, // long time ago
},
);
value
}
Some(anim) => {
let time_since_toggle = (input.time - anim.toggle_time) as f32;
// On the frame we toggle we don't want to return the old value,
// so we extrapolate forwards:
let time_since_toggle = time_since_toggle + input.predicted_dt;
let current_value = remap_clamp(
time_since_toggle,
0.0..=animation_time,
anim.from_value..=anim.to_value,
);
if anim.to_value != value {
anim.from_value = current_value; //start new animation from current position of playing animation
anim.to_value = value;
anim.toggle_time = input.time;
}
if animation_time == 0.0 {
anim.from_value = value;
anim.to_value = value;
}
current_value
}
}
}
}

View file

@ -176,7 +176,7 @@ pub(crate) struct Prepared {
impl Area {
pub fn show<R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let prepared = self.begin(ctx);
@ -186,7 +186,7 @@ impl Area {
InnerResponse { inner, response }
}
pub(crate) fn begin(self, ctx: &CtxRef) -> Prepared {
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
let Area {
id,
movable,
@ -234,7 +234,7 @@ impl Area {
}
}
pub fn show_open_close_animation(&self, ctx: &CtxRef, frame: &Frame, is_open: bool) {
pub fn show_open_close_animation(&self, ctx: &Context, frame: &Frame, is_open: bool) {
// must be called first so animation managers know the latest state
let visibility_factor = ctx.animate_bool(self.id.with("close_animation"), is_open);
@ -276,7 +276,7 @@ impl Prepared {
self.drag_bounds
}
pub(crate) fn content_ui(&self, ctx: &CtxRef) -> Ui {
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.input().screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
@ -317,7 +317,7 @@ impl Prepared {
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &CtxRef, content_ui: Ui) -> Response {
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Prepared {
layer_id,
mut state,
@ -370,8 +370,9 @@ impl Prepared {
}
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(pointer_pos) = ctx.input().pointer.interact_pos() {
ctx.input().pointer.any_pressed() && ctx.layer_id_at(pointer_pos) == Some(layer_id)
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let any_pressed = ctx.input().pointer.any_pressed();
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
}

View file

@ -1,9 +1,9 @@
use std::hash::Hash;
use crate::*;
use epaint::{Shape, TextStyle};
use epaint::Shape;
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) struct State {
@ -13,22 +13,13 @@ pub(crate) struct State {
open_height: Option<f32>,
}
impl Default for State {
fn default() -> Self {
Self {
open: false,
open_height: None,
}
}
}
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
ctx.data().insert_persisted(id, self);
}
pub fn from_memory_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
@ -107,7 +98,7 @@ impl State {
}
/// Paint the arrow icon that indicated if the region is open or not
pub(crate) fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) {
pub(crate) fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
let visuals = ui.style().interact(response);
let stroke = visuals.fg_stroke;
@ -126,6 +117,9 @@ pub(crate) fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) {
ui.painter().add(Shape::closed_line(points, stroke));
}
/// A function that paints an icon indicating if the region is open or not
pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
/// A header which can be collapsed/expanded, revealing a contained [`Ui`] region.
///
///
@ -150,6 +144,7 @@ pub struct CollapsingHeader {
selectable: bool,
selected: bool,
show_background: bool,
icon: Option<IconPainter>,
}
impl CollapsingHeader {
@ -171,6 +166,7 @@ impl CollapsingHeader {
selectable: false,
selected: false,
show_background: false,
icon: None,
}
}
@ -245,6 +241,28 @@ impl CollapsingHeader {
self.show_background = show_background;
self
}
/// Use the provided function to render a different `CollapsingHeader` icon.
/// Defaults to a triangle that animates as the `CollapsingHeader` opens and closes.
///
/// For example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
/// let stroke = ui.style().interact(&response).fg_stroke;
/// let radius = egui::lerp(2.0..=3.0, openness);
/// ui.painter().circle_filled(response.rect.center(), radius, stroke.color);
/// }
///
/// egui::CollapsingHeader::new("Circles")
/// .icon(circle_icon)
/// .show(ui, |ui| { ui.label("Hi!"); });
/// # });
/// ```
pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self {
self.icon = Some(Box::new(icon_fn));
self
}
}
struct Prepared {
@ -260,6 +278,7 @@ impl CollapsingHeader {
"Horizontal collapsing is unimplemented"
);
let Self {
icon,
text,
default_open,
open,
@ -319,7 +338,7 @@ impl CollapsingHeader {
if ui.visuals().collapsing_header_frame || self.show_background {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
@ -331,9 +350,8 @@ impl CollapsingHeader {
{
let rect = rect.expand(visuals.expansion);
let corner_radius = 2.0;
ui.painter()
.rect(rect, corner_radius, visuals.bg_fill, visuals.bg_stroke);
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
}
{
@ -347,7 +365,11 @@ impl CollapsingHeader {
..header_response.clone()
};
let openness = state.openness(ui.ctx(), id);
paint_icon(ui, openness, &icon_response);
if let Some(icon) = icon {
icon(ui, openness, &icon_response);
} else {
paint_default_icon(ui, openness, &icon_response);
}
}
text.paint_with_visuals(ui.painter(), text_pos, &visuals);

View file

@ -27,6 +27,16 @@ pub struct ComboBox {
}
impl ComboBox {
/// Create new `ComboBox` with id and label
pub fn new(id_source: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
Self {
id_source: Id::new(id_source),
label: Some(label.into()),
selected_text: Default::default(),
width: None,
}
}
/// Label shown next to the combo box
pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into();
@ -196,6 +206,7 @@ fn combo_box_dyn<'c, R>(
ScrollArea::vertical()
.max_height(ui.spacing().combo_height)
.show(ui, menu_contents)
.inner
});
InnerResponse {
@ -239,7 +250,7 @@ fn button_frame(
where_to_put_background,
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
},

View file

@ -9,7 +9,7 @@ use epaint::*;
pub struct Frame {
/// On each side
pub margin: Vec2,
pub corner_radius: f32,
pub rounding: Rounding,
pub shadow: Shadow,
pub fill: Color32,
pub stroke: Stroke,
@ -24,7 +24,7 @@ impl Frame {
pub fn group(style: &Style) -> Self {
Self {
margin: Vec2::splat(6.0), // symmetric looks best in corners when nesting
corner_radius: style.visuals.widgets.noninteractive.corner_radius,
rounding: style.visuals.widgets.noninteractive.rounding,
stroke: style.visuals.widgets.noninteractive.bg_stroke,
..Default::default()
}
@ -33,7 +33,7 @@ impl Frame {
pub(crate) fn side_top_panel(style: &Style) -> Self {
Self {
margin: Vec2::new(8.0, 2.0),
corner_radius: 0.0,
rounding: Rounding::none(),
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
@ -43,7 +43,7 @@ impl Frame {
pub(crate) fn central_panel(style: &Style) -> Self {
Self {
margin: Vec2::new(8.0, 8.0),
corner_radius: 0.0,
rounding: Rounding::none(),
fill: style.visuals.window_fill(),
stroke: Default::default(),
..Default::default()
@ -53,7 +53,7 @@ impl Frame {
pub fn window(style: &Style) -> Self {
Self {
margin: style.spacing.window_padding,
corner_radius: style.visuals.window_corner_radius,
rounding: style.visuals.window_rounding,
shadow: style.visuals.window_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -63,7 +63,7 @@ impl Frame {
pub fn menu(style: &Style) -> Self {
Self {
margin: Vec2::splat(1.0),
corner_radius: style.visuals.widgets.noninteractive.corner_radius,
rounding: style.visuals.widgets.noninteractive.rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -73,7 +73,7 @@ impl Frame {
pub fn popup(style: &Style) -> Self {
Self {
margin: style.spacing.window_padding,
corner_radius: style.visuals.widgets.noninteractive.corner_radius,
rounding: style.visuals.widgets.noninteractive.rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -84,7 +84,7 @@ impl Frame {
pub fn dark_canvas(style: &Style) -> Self {
Self {
margin: Vec2::new(10.0, 10.0),
corner_radius: style.visuals.widgets.noninteractive.corner_radius,
rounding: style.visuals.widgets.noninteractive.rounding,
fill: Color32::from_black_alpha(250),
stroke: style.visuals.window_stroke(),
..Default::default()
@ -103,8 +103,8 @@ impl Frame {
self
}
pub fn corner_radius(mut self, corner_radius: f32) -> Self {
self.corner_radius = corner_radius;
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into();
self
}
@ -172,7 +172,7 @@ impl Frame {
pub fn paint(&self, outer_rect: Rect) -> Shape {
let Self {
margin: _,
corner_radius,
rounding,
shadow,
fill,
stroke,
@ -180,7 +180,7 @@ impl Frame {
let frame_shape = Shape::Rect(epaint::RectShape {
rect: outer_rect,
corner_radius,
rounding,
fill,
stroke,
});
@ -188,7 +188,7 @@ impl Frame {
if shadow == Default::default() {
frame_shape
} else {
let shadow = shadow.tessellate(outer_rect, corner_radius);
let shadow = shadow.tessellate(outer_rect, rounding);
let shadow = Shape::Mesh(shadow);
Shape::Vec(vec![shadow, frame_shape])
}

View file

@ -25,11 +25,11 @@ struct PanelState {
impl PanelState {
fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(bar_id)
ctx.data().get_persisted(bar_id)
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.memory().data.insert_persisted(bar_id, self);
ctx.data().insert_persisted(bar_id, self);
}
}
@ -199,7 +199,7 @@ impl SidePanel {
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
if let Some(pointer) = ui.input().pointer.latest_pos() {
if let Some(pointer) = ui.ctx().latest_pointer_pos() {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
@ -284,7 +284,7 @@ impl SidePanel {
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
@ -293,7 +293,7 @@ impl SidePanel {
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
@ -485,7 +485,8 @@ impl TopBottomPanel {
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
if let Some(pointer) = ui.input().pointer.latest_pos() {
let latest_pos = ui.input().pointer.latest_pos();
if let Some(pointer) = latest_pos {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
@ -570,7 +571,7 @@ impl TopBottomPanel {
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
@ -579,7 +580,7 @@ impl TopBottomPanel {
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
@ -670,7 +671,7 @@ impl CentralPanel {
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
@ -679,7 +680,7 @@ impl CentralPanel {
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let available_rect = ctx.available_rect();

View file

@ -13,11 +13,11 @@ pub(crate) struct MonoState {
impl MonoState {
fn load(ctx: &Context) -> Option<Self> {
ctx.memory().data.get_temp(Id::null())
ctx.data().get_temp(Id::null())
}
fn store(self, ctx: &Context) {
ctx.memory().data.insert_temp(Id::null(), self);
ctx.data().insert_temp(Id::null(), self);
}
fn tooltip_size(&self, id: Id, index: usize) -> Option<Vec2> {
@ -66,7 +66,11 @@ impl MonoState {
/// }
/// # });
/// ```
pub fn show_tooltip<R>(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
pub fn show_tooltip<R>(
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
show_tooltip_at_pointer(ctx, id, add_contents)
}
@ -88,7 +92,7 @@ pub fn show_tooltip<R>(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui)
/// # });
/// ```
pub fn show_tooltip_at_pointer<R>(
ctx: &CtxRef,
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
@ -104,7 +108,7 @@ pub fn show_tooltip_at_pointer<R>(
///
/// If the tooltip does not fit under the area, it tries to place it above it instead.
pub fn show_tooltip_for<R>(
ctx: &CtxRef,
ctx: &Context,
id: Id,
rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
@ -129,7 +133,7 @@ pub fn show_tooltip_for<R>(
///
/// Returns `None` if the tooltip could not be placed.
pub fn show_tooltip_at<R>(
ctx: &CtxRef,
ctx: &Context,
id: Id,
suggested_position: Option<Pos2>,
add_contents: impl FnOnce(&mut Ui) -> R,
@ -146,7 +150,7 @@ pub fn show_tooltip_at<R>(
}
fn show_tooltip_at_avoid_dyn<'c, R>(
ctx: &CtxRef,
ctx: &Context,
mut id: Id,
suggested_position: Option<Pos2>,
above: bool,
@ -229,7 +233,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
/// }
/// # });
/// ```
pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into<WidgetText>) -> Option<()> {
pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) -> Option<()> {
show_tooltip(ctx, id, |ui| {
crate::widgets::Label::new(text).ui(ui);
})
@ -237,7 +241,7 @@ pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into<WidgetText>) -> O
/// Show a pop-over window.
fn show_tooltip_area_dyn<'c, R>(
ctx: &CtxRef,
ctx: &Context,
id: Id,
window_pos: Pos2,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,

View file

@ -18,11 +18,11 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
ctx.data().insert_persisted(id, self);
}
}

View file

@ -10,9 +10,9 @@ use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) struct State {
pub struct State {
/// Positive offset means scrolling down/right
offset: Vec2,
pub offset: Vec2,
show_scroll: [bool; 2],
@ -43,14 +43,28 @@ impl Default for State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
ctx.data().insert_persisted(id, self);
}
}
pub struct ScrollAreaOutput<R> {
/// What the user closure returned.
pub inner: R,
/// `Id` of the `ScrollArea`.
pub id: Id,
/// The current state of the scroll area.
pub state: State,
/// Where on the screen the content is (excludes scroll bars).
pub inner_rect: Rect,
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// ```
@ -258,6 +272,7 @@ struct Prepared {
/// width of the vertical bar, and the height of the horizontal bar?
current_bar_use: Vec2,
always_show_scroll: bool,
/// Where on the screen the content is (excludes scroll bars).
inner_rect: Rect,
content_ui: Ui,
/// Relative coordinates: the offset and size of the view of the inner UI.
@ -365,7 +380,11 @@ impl ScrollArea {
/// Show the `ScrollArea`, and add the contents to the viewport.
///
/// If the inner area can be very long, consider using [`Self::show_rows`] instead.
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> ScrollAreaOutput<R> {
self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
}
@ -374,7 +393,7 @@ impl ScrollArea {
/// ```
/// # egui::__run_test_ui(|ui| {
/// let text_style = egui::TextStyle::Body;
/// let row_height = ui.fonts()[text_style].row_height();
/// let row_height = ui.text_style_height(&text_style);
/// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
/// let total_rows = 10_000;
/// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| {
@ -391,7 +410,7 @@ impl ScrollArea {
row_height_sans_spacing: f32,
total_rows: usize,
add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
) -> R {
) -> ScrollAreaOutput<R> {
let spacing = ui.spacing().item_spacing;
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
self.show_viewport(ui, |ui, viewport| {
@ -420,7 +439,11 @@ impl ScrollArea {
///
/// `add_contents` is past the viewport, which is the relative view of the content.
/// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
pub fn show_viewport<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, Rect) -> R) -> R {
pub fn show_viewport<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui, Rect) -> R,
) -> ScrollAreaOutput<R> {
self.show_viewport_dyn(ui, Box::new(add_contents))
}
@ -428,16 +451,23 @@ impl ScrollArea {
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
) -> R {
) -> ScrollAreaOutput<R> {
let mut prepared = self.begin(ui);
let ret = add_contents(&mut prepared.content_ui, prepared.viewport);
prepared.end(ui);
ret
let id = prepared.id;
let inner_rect = prepared.inner_rect;
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
let state = prepared.end(ui);
ScrollAreaOutput {
inner,
id,
state,
inner_rect,
}
}
}
impl Prepared {
fn end(self, ui: &mut Ui) {
fn end(self, ui: &mut Ui) -> State {
let Prepared {
id,
mut state,
@ -523,12 +553,11 @@ impl Prepared {
};
let content_response = ui.interact(inner_rect, id.with("area"), sense);
let input = ui.input();
if content_response.dragged() {
for d in 0..2 {
if has_bar[d] {
state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d];
state.offset[d] -= ui.input().pointer.delta()[d];
state.vel[d] = ui.input().pointer.velocity()[d];
state.scroll_stuck_to_end[d] = false;
} else {
state.vel[d] = 0.0;
@ -537,7 +566,7 @@ impl Prepared {
} else {
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = input.unstable_dt;
let dt = ui.input().unstable_dt;
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
@ -711,13 +740,13 @@ impl Prepared {
ui.painter().add(epaint::Shape::rect_filled(
outer_scroll_rect,
visuals.corner_radius,
visuals.rounding,
ui.visuals().extreme_bg_color,
));
ui.painter().add(epaint::Shape::rect_filled(
handle_rect,
visuals.corner_radius,
visuals.rounding,
visuals.bg_fill,
));
}
@ -748,6 +777,8 @@ impl Prepared {
state.show_scroll = show_scroll_this_frame;
state.store(ui.ctx(), id);
state
}
}

View file

@ -235,7 +235,7 @@ impl<'open> Window<'open> {
#[inline]
pub fn show<R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<Option<R>>> {
self.show_dyn(ctx, Box::new(add_contents))
@ -243,7 +243,7 @@ impl<'open> Window<'open> {
fn show_dyn<'c, R>(
self,
ctx: &CtxRef,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<InnerResponse<Option<R>>> {
let Window {
@ -296,7 +296,8 @@ impl<'open> Window<'open> {
.and_then(|window_interaction| {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
title.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing
let style = ctx.style();
title.font_height(&ctx.fonts(), &style) + title_content_spacing
} else {
0.0
};
@ -353,7 +354,7 @@ impl<'open> Window<'open> {
}
if scroll.has_any_bar() {
scroll.show(ui, add_contents)
scroll.show(ui, add_contents).inner
} else {
add_contents(ui)
}
@ -698,42 +699,62 @@ fn paint_frame_interaction(
) {
use epaint::tessellator::path::add_circle_quadrant;
let cr = ui.visuals().window_corner_radius;
let rounding = ui.visuals().window_rounding;
let Rect { min, max } = rect;
let mut points = Vec::new();
if interaction.right && !interaction.bottom && !interaction.top {
points.push(pos2(max.x, min.y + cr));
points.push(pos2(max.x, max.y - cr));
points.push(pos2(max.x, min.y + rounding.ne));
points.push(pos2(max.x, max.y - rounding.se));
}
if interaction.right && interaction.bottom {
points.push(pos2(max.x, min.y + cr));
points.push(pos2(max.x, max.y - cr));
add_circle_quadrant(&mut points, pos2(max.x - cr, max.y - cr), cr, 0.0);
points.push(pos2(max.x, min.y + rounding.ne));
points.push(pos2(max.x, max.y - rounding.se));
add_circle_quadrant(
&mut points,
pos2(max.x - rounding.se, max.y - rounding.se),
rounding.se,
0.0,
);
}
if interaction.bottom {
points.push(pos2(max.x - cr, max.y));
points.push(pos2(min.x + cr, max.y));
points.push(pos2(max.x - rounding.se, max.y));
points.push(pos2(min.x + rounding.sw, max.y));
}
if interaction.left && interaction.bottom {
add_circle_quadrant(&mut points, pos2(min.x + cr, max.y - cr), cr, 1.0);
add_circle_quadrant(
&mut points,
pos2(min.x + rounding.sw, max.y - rounding.sw),
rounding.sw,
1.0,
);
}
if interaction.left {
points.push(pos2(min.x, max.y - cr));
points.push(pos2(min.x, min.y + cr));
points.push(pos2(min.x, max.y - rounding.sw));
points.push(pos2(min.x, min.y + rounding.nw));
}
if interaction.left && interaction.top {
add_circle_quadrant(&mut points, pos2(min.x + cr, min.y + cr), cr, 2.0);
add_circle_quadrant(
&mut points,
pos2(min.x + rounding.nw, min.y + rounding.nw),
rounding.nw,
2.0,
);
}
if interaction.top {
points.push(pos2(min.x + cr, min.y));
points.push(pos2(max.x - cr, min.y));
points.push(pos2(min.x + rounding.nw, min.y));
points.push(pos2(max.x - rounding.ne, min.y));
}
if interaction.right && interaction.top {
add_circle_quadrant(&mut points, pos2(max.x - cr, min.y + cr), cr, 3.0);
points.push(pos2(max.x, min.y + cr));
points.push(pos2(max.x, max.y - cr));
add_circle_quadrant(
&mut points,
pos2(max.x - rounding.ne, min.y + rounding.ne),
rounding.ne,
3.0,
);
points.push(pos2(max.x, min.y + rounding.ne));
points.push(pos2(max.x, max.y - rounding.se));
}
ui.painter().add(Shape::line(points, visuals.bg_stroke));
}
@ -741,9 +762,16 @@ fn paint_frame_interaction(
// ----------------------------------------------------------------------------
struct TitleBar {
/// A title Id used for dragging windows
id: Id,
/// Prepared text in the title
title_galley: WidgetTextGalley,
/// Size of the title bar in a collapsed state (if window is collapsible),
/// which includes all necessary space for showing the expand button, the
/// title and the close button.
min_rect: Rect,
/// Size of the title bar in an expanded state. This size become known only
/// after expanding window and painting its content
rect: Rect,
}
@ -757,7 +785,7 @@ fn show_title_bar(
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = title
.font_height(ui.fonts(), ui.style())
.font_height(&ui.fonts(), ui.style())
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);
@ -775,7 +803,7 @@ fn show_title_bar(
collapsing.toggle(ui);
}
let openness = collapsing.openness(ui.ctx(), collapsing_id);
collapsing_header::paint_icon(ui, openness, &collapse_button_response);
collapsing_header::paint_default_icon(ui, openness, &collapse_button_response);
}
let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading);
@ -804,6 +832,20 @@ fn show_title_bar(
}
impl TitleBar {
/// Finishes painting of the title bar when the window content size already known.
///
/// # Parameters
///
/// - `ui`:
/// - `outer_rect`:
/// - `content_response`: if `None`, window is collapsed at this frame, otherwise contains
/// a result of rendering the window content
/// - `open`: if `None`, no "Close" button will be rendered, otherwise renders and processes
/// the "Close" button and writes a `false` if window was closed
/// - `collapsing`: holds the current expanding state. Can be changed by double click on the
/// title if `collapsible` is `true`
/// - `collapsible`: if `true`, double click on the title bar will be handled for a change
/// of `collapsing` state
fn ui(
mut self,
ui: &mut Ui,
@ -857,6 +899,11 @@ impl TitleBar {
}
}
/// Paints the "Close" button at the right side of the title bar
/// and processes clicks on it.
///
/// The button is square and its size is determined by the
/// [`crate::style::Spacing::icon_width`] setting.
fn close_button_ui(&self, ui: &mut Ui) -> Response {
let button_size = Vec2::splat(ui.spacing().icon_width);
let pad = (self.rect.height() - button_size.y) / 2.0; // calculated so that the icon is on the diagonal (if window padding is symmetrical)
@ -872,6 +919,16 @@ impl TitleBar {
}
}
/// Paints the "Close" button of the window and processes clicks on it.
///
/// The close button is just an `X` symbol painted by a current stroke
/// for foreground elements (such as a label text).
///
/// # Parameters
/// - `ui`:
/// - `rect`: The rectangular area to fit the button in
///
/// Returns the result of a click on a button if it was pressed
fn close_button(ui: &mut Ui, rect: Rect) -> Response {
let close_id = ui.auto_id_with("window_close_button");
let response = ui.interact(rect, close_id, Sense::click());
@ -880,9 +937,9 @@ fn close_button(ui: &mut Ui, rect: Rect) -> Response {
let visuals = ui.style().interact(&response);
let rect = rect.shrink(2.0).expand(visuals.expansion);
let stroke = visuals.fg_stroke;
ui.painter()
ui.painter() // paints \
.line_segment([rect.left_top(), rect.right_bottom()], stroke);
ui.painter()
ui.painter() // paints /
.line_segment([rect.right_top(), rect.left_bottom()], stroke);
response
}

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,13 @@ pub struct RawInput {
/// Set this the first frame, whenever it changes, or just on every frame.
pub pixels_per_point: Option<f32>,
/// Maximum size of one side of the font texture.
///
/// 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,
/// 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).
pub time: Option<f64>,
@ -62,6 +69,7 @@ impl Default for RawInput {
Self {
screen_rect: None,
pixels_per_point: None,
max_texture_side: 2048,
time: None,
predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(),
@ -81,6 +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,
time: self.time.take(),
predicted_dt: self.predicted_dt,
modifiers: self.modifiers,
@ -95,6 +104,7 @@ impl RawInput {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
@ -105,6 +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.time = time; // use latest time
self.predicted_dt = predicted_dt; // use latest dt
self.modifiers = modifiers; // use latest
@ -135,7 +146,7 @@ pub struct DroppedFile {
/// Set by the `egui_web` backend.
pub last_modified: Option<std::time::SystemTime>,
/// Set by the `egui_web` backend.
pub bytes: Option<std::sync::Arc<[u8]>>,
pub bytes: Option<epaint::mutex::Arc<[u8]>>,
}
/// An input event generated by the integration.
@ -148,7 +159,9 @@ pub enum Event {
Copy,
/// The integration detected a "cut" event (e.g. Cmd+X).
Cut,
/// Text input, e.g. via keyboard or paste action.
/// The integration detected a "paste" event (e.g. Cmd+V).
Paste(String),
/// Text input, e.g. via keyboard.
///
/// When the user presses enter/return, do not send a `Text` (just [`Key::Enter`]).
Text(String),
@ -178,6 +191,8 @@ pub enum Event {
/// The direction of the vector indicates how to move the _content_ that is being viewed.
/// So if you get positive values, the content being viewed should move to the right and down,
/// revealing new things to the left and up.
///
/// Shift-scroll should result in horizontal scrolling (it is up to the integrations to do this).
Scroll(Vec2),
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
@ -353,6 +368,7 @@ impl RawInput {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
@ -366,6 +382,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));
if let Some(time) = time {
ui.label(format!("time: {:.3} s", time));
} else {
@ -373,10 +390,13 @@ impl RawInput {
}
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("events: {:?}", events))
.on_hover_text("key presses etc");
ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
.on_hover_text("key presses etc");
});
}
}

View file

@ -35,6 +35,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 {
@ -71,6 +74,7 @@ impl Output {
mut events,
mutable_text_under_cursor,
text_cursor_pos,
textures_delta,
} = newer;
self.cursor_icon = cursor_icon;
@ -84,6 +88,7 @@ impl Output {
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)

View file

@ -72,7 +72,7 @@ impl FrameState {
pub(crate) fn available_rect(&self) -> Rect {
crate::egui_assert!(
self.available_rect.is_finite(),
"Called `available_rect()` before `CtxRef::run()`"
"Called `available_rect()` before `Context::run()`"
);
self.available_rect
}

View file

@ -9,11 +9,11 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
ctx.data().insert_persisted(id, self);
}
fn set_min_col_width(&mut self, col: usize, width: f32) {
@ -45,8 +45,8 @@ impl State {
// ----------------------------------------------------------------------------
pub(crate) struct GridLayout {
ctx: CtxRef,
style: std::sync::Arc<Style>,
ctx: Context,
style: epaint::mutex::Arc<Style>,
id: Id,
/// State previous frame (if any).

View file

@ -8,7 +8,7 @@
/// moved outside the slider.
///
/// For some widgets `Id`s are also used to persist some state about the
/// widgets, such as Window position or wether not a collapsing header region is open.
/// widgets, such as Window position or whether not a collapsing header region is open.
///
/// This implies that the `Id`s must be unique.
///
@ -19,7 +19,7 @@
///
/// For things that need to persist state even after moving (windows, collapsing headers)
/// the location of the widgets is obviously not good enough. For instance,
/// a collapsing region needs to remember wether or not it is open even
/// a collapsing region needs to remember whether or not it is open even
/// if the layout next frame is different and the collapsing is not lower down
/// on the screen.
///

View file

@ -261,7 +261,8 @@ impl InputState {
/// # egui::__run_test_ui(|ui| {
/// let mut zoom = 1.0; // no zoom
/// let mut rotation = 0.0; // no rotation
/// if let Some(multi_touch) = ui.input().multi_touch() {
/// let multi_touch = ui.input().multi_touch();
/// if let Some(multi_touch) = multi_touch {
/// zoom *= multi_touch.zoom_delta;
/// rotation += multi_touch.rotation_delta;
/// }
@ -699,7 +700,12 @@ impl InputState {
events,
} = self;
ui.style_mut().body_text_style = epaint::TextStyle::Monospace;
ui.style_mut()
.text_styles
.get_mut(&crate::TextStyle::Body)
.unwrap()
.family = crate::FontFamily::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("🖱 Pointer")
@ -729,8 +735,11 @@ impl InputState {
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("keys_down: {:?}", keys_down));
ui.label(format!("events: {:?}", events))
.on_hover_text("key presses etc");
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
.on_hover_text("key presses etc");
});
}
}

View file

@ -7,6 +7,7 @@ use crate::{
};
/// All you probably need to know about a multi-touch gesture.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MultiTouchInfo {
/// Point in time when the gesture started.
pub start_time: f64,

View file

@ -1,75 +1,79 @@
//! uis for egui types.
//! Showing UI:s for egui/epaint types.
use crate::*;
impl Widget for &epaint::FontImage {
fn ui(self, ui: &mut Ui) -> Response {
use epaint::Mesh;
ui.vertical(|ui| {
// Show font texture in demo Ui
ui.label(format!(
"Texture size: {} x {} (hover to zoom)",
self.width, self.height
));
if self.width <= 1 || self.height <= 1 {
return;
}
let mut size = vec2(self.width as f32, self.height as f32);
if size.x > ui.available_width() {
size *= ui.available_width() / size.x;
}
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(
rect,
[pos2(0.0, 0.0), pos2(1.0, 1.0)].into(),
Color32::WHITE,
);
ui.painter().add(Shape::mesh(mesh));
let (tex_w, tex_h) = (self.width as f32, self.height as f32);
response
.on_hover_cursor(CursorIcon::ZoomIn)
.on_hover_ui_at_pointer(|ui| {
if let Some(pos) = ui.input().pointer.latest_pos() {
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);
let texel_radius = 32.0;
let u = u.at_least(texel_radius).at_most(tex_w - texel_radius);
let v = v.at_least(texel_radius).at_most(tex_h - texel_radius);
let uv_rect = Rect::from_min_max(
pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h),
pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h),
);
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(zoom_rect, uv_rect, Color32::WHITE);
ui.painter().add(Shape::mesh(mesh));
}
});
})
.response
}
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
let families = ui.fonts().families();
ui.horizontal(|ui| {
for alternative in families {
let text = alternative.to_string();
ui.radio_value(font_family, alternative, text);
}
});
}
impl Widget for &mut epaint::text::FontDefinitions {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
for (text_style, (_family, size)) in self.family_and_size.iter_mut() {
// TODO: radio button for family
ui.add(
Slider::new(size, 4.0..=40.0)
.max_decimals(0)
.text(format!("{:?}", text_style)),
);
}
crate::reset_button(ui, self);
})
.response
}
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
let families = ui.fonts().families();
ui.horizontal(|ui| {
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(0));
for alternative in families {
let text = alternative.to_string();
ui.radio_value(&mut font_id.family, alternative, text);
}
});
}
// Show font texture in demo Ui
pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response {
use epaint::Mesh;
ui.vertical(|ui| {
let color = if ui.visuals().dark_mode {
Color32::WHITE
} else {
Color32::BLACK
};
ui.label(format!(
"Texture size: {} x {} (hover to zoom)",
width, height
));
if width <= 1 || height <= 1 {
return;
}
let mut size = vec2(width as f32, height as f32);
if size.x > ui.available_width() {
size *= ui.available_width() / size.x;
}
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), color);
ui.painter().add(Shape::mesh(mesh));
let (tex_w, tex_h) = (width as f32, height as f32);
response
.on_hover_cursor(CursorIcon::ZoomIn)
.on_hover_ui_at_pointer(|ui| {
if let Some(pos) = ui.ctx().latest_pointer_pos() {
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);
let texel_radius = 32.0;
let u = u.at_least(texel_radius).at_most(tex_w - texel_radius);
let v = v.at_least(texel_radius).at_most(tex_h - texel_radius);
let uv_rect = Rect::from_min_max(
pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h),
pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h),
);
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(zoom_rect, uv_rect, color);
ui.painter().add(Shape::mesh(mesh));
}
});
})
.response
}
impl Widget for &epaint::stats::PaintStats {
@ -77,11 +81,11 @@ impl Widget for &epaint::stats::PaintStats {
ui.vertical(|ui| {
ui.label(
"egui generates intermediate level shapes like circles and text. \
These are later tessellated into triangles.",
These are later tessellated into triangles.",
);
ui.add_space(10.0);
ui.style_mut().body_text_style = TextStyle::Monospace;
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
let epaint::stats::PaintStats {
shapes,
@ -124,7 +128,7 @@ impl Widget for &epaint::stats::PaintStats {
}
}
pub fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response {
fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response {
ui.add(Label::new(alloc_info.format(what)).wrap(false))
}
@ -140,9 +144,17 @@ impl Widget for &mut epaint::TessellationOptions {
debug_paint_clip_rects,
debug_paint_text_rects,
debug_ignore_clip_rects,
bezier_tolerance,
epsilon: _,
} = self;
ui.checkbox(anti_alias, "Antialias")
.on_hover_text("Turn off for small performance gain.");
ui.add(
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
.logarithmic(true)
.show_value(true)
.text("Spline Tolerance"),
);
ui.collapsing("debug", |ui| {
ui.checkbox(
coarse_tessellation_culling,

View file

@ -2,9 +2,7 @@
//! are sometimes painted behind or in front of other things.
use crate::{Id, *};
use epaint::mutex::Mutex;
use epaint::{ClippedShape, Shape};
use std::sync::Arc;
/// Different layer categories
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
@ -153,10 +151,10 @@ impl PaintList {
}
#[derive(Clone, Default)]
pub(crate) struct GraphicLayers([IdMap<Arc<Mutex<PaintList>>>; Order::COUNT]);
pub(crate) struct GraphicLayers([IdMap<PaintList>; Order::COUNT]);
impl GraphicLayers {
pub fn list(&mut self, layer_id: LayerId) -> &Arc<Mutex<PaintList>> {
pub fn list(&mut self, layer_id: LayerId) -> &mut PaintList {
self.0[layer_id.order as usize]
.entry(layer_id.id)
.or_default()
@ -171,20 +169,20 @@ impl GraphicLayers {
// If a layer is empty at the start of the frame
// then nobody has added to it, and it is old and defunct.
// Free it to save memory:
order_map.retain(|_, list| !list.lock().is_empty());
order_map.retain(|_, list| !list.is_empty());
// First do the layers part of area_order:
for layer_id in area_order {
if layer_id.order == order {
if let Some(list) = order_map.get_mut(&layer_id.id) {
all_shapes.append(&mut list.lock().0);
all_shapes.append(&mut list.0);
}
}
}
// Also draw areas that are missing in `area_order`:
for shapes in order_map.values_mut() {
all_shapes.append(&mut shapes.lock().0);
all_shapes.append(&mut shapes.0);
}
}

View file

@ -77,7 +77,6 @@ impl Region {
/// Layout direction, one of `LeftToRight`, `RightToLeft`, `TopDown`, `BottomUp`.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Direction {
LeftToRight,
RightToLeft,

View file

@ -1,6 +1,6 @@
//! `egui`: an easy-to-use GUI in pure Rust!
//!
//! Try the live web demo: <https://emilk.github.io/egui/index.html>. Read more about egui at <https://github.com/emilk/egui>.
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.56.0 or later to use `egui`.
@ -8,13 +8,13 @@
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
//!
//! To create a GUI using egui you first need a [`CtxRef`] (by convention referred to by `ctx`).
//! To create a GUI using egui you first need a [`Context`] (by convention referred to by `ctx`).
//! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
//!
//!
//! # Using egui
//!
//! To see what is possible to build with egui you can check out the online demo at <https://emilk.github.io/egui/#demo>.
//! To see what is possible to build with egui you can check out the online demo at <https://www.egui.rs/#demo>.
//!
//! If you like the "learning by doing" approach, clone <https://github.com/emilk/eframe_template> and get started using egui right away.
//!
@ -58,7 +58,7 @@
//!
//! ### Quick start
//!
//! ``` rust
//! ```
//! # egui::__run_test_ui(|ui| {
//! # let mut my_string = String::new();
//! # let mut my_boolean = true;
@ -113,7 +113,7 @@
//! # fn handle_output(_: egui::Output) {}
//! # fn paint(_: Vec<egui::ClippedMesh>) {}
//! # fn gather_input() -> egui::RawInput { egui::RawInput::default() }
//! let mut ctx = egui::CtxRef::default();
//! let mut ctx = egui::Context::default();
//!
//! // Game loop:
//! loop {
@ -218,7 +218,7 @@
//! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`].
//! 3. Use a justified layout:
//!
//! ``` rust
//! ```
//! # egui::__run_test_ui(|ui| {
//! ui.with_layout(egui::Layout::top_down_justified(egui::Align::Center), |ui| {
//! ui.button("I am becoming wider as needed");
@ -228,7 +228,7 @@
//!
//! 4. Fill in extra space with emptiness:
//!
//! ``` rust
//! ```
//! # egui::__run_test_ui(|ui| {
//! ui.allocate_space(ui.available_size()); // put this LAST in your panel/window code
//! # });
@ -364,7 +364,7 @@ mod frame_state;
pub(crate) mod grid;
mod id;
mod input_state;
mod introspection;
pub mod introspection;
pub mod layers;
mod layout;
mod memory;
@ -385,20 +385,22 @@ 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, TextStyle},
ClippedMesh, Color32, FontImage, Rgba, Shape, Stroke, TextureId,
text::{FontData, FontDefinitions, FontFamily, FontId},
textures::TexturesDelta,
AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Rgba, Rounding, Shape, Stroke,
TextureHandle, TextureId,
};
pub mod text {
pub use epaint::text::{
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat,
TextStyle, TAB_SIZE,
TAB_SIZE,
};
}
pub use {
containers::*,
context::{Context, CtxRef},
context::Context,
data::{
input::*,
output::{self, CursorIcon, Output, WidgetInfo},
@ -412,7 +414,7 @@ pub use {
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,
style::{Style, Visuals},
style::{FontSelection, Style, TextStyle, Visuals},
text::{Galley, TextFormat},
ui::Ui,
widget_text::{RichText, WidgetText},
@ -509,7 +511,7 @@ macro_rules! egui_assert {
// ----------------------------------------------------------------------------
/// egui supports around 1216 emojis in total.
/// The default egui fonts supports around 1216 emojis in total.
/// Here are some of the most useful:
/// ∞⊗⎗⎘⎙⏏⏴⏵⏶⏷
/// ⏩⏪⏭⏮⏸⏹⏺■▶📾🔀🔁🔃
@ -524,7 +526,7 @@ macro_rules! egui_assert {
///
/// NOTE: In egui all emojis are monochrome!
///
/// You can explore them all in the Font Book in [the online demo](https://emilk.github.io/egui/).
/// You can explore them all in the Font Book in [the online demo](https://www.egui.rs/#demo).
///
/// In addition, egui supports a few special emojis that are not part of the unicode standard.
/// This module contains some of them:
@ -574,8 +576,8 @@ pub enum WidgetType {
// ----------------------------------------------------------------------------
/// For use in tests; especially doctests.
pub fn __run_test_ctx(mut run_ui: impl FnMut(&CtxRef)) {
let mut ctx = CtxRef::default();
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
let ctx = Context::default();
let _ = ctx.run(Default::default(), |ctx| {
run_ui(ctx);
});
@ -583,7 +585,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&CtxRef)) {
/// For use in tests; especially doctests.
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let mut ctx = CtxRef::default();
let ctx = Context::default();
let _ = ctx.run(Default::default(), |ctx| {
crate::CentralPanel::default().show(ctx, |ui| {
add_contents(ui);

View file

@ -19,7 +19,15 @@ use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
pub struct Memory {
pub options: Options,
/// This map stores current states for all widgets with custom `Id`s.
/// This map stores some superficial state for all widgets with custom `Id`s.
///
/// This includes storing if a [`crate::CollapsingHeader`] is open, how far scrolled a
/// [`crate::ScrollArea`] is, where the cursor in a [`crate::TextEdit`] is, etc.
///
/// This is NOT meant to store any important data. Store that in your own structures!
///
/// Each read clones the data, so keep your values cheap to clone.
/// If you want to store a lot of data you should wrap it in `Arc<Mutex<…>>` so it is cheap to clone.
///
/// This will be saved between different program runs if you use the `persistence` feature.
///
@ -45,7 +53,7 @@ pub struct Memory {
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut ctx = egui::CtxRef::default();
/// # let mut ctx = egui::Context::default();
/// let mut memory = ctx.memory();
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
@ -85,13 +93,13 @@ pub struct Memory {
// ----------------------------------------------------------------------------
/// Some global options that you can read and write.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Options {
/// The default style for new `Ui`:s.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) style: std::sync::Arc<Style>,
pub(crate) style: epaint::mutex::Arc<Style>,
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
@ -100,6 +108,25 @@ pub struct Options {
/// but is a signal to any backend that we want the [`crate::Output::events`] read out loud.
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
pub screen_reader: bool,
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
///
/// Only the fonts in [`Style::text_styles`] will be pre-cached.
///
/// This can lead to fewer texture operations, but may use up the texture atlas quicker
/// if you are changing [`Style::text_styles`], of have a lot of text styles.
pub preload_font_glyphs: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
style: Default::default(),
tessellation_options: Default::default(),
screen_reader: false,
preload_font_glyphs: true,
}
}
}
// ----------------------------------------------------------------------------
@ -396,7 +423,7 @@ impl Memory {
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be be open at a time.
impl Memory {
pub fn is_popup_open(&mut self, popup_id: Id) -> bool {
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popup == Some(popup_id) || self.everything_is_visible()
}
@ -541,7 +568,8 @@ impl Areas {
..
} = self;
*visible_last_frame = std::mem::take(visible_current_frame);
std::mem::swap(visible_last_frame, visible_current_frame);
visible_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
wants_to_be_on_top.clear();
}

View file

@ -16,12 +16,11 @@
//! ```
use super::{
style::WidgetVisuals, Align, CtxRef, Id, InnerResponse, PointerState, Pos2, Rect, Response,
style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
Sense, TextStyle, Ui, Vec2,
};
use crate::{widgets::*, *};
use epaint::{mutex::RwLock, Stroke};
use std::sync::Arc;
use epaint::{mutex::Arc, mutex::RwLock, Stroke};
/// What is saved between frames.
#[derive(Clone, Default)]
@ -31,14 +30,11 @@ pub(crate) struct BarState {
impl BarState {
fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.memory()
.data
.get_temp::<Self>(bar_id)
.unwrap_or_default()
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.memory().data.insert_temp(bar_id, self);
ctx.data().insert_temp(bar_id, self);
}
/// Show a menu at pointer if primary-clicked response.
@ -116,7 +112,7 @@ pub(crate) fn submenu_button<R>(
/// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>(
ctx: &CtxRef,
ctx: &Context,
menu_id: impl std::hash::Hash,
menu_state_arc: &Arc<RwLock<MenuState>>,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
@ -187,33 +183,19 @@ fn stationary_menu_impl<'c, R>(
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Stores the state for the context menu.
#[derive(Default)]
pub(crate) struct ContextMenuSystem {
root: MenuRootManager,
}
impl ContextMenuSystem {
/// Show a menu at pointer if right-clicked response.
/// Should be called from [`Context`] on a [`Response`]
pub fn context_menu(
&mut self,
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
MenuRoot::context_click_interaction(response, &mut self.root, response.id);
self.root.show(response, add_contents)
}
}
impl std::ops::Deref for ContextMenuSystem {
type Target = MenuRootManager;
fn deref(&self) -> &Self::Target {
&self.root
}
}
impl std::ops::DerefMut for ContextMenuSystem {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.root
}
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub(crate) fn context_menu(
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
let menu_id = Id::new("__egui::context_menu");
let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state, response.id);
let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id);
inner_response
}
/// Stores the state for the context menu.
@ -429,7 +411,8 @@ impl SubMenuButton {
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = text.into_galley(ui, Some(true), text_available_width, text_style);
let text_galley =
text.into_galley(ui, Some(true), text_available_width, text_style.clone());
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style);
@ -455,7 +438,7 @@ impl SubMenuButton {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.rounding,
visuals.bg_fill,
);
@ -520,7 +503,7 @@ impl MenuState {
self.response = MenuResponse::Close;
}
pub fn show<R>(
ctx: &CtxRef,
ctx: &Context,
menu_state: &Arc<RwLock<Self>>,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
@ -529,7 +512,7 @@ impl MenuState {
}
fn show_submenu<R>(
&mut self,
ctx: &CtxRef,
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {

View file

@ -1,12 +1,12 @@
use crate::{
emath::{Align2, Pos2, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, CtxRef,
Color32, Context, FontId,
};
use epaint::{
mutex::Mutex,
text::{Fonts, Galley, TextStyle},
CircleShape, RectShape, Shape, Stroke, TextShape,
mutex::{Arc, RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke, TextShape,
};
/// Helper to paint shapes and text to a specific region on a specific layer.
@ -15,13 +15,11 @@ use epaint::{
#[derive(Clone)]
pub struct Painter {
/// Source of fonts and destination of shapes
ctx: CtxRef,
ctx: Context,
/// Where we paint
layer_id: LayerId,
paint_list: std::sync::Arc<Mutex<PaintList>>,
/// Everything painted in this `Painter` will be clipped against this.
/// This means nothing outside of this rectangle will be visible on screen.
clip_rect: Rect,
@ -32,33 +30,30 @@ pub struct Painter {
}
impl Painter {
pub fn new(ctx: CtxRef, layer_id: LayerId, clip_rect: Rect) -> Self {
let paint_list = ctx.graphics().list(layer_id).clone();
/// Create a painter to a specific layer within a certain clip rectangle.
pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
Self {
ctx,
layer_id,
paint_list,
clip_rect,
fade_to_color: None,
}
}
/// Redirect where you are painting.
#[must_use]
pub fn with_layer_id(self, layer_id: LayerId) -> Self {
let paint_list = self.ctx.graphics().list(layer_id).clone();
Self {
ctx: self.ctx,
paint_list,
layer_id,
clip_rect: self.clip_rect,
fade_to_color: None,
}
}
/// redirect
/// Redirect where you are painting.
pub fn set_layer_id(&mut self, layer_id: LayerId) {
self.layer_id = layer_id;
self.paint_list = self.ctx.graphics().list(self.layer_id).clone();
}
/// If set, colors will be modified to look like this
@ -83,7 +78,6 @@ impl Painter {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
paint_list: self.paint_list.clone(),
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
}
@ -92,15 +86,15 @@ impl Painter {
/// ## Accessors etc
impl Painter {
/// Get a reference to the parent [`CtxRef`].
/// Get a reference to the parent [`Context`].
#[inline(always)]
pub fn ctx(&self) -> &CtxRef {
pub fn ctx(&self) -> &Context {
&self.ctx
}
/// Available fonts.
#[inline(always)]
pub fn fonts(&self) -> &Fonts {
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx.fonts()
}
@ -145,6 +139,10 @@ impl Painter {
/// ## Low level
impl Painter {
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
}
fn transform_shape(&self, shape: &mut Shape) {
if let Some(fade_to_color) = self.fade_to_color {
tint_shape_towards(shape, fade_to_color);
@ -156,11 +154,11 @@ impl Painter {
/// NOTE: all coordinates are screen coordinates!
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
self.paint_list.lock().add(self.clip_rect, Shape::Noop)
self.paint_list().add(self.clip_rect, Shape::Noop)
} else {
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list.lock().add(self.clip_rect, shape)
self.paint_list().add(self.clip_rect, shape)
}
}
@ -178,7 +176,7 @@ impl Painter {
}
}
self.paint_list.lock().extend(self.clip_rect, shapes);
self.paint_list().extend(self.clip_rect, shapes);
}
}
@ -189,7 +187,7 @@ impl Painter {
}
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list.lock().set(idx, self.clip_rect, shape);
self.paint_list().set(idx, self.clip_rect, shape);
}
}
@ -198,12 +196,11 @@ impl Painter {
#[allow(clippy::needless_pass_by_value)]
pub fn debug_rect(&mut self, rect: Rect, color: Color32, text: impl ToString) {
self.rect_stroke(rect, 0.0, (1.0, color));
let text_style = TextStyle::Monospace;
self.text(
rect.min,
Align2::LEFT_TOP,
text.to_string(),
text_style,
FontId::monospace(14.0),
color,
);
}
@ -221,7 +218,7 @@ impl Painter {
color: Color32,
text: impl ToString,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color);
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(14.0), color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
let frame_rect = rect.expand(2.0);
self.add(Shape::rect_filled(
@ -236,6 +233,8 @@ impl Painter {
/// # Paint different primitives
impl Painter {
/// Paints the line from the first point to the second using the `stroke`
/// for outlining shape.
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
self.add(Shape::LineSegment {
points,
@ -279,31 +278,41 @@ impl Painter {
pub fn rect(
&self,
rect: Rect,
corner_radius: f32,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
corner_radius,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
});
}
pub fn rect_filled(&self, rect: Rect, corner_radius: f32, fill_color: impl Into<Color32>) {
pub fn rect_filled(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) {
self.add(RectShape {
rect,
corner_radius,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
});
}
pub fn rect_stroke(&self, rect: Rect, corner_radius: f32, stroke: impl Into<Stroke>) {
pub fn rect_stroke(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
corner_radius,
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
});
@ -326,7 +335,7 @@ impl Painter {
impl Painter {
/// Lay out and paint some text.
///
/// To center the text at the given position, use `anchor: (Center, Center)`.
/// To center the text at the given position, use `Align2::CENTER_CENTER`.
///
/// To find out the size of text before painting it, use
/// [`Self::layout`] or [`Self::layout_no_wrap`].
@ -338,10 +347,10 @@ impl Painter {
pos: Pos2,
anchor: Align2,
text: impl ToString,
text_style: TextStyle,
font_id: FontId,
text_color: Color32,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), text_style, text_color);
let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
self.galley(rect.min, galley);
rect
@ -354,11 +363,11 @@ impl Painter {
pub fn layout(
&self,
text: String,
text_style: TextStyle,
font_id: FontId,
color: crate::Color32,
wrap_width: f32,
) -> std::sync::Arc<Galley> {
self.fonts().layout(text, text_style, color, wrap_width)
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, wrap_width)
}
/// Will line break at `\n`.
@ -368,10 +377,10 @@ impl Painter {
pub fn layout_no_wrap(
&self,
text: String,
text_style: TextStyle,
font_id: FontId,
color: crate::Color32,
) -> std::sync::Arc<Galley> {
self.fonts().layout(text, text_style, color, f32::INFINITY)
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, f32::INFINITY)
}
/// Paint text that has already been layed out in a [`Galley`].
@ -380,7 +389,7 @@ impl Painter {
///
/// If you want to change the color of the text, use [`Self::galley_with_color`].
#[inline(always)]
pub fn galley(&self, pos: Pos2, galley: std::sync::Arc<Galley>) {
pub fn galley(&self, pos: Pos2, galley: Arc<Galley>) {
if !galley.is_empty() {
self.add(Shape::galley(pos, galley));
}
@ -392,12 +401,7 @@ impl Painter {
///
/// The text color in the [`Galley`] will be replaced with the given color.
#[inline(always)]
pub fn galley_with_color(
&self,
pos: Pos2,
galley: std::sync::Arc<Galley>,
text_color: Color32,
) {
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(TextShape {
override_text_color: Some(text_color),

View file

@ -1,6 +1,7 @@
use crate::{
emath::{lerp, Align, Pos2, Rect, Vec2},
CtxRef, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, NUM_POINTER_BUTTONS,
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
NUM_POINTER_BUTTONS,
};
// ----------------------------------------------------------------------------
@ -16,7 +17,7 @@ use crate::{
pub struct Response {
// CONTEXT:
/// Used for optionally showing a tooltip and checking for more interactions.
pub ctx: CtxRef,
pub ctx: Context,
// IN:
/// Which layer the widget is part of.
@ -382,6 +383,14 @@ impl Response {
true
}
/// Like `on_hover_text`, but show the text next to cursor.
#[doc(alias = "tooltip")]
pub fn on_hover_text_at_pointer(self, text: impl Into<WidgetText>) -> Self {
self.on_hover_ui_at_pointer(|ui| {
ui.add(crate::widgets::Label::new(text));
})
}
/// Show this text if the widget was hovered (i.e. a tooltip).
///
/// The text will not be visible if the widget is not enabled.
@ -479,7 +488,7 @@ impl Response {
/// Response to secondary clicks (right-clicks) by showing the given menu.
///
/// ``` rust
/// ```
/// # egui::__run_test_ui(|ui| {
/// let response = ui.label("Right-click me!");
/// response.context_menu(|ui| {
@ -492,9 +501,7 @@ impl Response {
///
/// See also: [`Ui::menu_button`] and [`Ui::close_menu`].
pub fn context_menu(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
self.ctx
.context_menu_system()
.context_menu(&self, add_contents);
menu::context_menu(&self, add_contents);
self
}
}
@ -587,7 +594,9 @@ impl std::ops::BitOrAssign for Response {
/// ```
#[derive(Debug)]
pub struct InnerResponse<R> {
/// What the user closure returned.
pub inner: R,
/// The response of the area.
pub response: Response,
}

View file

@ -2,8 +2,123 @@
#![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, Response, RichText, WidgetText};
use epaint::{Shadow, Stroke, TextStyle};
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
use epaint::{mutex::Arc, Rounding, Shadow, Stroke};
use std::collections::BTreeMap;
// ----------------------------------------------------------------------------
/// Alias for a [`FontId`] (font of a certain size).
///
/// The font is found via look-up in [`Style::text_styles`].
/// You can use [`TextStyle::resolve`] to do this lookup.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextStyle {
/// Used when small text is needed.
Small,
/// Normal labels. Easily readable, doesn't take up too much space.
Body,
/// Same size as [`Self::Body]`, but used when monospace is important (for aligning number, code snippets, etc).
Monospace,
/// Buttons. Maybe slightly bigger than [`Self::Body]`.
/// Signifies that he item is interactive.
Button,
/// Heading. Probably larger than [`Self::Body]`.
Heading,
/// A user-chosen style, found in [`Style::text_styles`].
/// ```
/// egui::TextStyle::Name("footing".into());
/// ````
Name(Arc<str>),
}
impl std::fmt::Display for TextStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Small => "Small".fmt(f),
Self::Body => "Body".fmt(f),
Self::Monospace => "Monospace".fmt(f),
Self::Button => "Button".fmt(f),
Self::Heading => "Heading".fmt(f),
Self::Name(name) => (*name).fmt(f),
}
}
}
impl TextStyle {
/// Look up this [`TextStyle`] in [`Style::text_styles`].
pub fn resolve(&self, style: &Style) -> FontId {
style.text_styles.get(self).cloned().unwrap_or_else(|| {
panic!(
"Failed to find {:?} in Style::text_styles. Available styles:\n{:#?}",
self,
style.text_styles()
)
})
}
}
// ----------------------------------------------------------------------------
/// A way to select [`FontId`], either by picking one directly or by using a [`TextStyle`].
pub enum FontSelection {
/// Default text style - will use [`TextStyle::Body`], unless
/// [`Style::override_font_id`] or [`Style::override_text_style`] is set.
Default,
/// Directly select size and font family
FontId(FontId),
/// Use a [`TextStyle`] to look up the [`FontId`] in [`Style::text_styles`].
Style(TextStyle),
}
impl Default for FontSelection {
#[inline]
fn default() -> Self {
Self::Default
}
}
impl FontSelection {
pub fn resolve(self, style: &Style) -> FontId {
match self {
Self::Default => {
if let Some(override_font_id) = &style.override_font_id {
override_font_id.clone()
} else if let Some(text_style) = &style.override_text_style {
text_style.resolve(style)
} else {
TextStyle::Body.resolve(style)
}
}
Self::FontId(font_id) => font_id,
Self::Style(text_style) => text_style.resolve(style),
}
}
}
impl From<FontId> for FontSelection {
#[inline(always)]
fn from(font_id: FontId) -> Self {
Self::FontId(font_id)
}
}
impl From<TextStyle> for FontSelection {
#[inline(always)]
fn from(text_style: TextStyle) -> Self {
Self::Style(text_style)
}
}
// ----------------------------------------------------------------------------
/// Specifies the look and feel of egui.
///
@ -15,15 +130,23 @@ use epaint::{Shadow, Stroke, TextStyle};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Style {
/// Default `TextStyle` for normal text (i.e. for `Label` and `TextEdit`).
pub body_text_style: TextStyle,
/// If set this will change the default [`TextStyle`] for all widgets.
///
/// On most widgets you can also set an explicit text style,
/// which will take precedence over this.
pub override_text_style: Option<TextStyle>,
/// If set this will change the font family and size for all widgets.
///
/// On most widgets you can also set an explicit text style,
/// which will take precedence over this.
pub override_font_id: Option<FontId>,
/// The [`FontFamily`] and size you want to use for a specific [`TextStyle`].
///
/// The most convenient way to look something up in this is to use [`TextStyle::resolve`].
pub text_styles: BTreeMap<TextStyle, FontId>,
/// If set, labels buttons wtc will use this to determine whether or not
/// to wrap the text at the right edge of the `Ui` they are in.
/// By default this is `None`.
@ -77,6 +200,11 @@ impl Style {
pub fn noninteractive(&self) -> &WidgetVisuals {
&self.visuals.widgets.noninteractive
}
/// All known text styles.
pub fn text_styles(&self) -> Vec<TextStyle> {
self.text_styles.keys().cloned().collect()
}
}
/// Controls the sizes and distances between widgets.
@ -187,10 +315,10 @@ pub struct Visuals {
///
/// If `text_color` is `None` (default), then the text color will be the same as the
/// foreground stroke color (`WidgetVisuals::fg_stroke`)
/// and will depend on wether or not the widget is being interacted with.
/// and will depend on whether or not the widget is being interacted with.
///
/// In the future we may instead modulate
/// the `text_color` based on wether or not it is interacted with
/// the `text_color` based on whether or not it is interacted with
/// so that `visuals.text_color` is always used,
/// but its alpha may be different based on whether or not
/// it is disabled, non-interactive, hovered etc.
@ -216,7 +344,7 @@ pub struct Visuals {
/// Background color behind code-styled monospaced labels.
pub code_bg_color: Color32,
pub window_corner_radius: f32,
pub window_rounding: Rounding,
pub window_shadow: Shadow,
pub popup_shadow: Shadow,
@ -257,6 +385,7 @@ impl Visuals {
self.widgets.active.text_color()
}
/// Window background color.
#[inline(always)]
pub fn window_fill(&self) -> Color32 {
self.widgets.noninteractive.bg_fill
@ -324,7 +453,7 @@ pub struct WidgetVisuals {
pub bg_stroke: Stroke,
/// Button frames etc.
pub corner_radius: f32,
pub rounding: Rounding,
/// Stroke and text color of the interactive part of a component (button text, slider grab, check-mark, …).
pub fg_stroke: Stroke,
@ -355,11 +484,35 @@ pub struct DebugOptions {
// ----------------------------------------------------------------------------
/// The default text styles of the default egui theme.
pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
let mut text_styles = BTreeMap::new();
text_styles.insert(
TextStyle::Small,
FontId::new(10.0, FontFamily::Proportional),
);
text_styles.insert(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional));
text_styles.insert(
TextStyle::Button,
FontId::new(14.0, FontFamily::Proportional),
);
text_styles.insert(
TextStyle::Heading,
FontId::new(20.0, FontFamily::Proportional),
);
text_styles.insert(
TextStyle::Monospace,
FontId::new(14.0, FontFamily::Monospace),
);
text_styles
}
impl Default for Style {
fn default() -> Self {
Self {
body_text_style: TextStyle::Body,
override_font_id: None,
override_text_style: None,
text_styles: default_text_styles(),
wrap: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
@ -413,7 +566,7 @@ impl Visuals {
faint_bg_color: Color32::from_gray(24),
extreme_bg_color: Color32::from_gray(10),
code_bg_color: Color32::from_gray(64),
window_corner_radius: 6.0,
window_rounding: Rounding::same(6.0),
window_shadow: Shadow::big_dark(),
popup_shadow: Shadow::small_dark(),
resize_corner_size: 12.0,
@ -476,35 +629,35 @@ impl Widgets {
bg_fill: Color32::from_gray(27), // window background
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(60), // button background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
corner_radius: 3.0,
rounding: Rounding::same(3.0),
expansion: 1.0,
},
active: WidgetVisuals {
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 1.0,
},
open: WidgetVisuals {
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
}
@ -516,35 +669,35 @@ impl Widgets {
bg_fill: Color32::from_gray(235), // window 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
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(215), // button background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // button text
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
bg_fill: Color32::from_gray(210),
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),
corner_radius: 3.0,
rounding: Rounding::same(3.0),
expansion: 1.0,
},
active: WidgetVisuals {
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 1.0,
},
open: WidgetVisuals {
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
corner_radius: 2.0,
rounding: Rounding::same(2.0),
expansion: 0.0,
},
}
@ -564,8 +717,9 @@ use crate::{widgets::*, Ui};
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
body_text_style,
override_font_id,
override_text_style,
text_styles,
wrap: _,
spacing,
interaction,
@ -578,11 +732,14 @@ impl Style {
visuals.light_dark_radio_buttons(ui);
crate::Grid::new("_options").show(ui, |ui| {
ui.label("Default body text style:");
ui.label("Override font id:");
ui.horizontal(|ui| {
for &style in &[TextStyle::Body, TextStyle::Monospace] {
let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
ui.radio_value(body_text_style, style, text);
ui.radio_value(override_font_id, None, "None");
if ui.radio(override_font_id.is_some(), "override").clicked() {
*override_font_id = Some(FontId::default());
}
if let Some(override_font_id) = override_font_id {
crate::introspection::font_id_ui(ui, override_font_id);
}
});
ui.end_row();
@ -591,12 +748,14 @@ impl Style {
crate::ComboBox::from_id_source("Override text style")
.selected_text(match override_text_style {
None => "None".to_owned(),
Some(override_text_style) => format!("{:?}", override_text_style),
Some(override_text_style) => override_text_style.to_string(),
})
.show_ui(ui, |ui| {
ui.selectable_value(override_text_style, None, "None");
for style in TextStyle::all() {
let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
let all_text_styles = ui.style().text_styles();
for style in all_text_styles {
let text =
crate::RichText::new(style.to_string()).text_style(style.clone());
ui.selectable_value(override_text_style, Some(style), text);
}
});
@ -611,6 +770,7 @@ impl Style {
ui.end_row();
});
ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
@ -625,6 +785,20 @@ impl Style {
}
}
fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap<TextStyle, FontId>) -> Response {
ui.vertical(|ui| {
crate::Grid::new("text_styles").show(ui, |ui| {
for (text_style, font_id) in text_styles.iter_mut() {
ui.label(RichText::new(text_style.to_string()).font(font_id.clone()));
crate::introspection::font_id_ui(ui, font_id);
ui.end_row();
}
});
crate::reset_button_with(ui, text_styles, default_text_styles());
})
.response
}
impl Spacing {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
@ -759,7 +933,7 @@ impl Selection {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { bg_fill, stroke } = self;
ui.label("Selectable labels");
ui_color(ui, bg_fill, "bg_fill");
ui_color(ui, bg_fill, "background fill");
stroke_ui(ui, stroke, "stroke");
}
}
@ -769,14 +943,16 @@ impl WidgetVisuals {
let Self {
bg_fill,
bg_stroke,
corner_radius,
rounding,
fg_stroke,
expansion,
} = self;
ui_color(ui, bg_fill, "bg_fill");
stroke_ui(ui, bg_stroke, "bg_stroke");
ui.add(Slider::new(corner_radius, 0.0..=10.0).text("corner_radius"));
stroke_ui(ui, fg_stroke, "fg_stroke (text)");
ui_color(ui, bg_fill, "background fill");
stroke_ui(ui, bg_stroke, "background stroke");
rounding_ui(ui, rounding);
stroke_ui(ui, fg_stroke, "foreground stroke (text)");
ui.add(Slider::new(expansion, -5.0..=5.0).text("expansion"))
.on_hover_text("make shapes this much larger");
}
@ -825,7 +1001,7 @@ impl Visuals {
faint_bg_color,
extreme_bg_color,
code_bg_color,
window_corner_radius,
window_rounding,
window_shadow,
popup_shadow,
resize_corner_size,
@ -850,7 +1026,9 @@ impl Visuals {
// Common shortcuts
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill");
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
ui.add(Slider::new(window_corner_radius, 0.0..=20.0).text("Rounding"));
rounding_ui(ui, window_rounding);
shadow_ui(ui, window_shadow, "Shadow");
shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)");
});
@ -940,3 +1118,29 @@ fn ui_color(ui: &mut Ui, srgba: &mut Color32, label: impl Into<WidgetText>) -> R
})
.response
}
fn rounding_ui(ui: &mut Ui, rounding: &mut Rounding) {
const MAX: f32 = 20.0;
let mut same = rounding.is_same();
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label("Rounding: ");
ui.radio_value(&mut same, true, "Same");
ui.radio_value(&mut same, false, "Separate");
});
if same {
let mut cr = rounding.nw;
ui.add(Slider::new(&mut cr, 0.0..=MAX));
*rounding = Rounding::same(cr);
} else {
ui.add(Slider::new(&mut rounding.nw, 0.0..=MAX).text("North-West"));
ui.add(Slider::new(&mut rounding.ne, 0.0..=MAX).text("North-East"));
ui.add(Slider::new(&mut rounding.sw, 0.0..=MAX).text("South-West"));
ui.add(Slider::new(&mut rounding.se, 0.0..=MAX).text("South-East"));
if rounding.is_same() {
rounding.se *= 1.00001;
}
}
});
}

View file

@ -1,12 +1,11 @@
// #![warn(missing_docs)]
use epaint::mutex::RwLock;
use epaint::mutex::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::hash::Hash;
use std::sync::Arc;
use crate::{
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, mutex::MutexGuard,
placer::Placer, widgets::*, *,
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
widgets::*, *,
};
// ----------------------------------------------------------------------------
@ -49,7 +48,7 @@ pub struct Ui {
/// The `Style` (visuals, spacing, etc) of this ui.
/// Commonly many `Ui`:s share the same `Style`.
/// The `Ui` implements copy-on-write for this.
style: std::sync::Arc<Style>,
style: Arc<Style>,
/// Handles the `Ui` size and the placement of new widgets.
placer: Placer,
@ -70,7 +69,7 @@ impl Ui {
///
/// Normally you would not use this directly, but instead use
/// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`].
pub fn new(ctx: CtxRef, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self {
pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self {
let style = ctx.style();
Ui {
id,
@ -122,7 +121,7 @@ impl Ui {
///
/// Note that this may be a different [`Style`] than that of [`Context::style`].
#[inline]
pub fn style(&self) -> &std::sync::Arc<Style> {
pub fn style(&self) -> &Arc<Style> {
&self.style
}
@ -134,17 +133,17 @@ impl Ui {
/// Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.style_mut().body_text_style = egui::TextStyle::Heading;
/// ui.style_mut().override_text_style = Some(egui::TextStyle::Heading);
/// # });
/// ```
pub fn style_mut(&mut self) -> &mut Style {
std::sync::Arc::make_mut(&mut self.style) // clone-on-write
Arc::make_mut(&mut self.style) // clone-on-write
}
/// Changes apply to this `Ui` and its subsequent children.
///
/// To set the visuals of all `Ui`:s, use [`Context::set_visuals`].
pub fn set_style(&mut self, style: impl Into<std::sync::Arc<Style>>) {
pub fn set_style(&mut self, style: impl Into<Arc<Style>>) {
self.style = style.into();
}
@ -195,9 +194,9 @@ impl Ui {
&mut self.style_mut().visuals
}
/// Get a reference to the parent [`CtxRef`].
/// Get a reference to the parent [`Context`].
#[inline]
pub fn ctx(&self) -> &CtxRef {
pub fn ctx(&self) -> &Context {
self.painter.ctx()
}
@ -313,34 +312,64 @@ impl Ui {
self.painter().layer_id()
}
/// The `Input` of the `Context` associated with the `Ui`.
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
/// // This is fine!
/// }
///
/// let pos = ui.input().pointer.hover_pos();
/// if let Some(pos) = pos {
/// // This is also fine!
/// }
///
/// if let Some(pos) = ui.input().pointer.hover_pos() {
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
/// }
/// # });
/// ```
#[inline]
pub fn input(&self) -> &InputState {
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
self.ctx().input()
}
/// The `Memory` of the `Context` associated with the `Ui`.
/// The [`Memory`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().memory()`.
#[inline]
pub fn memory(&self) -> MutexGuard<'_, Memory> {
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
self.ctx().memory()
}
/// The `Output` of the `Context` associated with the `Ui`.
/// Stores superficial widget state.
#[inline]
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
self.ctx().data()
}
/// The [`Output`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().output()`.
#[inline]
pub fn output(&self) -> MutexGuard<'_, Output> {
pub fn output(&self) -> RwLockWriteGuard<'_, Output> {
self.ctx().output()
}
/// The `Fonts` of the `Context` associated with the `Ui`.
/// The [`Fonts`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().fonts()`.
#[inline]
pub fn fonts(&self) -> &Fonts {
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx().fonts()
}
/// The height of text of this text style
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts().row_height(&style.resolve(self.style()))
}
/// Screen-space rectangle for clipping what we paint in this ui.
/// This is used, for instance, to avoid painting outside a window that is smaller than its contents.
#[inline]
@ -1055,7 +1084,7 @@ impl Ui {
/// Add extra space before the next widget.
///
/// The direction is dependent on the layout.
/// This will be in addition to the [`Spacing::item_spacing`}.
/// This will be in addition to the [`crate::style::Spacing::item_spacing`].
///
/// [`Self::min_rect`] will expand to contain the space.
#[inline]
@ -1068,6 +1097,16 @@ impl Ui {
/// Shortcut for `add(Label::new(text))`
///
/// See also [`Label`].
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// use egui::{RichText, FontId, Color32};
/// ui.label("Normal text");
/// ui.label(RichText::new("Large text").font(FontId::proportional(40.0)));
/// ui.label(RichText::new("Red text").color(Color32::RED));
/// # });
/// ```
#[inline]
pub fn label(&mut self, text: impl Into<WidgetText>) -> Response {
Label::new(text).ui(self)
@ -1238,12 +1277,12 @@ impl Ui {
pub fn radio_value<Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
alternative: Value,
text: impl Into<WidgetText>,
) -> Response {
let mut response = self.radio(*current_value == selected_value, text);
let mut response = self.radio(*current_value == alternative, text);
if response.clicked() {
*current_value = selected_value;
*current_value = alternative;
response.mark_changed();
}
response
@ -1323,9 +1362,30 @@ impl Ui {
/// Show an image here with the given size.
///
/// See also [`Image`].
/// In order to display an image you must first acquire a [`TextureHandle`]
/// using [`Context::load_texture`].
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::Image`] and [`crate::ImageButton`].
#[inline]
pub fn image(&mut self, texture_id: TextureId, size: impl Into<Vec2>) -> Response {
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self)
}
}

View file

@ -109,11 +109,11 @@ where
/// `(time, value)` pairs
/// Time difference between values can be zero, but never negative.
// TODO: impl IntoIter
pub fn iter(&'_ self) -> impl Iterator<Item = (f64, T)> + '_ {
pub fn iter(&'_ self) -> impl ExactSizeIterator<Item = (f64, T)> + '_ {
self.values.iter().map(|(time, value)| (*time, *value))
}
pub fn values(&'_ self) -> impl Iterator<Item = T> + '_ {
pub fn values(&'_ self) -> impl ExactSizeIterator<Item = T> + '_ {
self.values.iter().map(|(_time, value)| *value)
}

View file

@ -3,8 +3,8 @@
// For non-serializable types, these simply return `None`.
// This will also allow users to pick their own serialization format per type.
use epaint::mutex::Arc;
use std::any::Any;
use std::sync::Arc;
// -----------------------------------------------------------------------------------------------
@ -275,11 +275,12 @@ impl Element {
fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
match ron::from_str::<T>(ron) {
Ok(value) => Some(value),
Err(err) => {
eprintln!(
Err(_err) => {
#[cfg(feature = "tracing")]
tracing::warn!(
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
std::any::type_name::<T>(),
err,
_err,
ron
);
None
@ -294,7 +295,10 @@ use crate::Id;
// TODO: make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value.
///
/// so it maps `(Id, TypeId)` to any value you want.
/// In other words, it maps `(Id, TypeId)` to any value you want.
///
/// Values are cloned when read, so keep them small and light.
/// If you want to store something bigger, wrap them in `Arc<Mutex<…>>`.
///
/// Values can either be "persisted" (serializable) or "temporary" (cleared when egui is shut down).
///
@ -311,19 +315,19 @@ use crate::Id;
/// map.insert_temp(a, 42);
///
/// // `b` associated with an f64 and a `&'static str`
/// map.insert_persisted(b, 6.28);
/// map.insert_persisted(b, 13.37);
/// map.insert_temp(b, "Hello World".to_string());
///
/// // we can retrieve all four values:
/// assert_eq!(map.get_temp::<f64>(a), Some(3.14));
/// assert_eq!(map.get_temp::<i32>(a), Some(42));
/// assert_eq!(map.get_temp::<f64>(b), Some(6.28));
/// assert_eq!(map.get_temp::<f64>(b), Some(13.37));
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
///
/// // we can retrieve them like so also:
/// assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
/// assert_eq!(map.get_persisted::<i32>(a), Some(42));
/// assert_eq!(map.get_persisted::<f64>(b), Some(6.28));
/// assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
/// ```
#[derive(Clone, Debug, Default)]
@ -346,6 +350,8 @@ impl IdTypeMap {
}
/// Read a value without trying to deserialize a persisted value.
///
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_temp<T: 'static + Clone>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
@ -356,6 +362,8 @@ impl IdTypeMap {
}
/// Read a value, optionally deserializing it if available.
///
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
@ -544,11 +552,11 @@ fn test_two_id_two_type() {
let b = Id::new("b");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(a, 6.28);
map.insert_persisted(a, 13.37);
map.insert_temp(b, 42);
assert_eq!(map.get_persisted::<f64>(a), Some(6.28));
assert_eq!(map.get_persisted::<f64>(a), Some(13.37));
assert_eq!(map.get_persisted::<i32>(b), Some(42));
assert_eq!(map.get_temp::<f64>(a), Some(6.28));
assert_eq!(map.get_temp::<f64>(a), Some(13.37));
assert_eq!(map.get_temp::<i32>(b), Some(42));
}
@ -565,19 +573,19 @@ fn test_two_id_x_two_types() {
map.insert_temp(a, 42);
// `b` associated with an f64 and a `&'static str`
map.insert_persisted(b, 6.28);
map.insert_persisted(b, 13.37);
map.insert_temp(b, "Hello World".to_string());
// we can retrieve all four values:
assert_eq!(map.get_temp::<f64>(a), Some(3.14));
assert_eq!(map.get_temp::<i32>(a), Some(42));
assert_eq!(map.get_temp::<f64>(b), Some(6.28));
assert_eq!(map.get_temp::<f64>(b), Some(13.37));
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
// we can retrieve them like so also:
assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
assert_eq!(map.get_persisted::<i32>(a), Some(42));
assert_eq!(map.get_persisted::<f64>(b), Some(6.28));
assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
}
@ -586,11 +594,11 @@ fn test_one_id_two_types() {
let id = Id::new("a");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(id, 6.28);
map.insert_persisted(id, 13.37);
map.insert_temp(id, 42);
assert_eq!(map.get_temp::<f64>(id), Some(6.28));
assert_eq!(map.get_persisted::<f64>(id), Some(6.28));
assert_eq!(map.get_temp::<f64>(id), Some(13.37));
assert_eq!(map.get_persisted::<f64>(id), Some(13.37));
assert_eq!(map.get_temp::<i32>(id), Some(42));
// ------------
@ -601,8 +609,8 @@ fn test_one_id_two_types() {
assert_eq!(map.get_temp::<i32>(id), None);
// Other type is still there, even though it is the same if:
assert_eq!(map.get_temp::<f64>(id), Some(6.28));
assert_eq!(map.get_persisted::<f64>(id), Some(6.28));
assert_eq!(map.get_temp::<f64>(id), Some(13.37));
assert_eq!(map.get_persisted::<f64>(id), Some(13.37));
// But we can still remove the last:
map.remove::<f64>(id);

View file

@ -1,17 +1,30 @@
use std::sync::Arc;
use epaint::mutex::Arc;
use crate::{
style::WidgetVisuals, text::LayoutJob, Align, Color32, Galley, Pos2, Style, TextStyle, Ui,
Visuals,
style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2,
Style, TextStyle, Ui, Visuals,
};
/// Text and optional style choices for it.
///
/// The style choices (font, color) are applied to the entire text.
/// For more detailed control, use [`crate::text::LayoutJob`] instead.
///
/// A `RichText` can be used in most widgets and helper functions, e.g. [`Ui::label`] and [`Ui::button`].
///
/// ### Example
/// ```
/// use egui::{RichText, Color32};
///
/// RichText::new("Plain");
/// RichText::new("colored").color(Color32::RED);
/// RichText::new("Large and underlined").size(20.0).underline();
/// ```
#[derive(Clone, Default, PartialEq)]
pub struct RichText {
text: String,
size: Option<f32>,
family: Option<FontFamily>,
text_style: Option<TextStyle>,
background_color: Color32,
text_color: Option<Color32>,
@ -64,6 +77,35 @@ impl RichText {
&self.text
}
/// Select the font size (in points).
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Select the font family.
///
/// This overrides the value from [`Self::text_style`].
///
/// Only the families available in [`crate::FontDefinitions::families`] may be used.
#[inline]
pub fn family(mut self, family: FontFamily) -> Self {
self.family = Some(family);
self
}
/// Select the font and size.
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn font(mut self, font_id: crate::FontId) -> Self {
let crate::FontId { size, family } = font_id;
self.size = Some(size);
self.family = Some(family);
self
}
/// Override the [`TextStyle`].
#[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self {
@ -170,24 +212,33 @@ impl RichText {
}
/// Read the font height of the selected text style.
pub fn font_height(&self, fonts: &epaint::text::Fonts, style: &crate::Style) -> f32 {
let text_style = self
.text_style
.or(style.override_text_style)
.unwrap_or(style.body_text_style);
fonts.row_height(text_style)
pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = self.size {
font_id.size = size;
}
if let Some(family) = &self.family {
font_id.family = family.clone();
}
fonts.row_height(&font_id)
}
fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
let text_color = self.get_text_color(&style.visuals);
let Self {
text,
size,
family,
text_style,
background_color,
text_color: _, // already used by `get_text_color`
@ -204,9 +255,21 @@ impl RichText {
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
let text_style = text_style
.or(style.override_text_style)
.unwrap_or(default_text_style);
let font_id = {
let mut font_id = text_style
.or_else(|| style.override_text_style.clone())
.map_or_else(
|| fallback_font.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = size {
font_id.size = size;
}
if let Some(family) = family {
font_id.family = family;
}
font_id
};
let mut background_color = background_color;
if code {
@ -230,7 +293,7 @@ impl RichText {
};
let text_format = crate::text::TextFormat {
style: text_style,
font_id,
color: text_color,
background: background_color,
italics,
@ -267,8 +330,10 @@ impl RichText {
/// but it can be a [`RichText`] (text with color, style, etc),
/// a [`LayoutJob`] (for when you want full control of how the text looks)
/// or text that has already been layed out in a [`Galley`].
#[derive(Clone)]
pub enum WidgetText {
RichText(RichText),
/// Use this [`LayoutJob`] when laying out the text.
///
/// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected.
@ -279,6 +344,7 @@ pub enum WidgetText {
/// If you want all parts of the `LayoutJob` respected, then convert it to a
/// [`Galley`] and use [`Self::Galley`] instead.
LayoutJob(LayoutJob),
/// Use exactly this galley when painting the text.
Galley(Arc<Galley>),
}
@ -437,7 +503,7 @@ impl WidgetText {
}
}
pub(crate) fn font_height(&self, fonts: &epaint::text::Fonts, style: &crate::Style) -> f32 {
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
match self {
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
@ -454,11 +520,11 @@ impl WidgetText {
pub fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
match self {
Self::RichText(text) => text.into_text_job(style, default_text_style, default_valign),
Self::RichText(text) => text.into_text_job(style, fallback_font, default_valign),
Self::LayoutJob(job) => WidgetTextJob {
job,
job_has_color: true,
@ -481,7 +547,7 @@ impl WidgetText {
ui: &Ui,
wrap: Option<bool>,
available_width: f32,
default_text_style: TextStyle,
fallback_font: impl Into<FontSelection>,
) -> WidgetTextGalley {
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
let wrap_width = if wrap { available_width } else { f32::INFINITY };
@ -489,7 +555,7 @@ impl WidgetText {
match self {
Self::RichText(text) => {
let valign = ui.layout().vertical_align();
let mut text_job = text.into_text_job(ui.style(), default_text_style, valign);
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(text_job.job),

View file

@ -180,7 +180,7 @@ impl Widget for Button {
let stroke = stroke.unwrap_or(visuals.bg_stroke);
ui.painter().rect(
rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.rounding,
fill,
stroke,
);
@ -265,7 +265,7 @@ impl<'a> Widget for Checkbox<'a> {
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape {
rect: big_icon_rect.expand(visuals.expansion),
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
@ -394,7 +394,7 @@ pub struct ImageButton {
}
impl ImageButton {
pub fn new(texture_id: TextureId, size: impl Into<Vec2>) -> Self {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self {
image: widgets::Image::new(texture_id, size),
sense: Sense::click(),
@ -455,9 +455,14 @@ impl Widget for ImageButton {
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
if ui.is_rect_visible(rect) {
let (expansion, corner_radius, fill, stroke) = if selected {
let (expansion, rounding, fill, stroke) = if selected {
let selection = ui.visuals().selection;
(-padding, 0.0, selection.bg_fill, selection.stroke)
(
-padding,
Rounding::none(),
selection.bg_fill,
selection.stroke,
)
} else if frame {
let visuals = ui.style().interact(&response);
let expansion = if response.hovered {
@ -467,7 +472,7 @@ impl Widget for ImageButton {
};
(
expansion,
visuals.corner_radius,
visuals.rounding,
visuals.bg_fill,
visuals.bg_stroke,
)
@ -477,7 +482,7 @@ impl Widget for ImageButton {
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), corner_radius, fill);
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
@ -487,7 +492,7 @@ impl Widget for ImageButton {
// Draw frame outline:
ui.painter()
.rect_stroke(rect.expand2(expansion), corner_radius, stroke);
.rect_stroke(rect.expand2(expansion), rounding, stroke);
}
response

View file

@ -61,7 +61,7 @@ fn show_hsva(ui: &mut Ui, color: Hsva, desired_size: Vec2) -> Response {
} else {
ui.painter().add(RectShape {
rect,
corner_radius: 2.0,
rounding: Rounding::same(2.0),
fill: color.into(),
stroke: Stroke::new(3.0, color.to_opaque()),
});
@ -90,9 +90,15 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
ui.painter().rect_filled(left_half, 0.0, color);
ui.painter().rect_filled(right_half, 0.0, color.to_opaque());
let corner_radius = visuals.corner_radius.at_most(2.0);
let rounding = Rounding {
nw: visuals.rounding.nw.at_most(2.0),
ne: visuals.rounding.ne.at_most(2.0),
sw: visuals.rounding.sw.at_most(2.0),
se: visuals.rounding.se.at_most(2.0),
};
ui.painter()
.rect_stroke(rect, corner_radius, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
}
response
@ -308,8 +314,10 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) {
color_slider_2d(ui, v, s, |v, s| HsvaGamma { s, v, ..opaque }.into());
}
//// Shows a color picker where the user can change the given [`Hsva`] color.
///
/// Returns `true` on change.
fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
pub fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
let mut hsvag = HsvaGamma::from(*hsva);
ui.vertical(|ui| {
color_picker_hsvag_2d(ui, &mut hsvag, alpha);
@ -323,7 +331,7 @@ fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
}
}
/// Shows a color picker where the user can change the given color.
/// Shows a color picker where the user can change the given [`Color32`] color.
///
/// Returns `true` on change.
pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> bool {
@ -335,19 +343,19 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
}
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let pupup_id = ui.auto_id_with("popup");
let open = ui.memory().is_popup_open(pupup_id);
let popup_id = ui.auto_id_with("popup");
let open = ui.memory().is_popup_open(popup_id);
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");
}
if button_response.clicked() {
ui.memory().toggle_popup(pupup_id);
ui.memory().toggle_popup(popup_id);
}
// TODO: make it easier to show a temporary popup that closes when you click outside it
if ui.memory().is_popup_open(pupup_id) {
let area_response = Area::new(pupup_id)
if ui.memory().is_popup_open(popup_id) {
let area_response = Area::new(popup_id)
.order(Order::Foreground)
.default_pos(button_response.rect.max)
.show(ui.ctx(), |ui| {
@ -427,5 +435,5 @@ fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
// To ensure we keep hue slider when `srgba` is gray we store the full `Hsva` in a cache:
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
f(ctx.memory().data.get_temp_mut_or_default(Id::null()))
f(ctx.data().get_temp_mut_or_default(Id::null()))
}

View file

@ -159,8 +159,8 @@ impl<'a> Widget for DragValue<'a> {
max_decimals,
} = self;
let is_slow_speed =
ui.input().modifiers.shift_only() && ui.memory().is_being_dragged(ui.next_auto_id());
let shift = ui.input().modifiers.shift_only();
let is_slow_speed = shift && ui.memory().is_being_dragged(ui.next_auto_id());
let old_value = get(&mut get_set_value);
let value = clamp_to_range(old_value, clamp_range.clone());
@ -195,7 +195,7 @@ impl<'a> Widget for DragValue<'a> {
TextEdit::singleline(&mut value_text)
.id(kb_edit_id)
.desired_width(button_width)
.text_style(TextStyle::Monospace),
.font(TextStyle::Monospace),
);
if let Ok(parsed_value) = value_text.parse() {
let parsed_value = clamp_to_range(parsed_value, clamp_range);

View file

@ -2,17 +2,31 @@ use crate::*;
/// An widget to show an image of a given size.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let my_texture_id = egui::TextureId::User(0);
/// ui.add(egui::Image::new(my_texture_id, [640.0, 480.0]));
/// In order to display an image you must first acquire a [`TextureHandle`]
/// using [`Context::load_texture`].
///
/// // Shorter version:
/// ui.image(my_texture_id, [640.0, 480.0]);
/// # });
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.add(egui::Image::new(texture, texture.size_vec2()));
///
/// // Shorter version:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::ImageButton`].
/// Se also [`crate::Ui::image`] and [`crate::ImageButton`].
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Copy, Debug)]
pub struct Image {
@ -25,9 +39,9 @@ pub struct Image {
}
impl Image {
pub fn new(texture_id: TextureId, size: impl Into<Vec2>) -> Self {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self {
texture_id,
texture_id: texture_id.into(),
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
size: size.into(),
bg_fill: Default::default(),

View file

@ -2,6 +2,8 @@ use crate::{widget_text::WidgetTextGalley, *};
/// Static text.
///
/// Usually it is more convenient to use [`Ui::label`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.label("Equivalent");
@ -49,7 +51,7 @@ impl Label {
/// By calling this you can turn the label into a button of sorts.
/// This will also give the label the hover-effect of a button, but without the frame.
///
/// ``` rust
/// ```
/// # use egui::{Label, Sense};
/// # egui::__run_test_ui(|ui| {
/// if ui.add(Label::new("click me").sense(Sense::click())).clicked() {
@ -82,7 +84,9 @@ impl Label {
}
let valign = ui.layout().vertical_align();
let mut text_job = self.text.into_text_job(ui.style(), TextStyle::Body, valign);
let mut text_job = self
.text
.into_text_job(ui.style(), FontSelection::Default, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width();
@ -106,7 +110,7 @@ impl Label {
if let Some(first_section) = text_job.job.sections.first_mut() {
first_section.leading_space = first_row_indentation;
}
let text_galley = text_job.into_galley(ui.fonts());
let text_galley = text_job.into_galley(&*ui.fonts());
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(
@ -139,7 +143,7 @@ impl Label {
text_job.job.justify = ui.layout().horizontal_justify();
};
let text_galley = text_job.into_galley(ui.fonts());
let text_galley = text_job.into_galley(&*ui.fonts());
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.sense);
let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(),

View file

@ -1,5 +1,5 @@
use crate::emath::NumExt;
use crate::epaint::{Color32, RectShape, Shape, Stroke};
use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
use crate::plot::{BarChart, ScreenTransform, Value};
@ -129,7 +129,7 @@ impl Bar {
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
let rect = Shape::Rect(RectShape {
rect,
corner_radius: 0.0,
rounding: Rounding::none(),
fill,
stroke,
});

View file

@ -1,5 +1,5 @@
use crate::emath::NumExt;
use crate::epaint::{Color32, RectShape, Shape, Stroke};
use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
use crate::plot::{BoxPlot, ScreenTransform, Value};
@ -152,7 +152,7 @@ impl BoxElem {
);
let rect = Shape::Rect(RectShape {
rect,
corner_radius: 0.0,
rounding: Rounding::none(),
fill,
stroke,
});

View file

@ -9,11 +9,11 @@ use crate::*;
use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform};
use rect_elem::*;
use values::*;
use values::{ClosestElem, PlotGeometry};
pub use bar::Bar;
pub use box_elem::{BoxElem, BoxSpread};
pub use values::{LineStyle, MarkerShape, Value, Values};
pub use values::{LineStyle, MarkerShape, Orientation, Value, Values};
mod bar;
mod box_elem;
@ -117,8 +117,8 @@ impl HLine {
}
/// Highlight this line in the plot by scaling up the line.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -227,8 +227,8 @@ impl VLine {
}
/// Highlight this line in the plot by scaling up the line.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -338,8 +338,8 @@ impl Line {
}
/// Highlight this line in the plot by scaling up the line.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -506,8 +506,8 @@ impl Polygon {
/// Highlight this polygon in the plot by scaling up the stroke and reducing the fill
/// transparency.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -614,8 +614,7 @@ impl PlotItem for Polygon {
/// Text inside the plot.
pub struct Text {
pub(super) text: String,
pub(super) style: TextStyle,
pub(super) text: WidgetText,
pub(super) position: Value,
pub(super) name: String,
pub(super) highlight: bool,
@ -624,11 +623,9 @@ pub struct Text {
}
impl Text {
#[allow(clippy::needless_pass_by_value)]
pub fn new(position: Value, text: impl ToString) -> Self {
pub fn new(position: Value, text: impl Into<WidgetText>) -> Self {
Self {
text: text.to_string(),
style: TextStyle::Small,
text: text.into(),
position,
name: Default::default(),
highlight: false,
@ -638,18 +635,12 @@ impl Text {
}
/// Highlight this text in the plot by drawing a rectangle around it.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
/// Text style. Default is `TextStyle::Small`.
pub fn style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
/// Text color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
/// Text color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = color.into();
self
@ -681,14 +672,23 @@ impl PlotItem for Text {
} else {
self.color
};
let galley =
self.text
.clone()
.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Small);
let pos = transform.position_from_value(&self.position);
let galley = ui
.fonts()
.layout_no_wrap(self.text.clone(), self.style, color);
let rect = self
.anchor
.anchor_rect(Rect::from_min_size(pos, galley.size()));
shapes.push(Shape::galley(rect.min, galley));
let mut text_shape = epaint::TextShape::new(rect.min, galley.galley);
if !galley.galley_has_color {
text_shape.override_text_color = Some(color);
}
shapes.push(text_shape.into());
if self.highlight {
shapes.push(Shape::rect_stroke(
rect.expand(2.0),
@ -763,8 +763,8 @@ impl Points {
}
/// Highlight these points in the plot by scaling up their markers.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -979,8 +979,8 @@ impl Arrows {
}
/// Highlight these arrows in the plot.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -1087,12 +1087,16 @@ pub struct PlotImage {
impl PlotImage {
/// Create a new image with position and size in plot coordinates.
pub fn new(texture_id: TextureId, position: Value, size: impl Into<Vec2>) -> Self {
pub fn new(
texture_id: impl Into<TextureId>,
center_position: Value,
size: impl Into<Vec2>,
) -> Self {
Self {
position,
position: center_position,
name: Default::default(),
highlight: false,
texture_id,
texture_id: texture_id.into(),
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
size: size.into(),
bg_fill: Default::default(),
@ -1101,8 +1105,8 @@ impl PlotImage {
}
/// Highlight this image in the plot.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -1291,8 +1295,8 @@ impl BarChart {
}
/// Highlight all plot elements.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -1454,8 +1458,8 @@ impl BoxPlot {
}
/// Highlight all plot elements.
pub fn highlight(mut self) -> Self {
self.highlight = true;
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
@ -1617,13 +1621,15 @@ fn add_rulers_and_text(
text
});
let font_id = TextStyle::Body.resolve(plot.ui.style());
let corner_value = elem.corner_value();
shapes.push(Shape::text(
plot.ui.fonts(),
&*plot.ui.fonts(),
plot.transform.position_from_value(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
TextStyle::Body,
font_id,
plot.ui.visuals().text_color(),
));
}
@ -1673,12 +1679,14 @@ pub(super) fn rulers_at_value(
}
};
let font_id = TextStyle::Body.resolve(plot.ui.style());
shapes.push(Shape::text(
plot.ui.fonts(),
&*plot.ui.fonts(),
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
TextStyle::Body,
font_id,
plot.ui.visuals().text_color(),
));
}

View file

@ -125,6 +125,7 @@ impl ToString for LineStyle {
// ----------------------------------------------------------------------------
/// Determines whether a plot element is vertically or horizontally oriented.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Orientation {
Horizontal,
@ -139,20 +140,12 @@ impl Default for Orientation {
// ----------------------------------------------------------------------------
#[derive(Default)]
pub struct Values {
pub(super) values: Vec<Value>,
generator: Option<ExplicitGenerator>,
}
impl Default for Values {
fn default() -> Self {
Self {
values: Vec::new(),
generator: None,
}
}
}
impl Values {
pub fn from_values(values: Vec<Value>) -> Self {
Self {
@ -297,7 +290,7 @@ pub enum MarkerShape {
impl MarkerShape {
/// Get a vector containing all marker shapes.
pub fn all() -> impl Iterator<Item = MarkerShape> {
pub fn all() -> impl ExactSizeIterator<Item = MarkerShape> {
[
Self::Circle,
Self::Diamond,
@ -342,7 +335,7 @@ struct ExplicitGenerator {
// ----------------------------------------------------------------------------
/// Result of [`PlotItem::find_closest()`] search, identifies an element inside the item for immediate use
/// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use
pub(crate) struct ClosestElem {
/// Position of hovered-over value (or bar/box-plot/...) in PlotItem
pub index: usize,

View file

@ -29,7 +29,7 @@ impl Corner {
}
/// The configuration for a plot legend.
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, PartialEq)]
pub struct Legend {
pub text_style: TextStyle,
pub background_alpha: f32,
@ -82,16 +82,18 @@ impl LegendEntry {
}
}
fn ui(&mut self, ui: &mut Ui, text: String) -> Response {
fn ui(&mut self, ui: &mut Ui, text: String, text_style: &TextStyle) -> Response {
let Self {
color,
checked,
hovered,
} = self;
let galley =
ui.fonts()
.layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY);
let font_id = text_style.resolve(ui.style());
let galley = ui
.fonts()
.layout_delayed_color(text, font_id, f32::INFINITY);
let icon_size = galley.size().y;
let icon_spacing = icon_size / 5.0;
@ -236,10 +238,9 @@ impl Widget for &mut LegendWidget {
let mut legend_ui = ui.child_ui(legend_rect, layout);
legend_ui
.scope(|ui| {
ui.style_mut().body_text_style = config.text_style;
let background_frame = Frame {
margin: vec2(8.0, 4.0),
corner_radius: ui.style().visuals.window_corner_radius,
rounding: ui.style().visuals.window_rounding,
shadow: epaint::Shadow::default(),
fill: ui.style().visuals.extreme_bg_color,
stroke: ui.style().visuals.window_stroke(),
@ -249,7 +250,7 @@ impl Widget for &mut LegendWidget {
.show(ui, |ui| {
entries
.iter_mut()
.map(|(name, entry)| entry.ui(ui, name.clone()))
.map(|(name, entry)| entry.ui(ui, name.clone(), &config.text_style))
.reduce(|r1, r2| r1.union(r2))
.unwrap()
})

View file

@ -1,5 +1,7 @@
//! Simple plotting library.
use std::{cell::RefCell, rc::Rc};
use crate::*;
use epaint::ahash::AHashSet;
use epaint::color::Hsva;
@ -10,7 +12,7 @@ use transform::{PlotBounds, ScreenTransform};
pub use items::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
PlotImage, Points, Polygon, Text, VLine, Value, Values,
Orientation, PlotImage, Points, Polygon, Text, VLine, Value, Values,
};
pub use legend::{Corner, Legend};
@ -21,6 +23,9 @@ mod transform;
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>;
type AxisFormatterFn = dyn Fn(f64) -> String;
type AxisFormatter = Option<Box<AxisFormatterFn>>;
// ----------------------------------------------------------------------------
/// Information about the plot that has to persist between frames.
@ -32,15 +37,74 @@ struct PlotMemory {
hidden_items: AHashSet<String>,
min_auto_bounds: PlotBounds,
last_screen_transform: ScreenTransform,
/// Allows to remember the first click position when performing a boxed zoom
last_click_pos_for_zoom: Option<Pos2>,
}
impl PlotMemory {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
ctx.data().insert_persisted(id, self);
}
}
// ----------------------------------------------------------------------------
/// Defines how multiple plots share the same range for one or both of their axes. Can be added while building
/// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by
/// the user between frames.
#[derive(Clone, PartialEq)]
pub struct LinkedAxisGroup {
pub(crate) link_x: bool,
pub(crate) link_y: bool,
pub(crate) bounds: Rc<RefCell<Option<PlotBounds>>>,
}
impl LinkedAxisGroup {
pub fn new(link_x: bool, link_y: bool) -> Self {
Self {
link_x,
link_y,
bounds: Rc::new(RefCell::new(None)),
}
}
/// Only link the x-axis.
pub fn x() -> Self {
Self::new(true, false)
}
/// Only link the y-axis.
pub fn y() -> Self {
Self::new(false, true)
}
/// Link both axes. Note that this still respects the aspect ratio of the individual plots.
pub fn both() -> Self {
Self::new(true, true)
}
/// Change whether the x-axis is linked for this group. Using this after plots in this group have been
/// drawn in this frame already may lead to unexpected results.
pub fn set_link_x(&mut self, link: bool) {
self.link_x = link;
}
/// Change whether the y-axis is linked for this group. Using this after plots in this group have been
/// drawn in this frame already may lead to unexpected results.
pub fn set_link_y(&mut self, link: bool) {
self.link_y = link;
}
fn get(&self) -> Option<PlotBounds> {
*self.bounds.borrow()
}
fn set(&self, bounds: PlotBounds) {
*self.bounds.borrow_mut() = Some(bounds);
}
}
@ -70,6 +134,9 @@ pub struct Plot {
allow_drag: bool,
min_auto_bounds: PlotBounds,
margin_fraction: Vec2,
allow_boxed_zoom: bool,
boxed_zoom_pointer_button: PointerButton,
linked_axes: Option<LinkedAxisGroup>,
min_size: Vec2,
width: Option<f32>,
@ -80,6 +147,7 @@ pub struct Plot {
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
axis_formatters: [AxisFormatter; 2],
legend_config: Option<Legend>,
show_background: bool,
show_axes: [bool; 2],
@ -97,6 +165,9 @@ impl Plot {
allow_drag: true,
min_auto_bounds: PlotBounds::NOTHING,
margin_fraction: Vec2::splat(0.05),
allow_boxed_zoom: true,
boxed_zoom_pointer_button: PointerButton::Secondary,
linked_axes: None,
min_size: Vec2::splat(64.0),
width: None,
@ -107,6 +178,7 @@ impl Plot {
show_x: true,
show_y: true,
custom_label_func: None,
axis_formatters: [None, None], // [None; 2] requires Copy
legend_config: None,
show_background: true,
show_axes: [true; 2],
@ -181,6 +253,20 @@ impl Plot {
self
}
/// Whether to allow zooming in the plot by dragging out a box with the secondary mouse button.
///
/// Default: `true`.
pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
self.allow_boxed_zoom = on;
self
}
/// Config the button pointer to use for boxed zooming. Default: `Secondary`
pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
self
}
/// Whether to allow dragging in the plot to move the bounds. Default: `true`.
pub fn allow_drag(mut self, on: bool) -> Self {
self.allow_drag = on;
@ -208,11 +294,35 @@ impl Plot {
/// .show(ui, |plot_ui| plot_ui.line(line));
/// # });
/// ```
pub fn custom_label_func<F: 'static + Fn(&str, &Value) -> String>(
pub fn custom_label_func(
mut self,
custom_lebel_func: F,
custom_label_func: impl Fn(&str, &Value) -> String + 'static,
) -> Self {
self.custom_label_func = Some(Box::new(custom_lebel_func));
self.custom_label_func = Some(Box::new(custom_label_func));
self
}
/// Provide a function to customize the labels for the X axis.
///
/// 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 {
self.axis_formatters[0] = Some(Box::new(func));
self
}
/// Provide a function to customize the labels for the Y axis.
///
/// 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 {
self.axis_formatters[1] = Some(Box::new(func));
self
}
@ -252,6 +362,13 @@ impl Plot {
self
}
/// Add a [`LinkedAxisGroup`] so that this plot will share the bounds with other plots that have this
/// group assigned. A plot cannot belong to more than one group.
pub fn link_axis(mut self, group: LinkedAxisGroup) -> Self {
self.linked_axes = Some(group);
self
}
/// Interact with and add items to the plot and finally draw it.
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
let Self {
@ -260,6 +377,8 @@ impl Plot {
center_y_axis,
allow_zoom,
allow_drag,
allow_boxed_zoom,
boxed_zoom_pointer_button: boxed_zoom_pointer,
min_auto_bounds,
margin_fraction,
width,
@ -270,9 +389,11 @@ impl Plot {
mut show_x,
mut show_y,
custom_label_func,
axis_formatters,
legend_config,
show_background,
show_axes,
linked_axes,
} = self;
// Determine the size of the plot in the UI
@ -315,6 +436,7 @@ impl Plot {
center_x_axis,
center_y_axis,
),
last_click_pos_for_zoom: None,
});
// If the min bounds changed, recalculate everything.
@ -333,6 +455,7 @@ impl Plot {
mut hovered_entry,
mut hidden_items,
last_screen_transform,
mut last_click_pos_for_zoom,
..
} = memory;
@ -356,7 +479,7 @@ impl Plot {
if show_background {
ui.painter().sub_region(rect).add(epaint::RectShape {
rect,
corner_radius: 2.0,
rounding: Rounding::same(2.0),
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
});
@ -385,6 +508,22 @@ impl Plot {
// --- Bound computation ---
let mut bounds = *last_screen_transform.bounds();
// Transfer the bounds from a link group.
if let Some(axes) = linked_axes.as_ref() {
if let Some(linked_bounds) = axes.get() {
if axes.link_x {
bounds.min[0] = linked_bounds.min[0];
bounds.max[0] = linked_bounds.max[0];
}
if axes.link_y {
bounds.min[1] = linked_bounds.min[1];
bounds.max[1] = linked_bounds.max[1];
}
// Turn off auto bounds to keep it from overriding what we just set.
auto_bounds = false;
}
}
// Allow double clicking to reset to automatic bounds.
auto_bounds |= response.double_clicked_by(PointerButton::Primary);
@ -401,7 +540,10 @@ impl Plot {
// Enforce equal aspect ratio.
if let Some(data_aspect) = data_aspect {
transform.set_aspect(data_aspect as f64);
let preserve_y = linked_axes
.as_ref()
.map_or(false, |group| group.link_y && !group.link_x);
transform.set_aspect(data_aspect as f64, preserve_y);
}
// Dragging
@ -412,6 +554,53 @@ impl Plot {
}
// Zooming
let mut boxed_zoom_rect = None;
if allow_boxed_zoom {
// Save last click to allow boxed zooming
if response.drag_started() && response.dragged_by(boxed_zoom_pointer) {
// it would be best for egui that input has a memory of the last click pos because it's a common pattern
last_click_pos_for_zoom = response.hover_pos();
}
let box_start_pos = last_click_pos_for_zoom;
let box_end_pos = response.hover_pos();
if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
// while dragging prepare a Shape and draw it later on top of the plot
if response.dragged_by(boxed_zoom_pointer) {
response = response.on_hover_cursor(CursorIcon::ZoomIn);
let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos);
boxed_zoom_rect = Some((
epaint::RectShape::stroke(
rect,
0.0,
epaint::Stroke::new(4., Color32::DARK_BLUE),
), // Outer stroke
epaint::RectShape::stroke(
rect,
0.0,
epaint::Stroke::new(2., Color32::WHITE),
), // Inner stroke
));
}
// when the click is release perform the zoom
if response.drag_released() {
let box_start_pos = transform.value_from_position(box_start_pos);
let box_end_pos = transform.value_from_position(box_end_pos);
let new_bounds = PlotBounds {
min: [box_start_pos.x, box_end_pos.y],
max: [box_end_pos.x, box_start_pos.y],
};
if new_bounds.is_valid() {
*transform.bounds_mut() = new_bounds;
auto_bounds = false;
} else {
auto_bounds = true;
}
// reset the boxed zoom state
last_click_pos_for_zoom = None;
}
}
}
if allow_zoom {
if let Some(hover_pos) = response.hover_pos() {
let zoom_factor = if data_aspect.is_some() {
@ -442,23 +631,34 @@ impl Plot {
show_x,
show_y,
custom_label_func,
axis_formatters,
show_axes,
transform: transform.clone(),
};
prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
ui.painter().sub_region(rect).add(boxed_zoom_rect.0);
ui.painter().sub_region(rect).add(boxed_zoom_rect.1);
}
if let Some(mut legend) = legend {
ui.add(&mut legend);
hidden_items = legend.get_hidden_items();
hovered_entry = legend.get_hovered_entry_name();
}
if let Some(group) = linked_axes.as_ref() {
group.set(*transform.bounds());
}
let memory = PlotMemory {
auto_bounds,
hovered_entry,
hidden_items,
min_auto_bounds,
last_screen_transform: transform,
last_click_pos_for_zoom,
};
memory.store(ui.ctx(), plot_id);
@ -473,13 +673,13 @@ impl Plot {
}
/// Provides methods to interact with a plot while building it. It is the single argument of the closure
/// provided to `Plot::show`. See [`Plot`] for an example of how to use it.
/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it.
pub struct PlotUi {
items: Vec<Box<dyn PlotItem>>,
next_auto_color_idx: usize,
last_screen_transform: ScreenTransform,
response: Response,
ctx: CtxRef,
ctx: Context,
}
impl PlotUi {
@ -491,7 +691,7 @@ impl PlotUi {
Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space
}
pub fn ctx(&self) -> &CtxRef {
pub fn ctx(&self) -> &Context {
&self.ctx
}
@ -650,6 +850,7 @@ struct PreparedPlot {
show_x: bool,
show_y: bool,
custom_label_func: CustomLabelFuncRef,
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
}
@ -680,10 +881,15 @@ impl PreparedPlot {
}
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
let Self { transform, .. } = self;
let Self {
transform,
axis_formatters,
..
} = self;
let bounds = transform.bounds();
let text_style = TextStyle::Body;
let font_id = TextStyle::Body.resolve(ui.style());
let base: i64 = 10;
let basef = base as f64;
@ -739,18 +945,26 @@ impl PreparedPlot {
if text_alpha > 0.0 {
let color = color_from_alpha(ui, text_alpha);
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
let galley = ui.painter().layout_no_wrap(text, text_style, color);
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main)
} else {
emath::round_to_decimals(value_main, 5).to_string() // hack
};
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
// Custom formatters can return empty string to signal "no label at this resolution"
if !text.is_empty() {
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
// Make sure we see the labels, even if the axis is off-screen:
text_pos[1 - axis] = text_pos[1 - axis]
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
.at_least(transform.frame().min[1 - axis] + 1.0);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
shapes.push(Shape::galley(text_pos, galley));
// Make sure we see the labels, even if the axis is off-screen:
text_pos[1 - axis] = text_pos[1 - axis]
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
.at_least(transform.frame().min[1 - axis] + 1.0);
shapes.push(Shape::galley(text_pos, galley));
}
}
}

View file

@ -178,6 +178,10 @@ impl ScreenTransform {
&self.bounds
}
pub fn bounds_mut(&mut self) -> &mut PlotBounds {
&mut self.bounds
}
pub fn translate_bounds(&mut self, mut delta_pos: Vec2) {
if self.x_centered {
delta_pos.x = 0.;
@ -273,13 +277,20 @@ impl ScreenTransform {
(self.bounds.width() / rw) / (self.bounds.height() / rh)
}
pub fn set_aspect(&mut self, aspect: f64) {
let epsilon = 1e-5;
/// Sets the aspect ratio by either expanding the x-axis or contracting the y-axis.
pub fn set_aspect(&mut self, aspect: f64, preserve_y: bool) {
let current_aspect = self.get_aspect();
if current_aspect < aspect - epsilon {
let epsilon = 1e-5;
if (current_aspect - aspect).abs() < epsilon {
// Don't make any changes when the aspect is already almost correct.
return;
}
if preserve_y {
self.bounds
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
} else if current_aspect > aspect + epsilon {
} else {
self.bounds
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
}

View file

@ -77,10 +77,10 @@ impl Widget for ProgressBar {
}
let visuals = ui.style().visuals.clone();
let corner_radius = outer_rect.height() / 2.0;
let rounding = outer_rect.height() / 2.0;
ui.painter().rect(
outer_rect,
corner_radius,
rounding,
visuals.extreme_bg_color,
Stroke::none(),
);
@ -101,7 +101,7 @@ impl Widget for ProgressBar {
ui.painter().rect(
inner_rect,
corner_radius,
rounding,
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
Stroke::none(),
);
@ -110,14 +110,14 @@ impl Widget for ProgressBar {
let n_points = 20;
let start_angle = ui.input().time as f64 * 360f64.to_radians();
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
let circle_radius = corner_radius - 2.0;
let circle_radius = rounding - 2.0;
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
inner_rect.right_center()
+ circle_radius * vec2(cos as f32, sin as f32)
+ vec2(-corner_radius, 0.0)
+ vec2(-rounding, 0.0)
})
.collect();
ui.painter().add(Shape::line(

View file

@ -64,9 +64,8 @@ impl Widget for SelectableLabel {
if selected || response.hovered() || response.has_focus() {
let rect = rect.expand(visuals.expansion);
let corner_radius = 2.0;
ui.painter()
.rect(rect, corner_radius, visuals.bg_fill, visuals.bg_stroke);
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
}
text.paint_with_visuals(ui.painter(), text_pos, &visuals);

View file

@ -355,7 +355,7 @@ impl<'a> Slider<'a> {
let visuals = ui.style().interact(response);
ui.painter().add(epaint::RectShape {
rect: rail_rect,
corner_radius: ui.visuals().widgets.inactive.corner_radius,
rounding: ui.visuals().widgets.inactive.rounding,
fill: ui.visuals().widgets.inactive.bg_fill,
// fill: visuals.bg_fill,
// fill: ui.visuals().extreme_bg_color,
@ -466,10 +466,8 @@ impl<'a> Slider<'a> {
}
fn add_contents(&mut self, ui: &mut Ui) -> Response {
let text_style = TextStyle::Button;
let perpendicular = ui
.fonts()
.row_height(text_style)
.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);

View file

@ -1,4 +1,4 @@
use std::sync::Arc;
use epaint::mutex::Arc;
use epaint::text::{cursor::*, Galley, LayoutJob};
@ -8,7 +8,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
/// A text region that the user can edit the contents of.
///
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
///
/// Example:
///
@ -52,11 +52,12 @@ pub struct TextEdit<'t> {
hint_text: WidgetText,
id: Option<Id>,
id_source: Option<Id>,
text_style: Option<TextStyle>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
password: bool,
frame: bool,
margin: Vec2,
multiline: bool,
interactive: bool,
desired_width: Option<f32>,
@ -96,11 +97,12 @@ impl<'t> TextEdit<'t> {
hint_text: Default::default(),
id: None,
id_source: None,
text_style: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
frame: true,
margin: vec2(4.0, 2.0),
multiline: true,
interactive: true,
desired_width: None,
@ -115,7 +117,7 @@ impl<'t> TextEdit<'t> {
/// - monospaced font
/// - focus lock
pub fn code_editor(self) -> Self {
self.text_style(TextStyle::Monospace).lock_focus(true)
self.font(TextStyle::Monospace).lock_focus(true)
}
/// Use if you want to set an explicit `Id` for this widget.
@ -142,11 +144,17 @@ impl<'t> TextEdit<'t> {
self
}
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style);
/// Pick a [`FontId`] or [`TextStyle`].
pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
self.font_selection = font_selection.into();
self
}
#[deprecated = "Use .font(…) instead"]
pub fn text_style(self, text_style: TextStyle) -> Self {
self.font(text_style)
}
pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color);
self
@ -200,6 +208,12 @@ impl<'t> TextEdit<'t> {
self
}
/// Set margin of text. Default is [4.0,2.0]
pub fn margin(mut self, margin: Vec2) -> Self {
self.margin = margin;
self
}
/// Set to 0.0 to keep as small as possible.
/// Set to [`f32::INFINITY`] to take up all available space (i.e. disable automatic word wrap).
pub fn desired_width(mut self, desired_width: f32) -> Self {
@ -264,7 +278,7 @@ impl<'t> TextEdit<'t> {
let interactive = self.interactive;
let where_to_put_background = ui.painter().add(Shape::Noop);
let margin = Vec2::new(4.0, 2.0);
let margin = self.margin;
let max_rect = ui.available_rect_before_wrap().shrink2(margin);
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
let mut output = self.show_content(&mut content_ui);
@ -285,7 +299,7 @@ impl<'t> TextEdit<'t> {
if output.response.has_focus() {
epaint::RectShape {
rect: frame_rect,
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
// fill: ui.visuals().selection.bg_fill,
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().selection.stroke,
@ -293,7 +307,7 @@ impl<'t> TextEdit<'t> {
} else {
epaint::RectShape {
rect: frame_rect,
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
fill: ui.visuals().extreme_bg_color,
stroke: visuals.bg_stroke, // TODO: we want to show something here, or a text-edit field doesn't "pop".
}
@ -302,7 +316,7 @@ impl<'t> TextEdit<'t> {
let visuals = &ui.style().visuals.widgets.inactive;
epaint::RectShape {
rect: frame_rect,
corner_radius: visuals.corner_radius,
rounding: visuals.rounding,
// fill: ui.visuals().extreme_bg_color,
// fill: visuals.bg_fill,
fill: Color32::TRANSPARENT,
@ -322,11 +336,12 @@ impl<'t> TextEdit<'t> {
hint_text,
id,
id_source,
text_style,
font_selection,
text_color,
layouter,
password,
frame: _,
margin: _,
multiline,
interactive,
desired_width,
@ -341,10 +356,9 @@ impl<'t> TextEdit<'t> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_ref().to_owned();
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or_else(|| ui.style().body_text_style);
let row_height = ui.fonts().row_height(text_style);
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
const MIN_WIDTH: f32 = 24.0; // Never make a `TextEdit` more narrow than this.
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
@ -354,12 +368,13 @@ impl<'t> TextEdit<'t> {
desired_width.min(available_width)
};
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
let text = mask_if_password(password, text);
ui.fonts().layout_job(if multiline {
LayoutJob::simple(text, text_style, text_color, wrap_width)
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, text_style, text_color)
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
})
};
@ -390,7 +405,8 @@ impl<'t> TextEdit<'t> {
// dragging select text, or scroll the enclosing `ScrollArea` (if any)?
// Since currently copying selected text in not supported on `egui_web`,
// we prioritize touch-scrolling:
let allow_drag_to_select = !ui.input().any_touches() || ui.memory().has_focus(id);
let any_touches = ui.input().any_touches(); // separate line to avoid double-locking the same mutex
let allow_drag_to_select = !any_touches || ui.memory().has_focus(id);
let sense = if interactive {
if allow_drag_to_select {
@ -406,7 +422,7 @@ impl<'t> TextEdit<'t> {
let painter = ui.painter_at(text_clip_rect);
if interactive {
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.hovered() && text.is_mutable() {
ui.output().mutable_text_under_cursor = true;
}
@ -533,9 +549,9 @@ impl<'t> TextEdit<'t> {
if text.as_ref().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let galley = if multiline {
hint_text.into_galley(ui, Some(true), desired_size.x, text_style)
hint_text.into_galley(ui, Some(true), desired_size.x, font_id)
} else {
hint_text.into_galley(ui, Some(false), f32::INFINITY, text_style)
hint_text.into_galley(ui, Some(false), f32::INFINITY, font_id)
};
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
}
@ -665,7 +681,8 @@ fn events(
let mut any_change = false;
for event in &ui.input().events {
let events = ui.input().events.clone(); // avoid dead-lock by cloning. TODO: optimize
for event in &events {
let did_mutate_text = match event {
Event::Copy => {
if cursor_range.is_empty() {
@ -684,6 +701,15 @@ fn events(
Some(CCursorRange::one(delete_selected(text, &cursor_range)))
}
}
Event::Paste(text_to_insert) => {
if !text_to_insert.is_empty() {
let mut ccursor = delete_selected(text, &cursor_range);
insert_text(&mut ccursor, text, text_to_insert);
Some(CCursorRange::one(ccursor))
} else {
None
}
}
Event::Text(text_to_insert) => {
// Newlines are handled by `Key::Enter`.
if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {

View file

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

View file

@ -7,9 +7,21 @@ edition = "2021"
rust-version = "1.56"
publish = false
[package.metadata.docs.rs]
all-features = true
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["persistence"]
http = ["egui_demo_lib/http"]
persistence = ["eframe/persistence", "egui_demo_lib/persistence"]
screen_reader = ["eframe/screen_reader"] # experimental
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
[dependencies]
eframe = { version = "0.16.0", path = "../eframe" }
@ -17,10 +29,4 @@ eframe = { version = "0.16.0", path = "../eframe" }
# eframe = { version = "0.16.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"] }
[features]
default = ["persistence"]
http = ["egui_demo_lib/http"]
persistence = ["eframe/persistence", "egui_demo_lib/persistence"]
screen_reader = ["eframe/screen_reader"] # experimental
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
tracing-subscriber = "0.3"

View file

@ -7,10 +7,13 @@
// When compiling natively:
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let app = egui_demo_lib::WrapApp::default();
let options = eframe::NativeOptions {
// Let's show off that we support transparent windows
// transparent: true,
transparent: true,
drag_and_drop_support: true,
..Default::default()
};

View file

@ -18,22 +18,43 @@ all-features = true
[lib]
[features]
default = ["datetime"]
# Enable additional checks if debug assertions are enabled (debug builds).
extra_debug_asserts = ["egui/extra_debug_asserts"]
# Always enable additional checks.
extra_asserts = ["egui/extra_asserts"]
datetime = ["egui_extras/chrono", "chrono"]
http = ["ehttp", "image", "poll-promise"]
persistence = [
"egui/persistence",
"epi/persistence",
"egui_extras/persistence",
"serde",
]
serialize = ["egui/serialize", "serde"]
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" }
chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true }
enum-map = { version = "1", features = ["serde"] }
enum-map = { version = "2", features = ["serde"] }
unicode_names2 = { version = "0.4.0", default-features = false }
# feature "http":
ehttp = { version = "0.1.0", optional = true }
image = { version = "0.23", default-features = false, features = [
ehttp = { version = "0.2.0", optional = true }
image = { version = "0.24", default-features = false, features = [
"jpeg",
"png",
], optional = true }
poll-promise = { version = "0.1", default-features = false, optional = true }
# feature "syntax_highlighting":
syntect = { version = "4", default-features = false, features = [
"default-fancy",
@ -45,25 +66,6 @@ serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies]
criterion = { version = "0.3", default-features = false }
[features]
default = ["datetime"]
datetime = ["egui_extras/datetime", "chrono"]
# Enable additional checks if debug assertions are enabled (debug builds).
extra_debug_asserts = ["egui/extra_debug_asserts"]
# Always enable additional checks.
extra_asserts = ["egui/extra_asserts"]
http = ["ehttp", "image"]
persistence = [
"egui/persistence",
"epi/persistence",
"egui_extras/persistence",
"serde",
]
serialize = ["egui/serialize", "serde"]
syntax_highlighting = ["syntect"]
[[bench]]
name = "benchmark"
harness = false

View file

@ -4,16 +4,16 @@ use egui::epaint::TextShape;
use egui_demo_lib::LOREM_IPSUM_LONG;
pub fn criterion_benchmark(c: &mut Criterion) {
let raw_input = egui::RawInput::default();
use egui::RawInput;
{
let mut ctx = egui::CtxRef::default();
let ctx = egui::Context::default();
let mut demo_windows = egui_demo_lib::DemoWindows::default();
// The most end-to-end benchmark.
c.bench_function("demo_with_tessellate__realistic", |b| {
b.iter(|| {
let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| {
let (_output, shapes) = ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx);
});
ctx.tessellate(shapes)
@ -22,13 +22,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("demo_no_tessellate", |b| {
b.iter(|| {
ctx.run(raw_input.clone(), |ctx| {
ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx);
})
})
});
let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| {
let (_output, shapes) = ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx);
});
c.bench_function("demo_only_tessellate", |b| {
@ -37,12 +37,12 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}
if false {
let mut ctx = egui::CtxRef::default();
let ctx = egui::Context::default();
ctx.memory().set_everything_is_visible(true); // give us everything
let mut demo_windows = egui_demo_lib::DemoWindows::default();
c.bench_function("demo_full_no_tessellate", |b| {
b.iter(|| {
ctx.run(raw_input.clone(), |ctx| {
ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx);
})
})
@ -50,8 +50,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}
{
let mut ctx = egui::CtxRef::default();
let _ = ctx.run(raw_input, |ctx| {
let ctx = egui::Context::default();
let _ = ctx.run(RawInput::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
c.bench_function("label &str", |b| {
b.iter(|| {
@ -68,40 +68,68 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}
{
let pixels_per_point = 1.0;
let wrap_width = 512.0;
let text_style = egui::TextStyle::Body;
let color = egui::Color32::WHITE;
let fonts =
egui::epaint::text::Fonts::new(pixels_per_point, egui::FontDefinitions::default());
c.bench_function("text_layout_uncached", |b| {
b.iter(|| {
use egui::epaint::text::{layout, LayoutJob};
let ctx = egui::Context::default();
ctx.begin_frame(RawInput::default());
let job = LayoutJob::simple(
egui::CentralPanel::default().show(&ctx, |ui| {
c.bench_function("Painter::rect", |b| {
let painter = ui.painter();
let rect = ui.max_rect();
b.iter(|| {
painter.rect(rect, 2.0, egui::Color32::RED, (1.0, egui::Color32::WHITE));
})
});
});
// Don't call `end_frame` to not have to drain the huge paint list
}
{
let pixels_per_point = 1.0;
let max_texture_side = 8 * 1024;
let wrap_width = 512.0;
let font_id = egui::FontId::default();
let color = egui::Color32::WHITE;
let fonts = egui::epaint::text::Fonts::new(
pixels_per_point,
max_texture_side,
egui::FontDefinitions::default(),
);
{
let mut locked_fonts = fonts.lock();
c.bench_function("text_layout_uncached", |b| {
b.iter(|| {
use egui::epaint::text::{layout, LayoutJob};
let job = LayoutJob::simple(
LOREM_IPSUM_LONG.to_owned(),
font_id.clone(),
color,
wrap_width,
);
layout(&mut locked_fonts.fonts, job.into())
})
});
}
c.bench_function("text_layout_cached", |b| {
b.iter(|| {
fonts.layout(
LOREM_IPSUM_LONG.to_owned(),
egui::TextStyle::Body,
font_id.clone(),
color,
wrap_width,
);
layout(&fonts, job.into())
)
})
});
c.bench_function("text_layout_cached", |b| {
b.iter(|| fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width))
});
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width);
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
let mut tessellator = egui::epaint::Tessellator::from_options(Default::default());
let mut mesh = egui::epaint::Mesh::default();
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
let font_image_size = fonts.font_image_size();
c.bench_function("tessellate_text", |b| {
b.iter(|| {
tessellator.tessellate_text(
fonts.font_image().size(),
text_shape.clone(),
&mut mesh,
);
tessellator.tessellate_text(font_image_size, text_shape.clone(), &mut mesh);
mesh.clear();
})
});

View file

@ -34,7 +34,7 @@ impl epi::App for ColorTest {
"🎨 Color test"
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if frame.is_web() {
ui.label(
@ -43,14 +43,14 @@ impl epi::App for ColorTest {
ui.separator();
}
ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| {
self.ui(ui, Some(frame));
self.ui(ui);
});
});
}
}
impl ColorTest {
pub fn ui(&mut self, ui: &mut Ui, tex_allocator: Option<&dyn epi::TextureAllocator>) {
pub fn ui(&mut self, ui: &mut Ui) {
ui.set_max_width(680.0);
ui.vertical_centered(|ui| {
@ -70,13 +70,7 @@ impl ColorTest {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
self.tex_gradient(
ui,
tex_allocator,
"orange rgb(255, 165, 0) - texture",
WHITE,
&g,
);
self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
});
ui.separator();
@ -99,20 +93,18 @@ impl ColorTest {
{
let g = Gradient::one_color(Color32::from(tex_color * vertex_color));
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
self.tex_gradient(ui, tex_allocator, "Ground truth (texture)", WHITE, &g);
}
if let Some(tex_allocator) = tex_allocator {
ui.horizontal(|ui| {
let g = Gradient::one_color(Color32::from(tex_color));
let tex = self.tex_mngr.get(tex_allocator, &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv =
Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
}
ui.horizontal(|ui| {
let g = Gradient::one_color(Color32::from(tex_color));
let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
});
ui.separator();
@ -120,18 +112,18 @@ impl ColorTest {
// TODO: test color multiplication (image tint),
// to make sure vertex and texture color multiplication is done in linear space.
self.show_gradients(ui, tex_allocator, WHITE, (RED, GREEN));
self.show_gradients(ui, WHITE, (RED, GREEN));
if self.srgb {
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
}
ui.separator();
self.show_gradients(ui, tex_allocator, RED, (TRANSPARENT, GREEN));
self.show_gradients(ui, RED, (TRANSPARENT, GREEN));
ui.separator();
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, GREEN));
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN));
if self.srgb {
ui.label(
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
@ -142,15 +134,14 @@ impl ColorTest {
// TODO: another ground truth where we do the alpha-blending against the background also.
// TODO: exactly the same thing, but with vertex colors (no textures)
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, BLACK));
self.show_gradients(ui, WHITE, (TRANSPARENT, BLACK));
ui.separator();
self.show_gradients(ui, tex_allocator, BLACK, (TRANSPARENT, WHITE));
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE));
ui.separator();
ui.label("Additive blending: add more and more blue to the red background:");
self.show_gradients(
ui,
tex_allocator,
RED,
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
);
@ -160,13 +151,7 @@ impl ColorTest {
pixel_test(ui);
}
fn show_gradients(
&mut self,
ui: &mut Ui,
tex_allocator: Option<&dyn epi::TextureAllocator>,
bg_fill: Color32,
(left, right): (Color32, Color32),
) {
fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) {
let is_opaque = left.is_opaque() && right.is_opaque();
ui.horizontal(|ui| {
@ -186,13 +171,7 @@ impl ColorTest {
if is_opaque {
let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
self.tex_gradient(
ui,
tex_allocator,
"Ground Truth (CPU gradient) - texture",
bg_fill,
&g,
);
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
} else {
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
self.vertex_gradient(
@ -203,20 +182,13 @@ impl ColorTest {
);
self.tex_gradient(
ui,
tex_allocator,
"Ground Truth (CPU gradient, CPU blending) - texture",
bg_fill,
&g,
);
let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
self.tex_gradient(
ui,
tex_allocator,
"CPU gradient, GPU blending - texture",
bg_fill,
&g,
);
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
}
let g = Gradient::texture_gradient(left, right);
@ -226,13 +198,7 @@ impl ColorTest {
bg_fill,
&g,
);
self.tex_gradient(
ui,
tex_allocator,
"Texture of width 2 (test texture sampler)",
bg_fill,
&g,
);
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
if self.srgb {
let g =
@ -243,41 +209,26 @@ impl ColorTest {
bg_fill,
&g,
);
self.tex_gradient(
ui,
tex_allocator,
"Naive sRGBA interpolation (WRONG)",
bg_fill,
&g,
);
self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g);
}
});
}
fn tex_gradient(
&mut self,
ui: &mut Ui,
tex_allocator: Option<&dyn epi::TextureAllocator>,
label: &str,
bg_fill: Color32,
gradient: &Gradient,
) {
fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
if !self.texture_gradients {
return;
}
if let Some(tex_allocator) = tex_allocator {
ui.horizontal(|ui| {
let tex = self.tex_mngr.get(tex_allocator, gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.label(label);
});
}
ui.horizontal(|ui| {
let tex = self.tex_mngr.get(ui.ctx(), gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.label(label);
});
}
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
@ -384,18 +335,21 @@ impl Gradient {
}
#[derive(Default)]
struct TextureManager(HashMap<Gradient, TextureId>);
struct TextureManager(HashMap<Gradient, TextureHandle>);
impl TextureManager {
fn get(&mut self, tex_allocator: &dyn epi::TextureAllocator, gradient: &Gradient) -> TextureId {
*self.0.entry(gradient.clone()).or_insert_with(|| {
fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
self.0.entry(gradient.clone()).or_insert_with(|| {
let pixels = gradient.to_pixel_row();
let width = pixels.len();
let height = 1;
tex_allocator.alloc(epi::Image {
size: [width, height],
pixels,
})
ctx.load_texture(
"color_test_gradient",
epaint::ColorImage {
size: [width, height],
pixels,
},
)
})
}
}

View file

@ -16,7 +16,7 @@ impl epi::App for DemoApp {
fn setup(
&mut self,
_ctx: &egui::CtxRef,
_ctx: &egui::Context,
_frame: &epi::Frame,
_storage: Option<&dyn epi::Storage>,
) {
@ -31,7 +31,7 @@ impl epi::App for DemoApp {
epi::set_value(storage, epi::APP_KEY, self);
}
fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) {
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
self.demo_windows.ui(ctx);
}
}

View file

@ -26,7 +26,7 @@ impl super::Demo for CodeEditor {
"🖮 Code Editor"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use super::View as _;
egui::Window::new(self.name())
.open(open)
@ -71,7 +71,7 @@ impl super::View for CodeEditor {
ui.collapsing("Theme", |ui| {
ui.group(|ui| {
theme.ui(ui);
theme.store_in_memory(ui.ctx());
theme.clone().store_in_memory(ui.ctx());
});
});
@ -85,7 +85,7 @@ impl super::View for CodeEditor {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(code)
.text_style(egui::TextStyle::Monospace) // for cursor height
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(10)
.lock_focus(true)

View file

@ -68,7 +68,7 @@ impl super::Demo for CodeExample {
"🖮 Code Example"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use super::View;
egui::Window::new(self.name())
.open(open)
@ -98,7 +98,8 @@ impl CodeExample {
);
ui.horizontal(|ui| {
let indentation = 8.0 * ui.fonts()[egui::TextStyle::Monospace].glyph_width(' ');
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
let item_spacing = ui.spacing_mut().item_spacing;
ui.add_space(indentation - item_spacing.x);

View file

@ -47,7 +47,7 @@ impl super::Demo for ContextMenus {
"☰ Context Menus"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use super::View;
egui::Window::new(self.name())
.vscroll(false)

View file

@ -10,7 +10,7 @@ impl super::Demo for DancingStrings {
"♫ Dancing Strings"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)

View file

@ -1,5 +1,5 @@
use super::Demo;
use egui::{CtxRef, ScrollArea, Ui};
use egui::{Context, ScrollArea, Ui};
use std::collections::BTreeSet;
// ----------------------------------------------------------------------------
@ -26,6 +26,7 @@ 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()),
@ -60,7 +61,7 @@ impl Demos {
}
}
pub fn windows(&mut self, ctx: &CtxRef) {
pub fn windows(&mut self, ctx: &Context) {
let Self { demos, open } = self;
for demo in demos {
let mut is_open = open.contains(demo.name());
@ -115,7 +116,7 @@ impl Tests {
}
}
pub fn windows(&mut self, ctx: &CtxRef) {
pub fn windows(&mut self, ctx: &Context) {
let Self { demos, open } = self;
for demo in demos {
let mut is_open = open.contains(demo.name());
@ -151,7 +152,7 @@ pub struct DemoWindows {
impl DemoWindows {
/// Show the app ui (menu bar and windows).
/// `sidebar_ui` can be used to optionally show some things in the sidebar
pub fn ui(&mut self, ctx: &CtxRef) {
pub fn ui(&mut self, ctx: &Context) {
let Self { demos, tests } = self;
egui::SidePanel::right("egui_demo_panel")
@ -216,7 +217,7 @@ impl DemoWindows {
}
/// Show the open windows.
fn windows(&mut self, ctx: &CtxRef) {
fn windows(&mut self, ctx: &Context) {
let Self { demos, tests } = self;
demos.windows(ctx);

View file

@ -25,7 +25,7 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
// (anything with `Order::Tooltip` always gets an empty `Response`)
// So this is fine!
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
let delta = pointer_pos - response.rect.center();
ui.ctx().translate_layer(layer_id, delta);
}
@ -66,7 +66,7 @@ pub fn drop_target<R>(
ui.painter().set(
where_to_put_background,
epaint::RectShape {
corner_radius: style.corner_radius,
rounding: style.rounding,
fill,
stroke,
rect,
@ -101,7 +101,7 @@ impl super::Demo for DragAndDropDemo {
"✋ Drag and Drop"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)

View file

@ -2,15 +2,15 @@ use std::collections::BTreeMap;
pub struct FontBook {
filter: String,
text_style: egui::TextStyle,
named_chars: BTreeMap<egui::TextStyle, BTreeMap<char, String>>,
font_id: egui::FontId,
named_chars: BTreeMap<egui::FontFamily, BTreeMap<char, String>>,
}
impl Default for FontBook {
fn default() -> Self {
Self {
filter: Default::default(),
text_style: egui::TextStyle::Button,
font_id: egui::FontId::proportional(20.0),
named_chars: Default::default(),
}
}
@ -21,7 +21,7 @@ impl super::Demo for FontBook {
"🔤 Font Book"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
use super::View as _;
self.ui(ui);
@ -34,7 +34,7 @@ impl super::View for FontBook {
ui.label(format!(
"The selected font supports {} characters.",
self.named_chars
.get(&self.text_style)
.get(&self.font_id.family)
.map(|map| map.len())
.unwrap_or_default()
));
@ -51,13 +51,7 @@ impl super::View for FontBook {
ui.separator();
egui::ComboBox::from_label("Text style")
.selected_text(format!("{:?}", self.text_style))
.show_ui(ui, |ui| {
for style in egui::TextStyle::all() {
ui.selectable_value(&mut self.text_style, style, format!("{:?}", style));
}
});
egui::introspection::font_id_ui(ui, &mut self.font_id);
ui.horizontal(|ui| {
ui.label("Filter:");
@ -68,16 +62,11 @@ impl super::View for FontBook {
}
});
let text_style = self.text_style;
let filter = &self.filter;
let named_chars = self.named_chars.entry(text_style).or_insert_with(|| {
ui.fonts()[text_style]
.characters()
.iter()
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
.map(|&chr| (chr, char_name(chr)))
.collect()
});
let named_chars = self
.named_chars
.entry(self.font_id.family.clone())
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
ui.separator();
@ -88,12 +77,14 @@ impl super::View for FontBook {
for (&chr, name) in named_chars {
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
let button = egui::Button::new(
egui::RichText::new(chr.to_string()).text_style(text_style),
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
)
.frame(false);
let tooltip_ui = |ui: &mut egui::Ui| {
ui.label(egui::RichText::new(chr.to_string()).text_style(text_style));
ui.label(
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
);
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
};
@ -107,6 +98,18 @@ impl super::View for FontBook {
}
}
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
ui.fonts()
.lock()
.fonts
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
.characters()
.iter()
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
.map(|&chr| (chr, char_name(chr)))
.collect()
}
fn char_name(chr: char) -> String {
special_char_name(chr)
.map(|s| s.to_owned())

View file

@ -11,7 +11,7 @@ impl super::Demo for GridDemo {
"▣ Grid Demo"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(true)

View file

@ -76,7 +76,7 @@ impl super::Demo for LayoutTest {
"Layout Test"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(false)

View file

@ -31,7 +31,7 @@ impl Demo for MiscDemoWindow {
"✨ Misc Demos"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &Context, open: &mut bool) {
Window::new(self.name())
.open(open)
.vscroll(true)
@ -140,7 +140,8 @@ impl Widgets {
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
ui.spacing_mut().item_spacing.x = ui.fonts()[TextStyle::Body].glyph_width(' ');
let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' ');
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
@ -268,7 +269,7 @@ impl ColorWidgets {
#[cfg_attr(feature = "serde", serde(default))]
struct BoxPainting {
size: Vec2,
corner_radius: f32,
rounding: f32,
stroke_width: f32,
num_boxes: usize,
}
@ -277,7 +278,7 @@ impl Default for BoxPainting {
fn default() -> Self {
Self {
size: vec2(64.0, 32.0),
corner_radius: 5.0,
rounding: 5.0,
stroke_width: 2.0,
num_boxes: 1,
}
@ -288,7 +289,7 @@ impl BoxPainting {
pub fn ui(&mut self, ui: &mut Ui) {
ui.add(Slider::new(&mut self.size.x, 0.0..=500.0).text("width"));
ui.add(Slider::new(&mut self.size.y, 0.0..=500.0).text("height"));
ui.add(Slider::new(&mut self.corner_radius, 0.0..=50.0).text("corner_radius"));
ui.add(Slider::new(&mut self.rounding, 0.0..=50.0).text("rounding"));
ui.add(Slider::new(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
ui.add(Slider::new(&mut self.num_boxes, 0..=8).text("num_boxes"));
@ -297,7 +298,7 @@ impl BoxPainting {
let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover());
ui.painter().rect(
rect,
self.corner_radius,
self.rounding,
Color32::from_gray(64),
Stroke::new(self.stroke_width, Color32::WHITE),
);
@ -417,7 +418,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"This is a demonstration of ",
first_row_indentation,
TextFormat {
style: TextStyle::Body,
color: default_color,
..Default::default()
},
@ -426,7 +426,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"the egui text layout engine. ",
0.0,
TextFormat {
style: TextStyle::Body,
color: strong_color,
..Default::default()
},
@ -435,7 +434,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"It supports ",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
..Default::default()
},
@ -444,7 +442,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"different ",
0.0,
TextFormat {
style: TextStyle::Body,
color: Color32::from_rgb(110, 255, 110),
..Default::default()
},
@ -453,7 +450,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"colors, ",
0.0,
TextFormat {
style: TextStyle::Body,
color: Color32::from_rgb(128, 140, 255),
..Default::default()
},
@ -462,7 +458,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"backgrounds, ",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
background: Color32::from_rgb(128, 32, 32),
..Default::default()
@ -472,7 +467,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"mixing ",
0.0,
TextFormat {
style: TextStyle::Heading,
font_id: FontId::proportional(20.0),
color: default_color,
..Default::default()
},
@ -481,7 +476,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"fonts, ",
0.0,
TextFormat {
style: TextStyle::Monospace,
font_id: FontId::monospace(14.0),
color: default_color,
..Default::default()
},
@ -490,7 +485,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"raised text, ",
0.0,
TextFormat {
style: TextStyle::Small,
font_id: FontId::proportional(8.0),
color: default_color,
valign: Align::TOP,
..Default::default()
@ -500,7 +495,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"with ",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
..Default::default()
},
@ -509,7 +503,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"underlining",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
underline: Stroke::new(1.0, Color32::LIGHT_BLUE),
..Default::default()
@ -519,7 +512,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
" and ",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
..Default::default()
},
@ -528,7 +520,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"strikethrough",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
strikethrough: Stroke::new(2.0, Color32::RED.linear_multiply(0.5)),
..Default::default()
@ -538,7 +529,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
". Of course, ",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
..Default::default()
},
@ -547,7 +537,6 @@ fn text_layout_ui(ui: &mut egui::Ui) {
"you can",
0.0,
TextFormat {
style: TextStyle::Body,
color: default_color,
strikethrough: Stroke::new(1.0, strong_color),
..Default::default()
@ -557,7 +546,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
" mix these!",
0.0,
TextFormat {
style: TextStyle::Small,
font_id: FontId::proportional(8.0),
color: Color32::LIGHT_BLUE,
background: Color32::from_rgb(128, 0, 0),
underline: Stroke::new(1.0, strong_color),

View file

@ -16,6 +16,7 @@ pub mod grid_demo;
pub mod layout_test;
pub mod misc_demo_window;
pub mod multi_touch;
pub mod paint_bezier;
pub mod painting;
pub mod password;
pub mod plot_demo;
@ -47,5 +48,5 @@ pub trait Demo {
fn name(&self) -> &'static str;
/// Show windows, etc
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool);
fn show(&mut self, ctx: &egui::Context, open: &mut bool);
}

View file

@ -26,7 +26,7 @@ impl super::Demo for MultiTouch {
"👌 Multi Touch"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.default_size(vec2(512.0, 512.0))
@ -77,7 +77,7 @@ impl super::View for MultiTouch {
// color and width:
let mut stroke_width = 1.;
let color = Color32::GRAY;
if let Some(multi_touch) = ui.input().multi_touch() {
if let Some(multi_touch) = ui.ctx().multi_touch() {
// This adjusts the current zoom factor and rotation angle according to the dynamic
// change (for the current frame) of the touch gesture:
self.zoom *= multi_touch.zoom_delta;

View file

@ -0,0 +1,254 @@
use egui::emath::RectTransform;
use egui::epaint::{CircleShape, CubicBezierShape, 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>,
/// 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),
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),
}
}
}
impl PaintBezier {
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response {
ui.horizontal(|ui| {
ui.vertical(|ui| {
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| {
{
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
}
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 to_screen = emath::RectTransform::from_to(
Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
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() {
3 => {
let quadratic = QuadraticBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.q_shapes.push(quadratic);
}
4 => {
let cubic = CubicBezierShape::from_points_stroke(
points,
self.closed,
self.fill,
self.stroke,
);
self.c_shapes.push(cubic);
}
_ => {
unreachable!();
}
}
}
response.mark_changed();
}
}
let mut 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,
));
}
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"
}
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)
.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!());
// });
self.ui_control(ui);
Frame::dark_canvas(ui.style()).show(ui, |ui| {
self.ui_content(ui);
});
}
}

View file

@ -74,7 +74,7 @@ impl super::Demo for Painting {
"🖊 Painting"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)

View file

@ -19,7 +19,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
// Get state for this widget.
// You should get state by value, not by reference to avoid borrowing of `Memory`.
let mut show_plaintext = ui.memory().data.get_temp::<bool>(state_id).unwrap_or(false);
let mut show_plaintext = ui.data().get_temp::<bool>(state_id).unwrap_or(false);
// Process ui, change a local copy of the state
// We want TextEdit to fill entire space, and have button after that, so in that case we can
@ -42,7 +42,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
});
// Store the (possibly changed) state:
ui.memory().data.insert_temp(state_id, show_plaintext);
ui.data().insert_temp(state_id, show_plaintext);
// All done! Return the interaction response so the user can check what happened
// (hovered, clicked, …) and maybe show a tooltip:

View file

@ -71,8 +71,8 @@ impl LineDemo {
ui.vertical(|ui| {
ui.style_mut().wrap = Some(false);
ui.checkbox(animate, "animate");
ui.checkbox(square, "square view")
ui.checkbox(animate, "Animate");
ui.checkbox(square, "Square view")
.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.");
@ -237,19 +237,11 @@ impl Widget for &mut MarkerDemo {
}
}
#[derive(PartialEq)]
#[derive(Default, PartialEq)]
struct LegendDemo {
config: Legend,
}
impl Default for LegendDemo {
fn default() -> Self {
Self {
config: Legend::default(),
}
}
}
impl LegendDemo {
fn line_with_slope(slope: f64) -> Line {
Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100))
@ -269,9 +261,10 @@ impl Widget for &mut LegendDemo {
egui::Grid::new("settings").show(ui, |ui| {
ui.label("Text style:");
ui.horizontal(|ui| {
TextStyle::all().for_each(|style| {
ui.selectable_value(&mut config.text_style, style, format!("{:?}", style));
});
let all_text_styles = ui.style().text_styles();
for style in all_text_styles {
ui.selectable_value(&mut config.text_style, style.clone(), style.to_string());
}
});
ui.end_row();
@ -292,7 +285,9 @@ impl Widget for &mut LegendDemo {
ui.end_row();
});
let legend_plot = Plot::new("legend_demo").legend(*config).data_aspect(1.0);
let legend_plot = Plot::new("legend_demo")
.legend(config.clone())
.data_aspect(1.0);
legend_plot
.show(ui, |plot_ui| {
plot_ui.line(LegendDemo::line_with_slope(0.5).name("lines"));
@ -305,10 +300,82 @@ impl Widget for &mut LegendDemo {
}
}
#[derive(PartialEq, Default)]
struct ItemsDemo {}
#[derive(PartialEq)]
struct LinkedAxisDemo {
link_x: bool,
link_y: bool,
group: plot::LinkedAxisGroup,
}
impl ItemsDemo {}
impl Default for LinkedAxisDemo {
fn default() -> Self {
let link_x = true;
let link_y = false;
Self {
link_x,
link_y,
group: plot::LinkedAxisGroup::new(link_x, link_y),
}
}
}
impl LinkedAxisDemo {
fn line_with_slope(slope: f64) -> Line {
Line::new(Values::from_explicit_callback(move |x| slope * x, .., 100))
}
fn sin() -> Line {
Line::new(Values::from_explicit_callback(move |x| x.sin(), .., 100))
}
fn cos() -> Line {
Line::new(Values::from_explicit_callback(move |x| x.cos(), .., 100))
}
fn configure_plot(plot_ui: &mut plot::PlotUi) {
plot_ui.line(LinkedAxisDemo::line_with_slope(0.5));
plot_ui.line(LinkedAxisDemo::line_with_slope(1.0));
plot_ui.line(LinkedAxisDemo::line_with_slope(2.0));
plot_ui.line(LinkedAxisDemo::sin());
plot_ui.line(LinkedAxisDemo::cos());
}
}
impl Widget for &mut LinkedAxisDemo {
fn ui(self, ui: &mut Ui) -> Response {
ui.horizontal(|ui| {
ui.label("Linked axes:");
ui.checkbox(&mut self.link_x, "X");
ui.checkbox(&mut self.link_y, "Y");
});
self.group.set_link_x(self.link_x);
self.group.set_link_y(self.link_y);
ui.horizontal(|ui| {
Plot::new("linked_axis_1")
.data_aspect(1.0)
.width(250.0)
.height(250.0)
.link_axis(self.group.clone())
.show(ui, LinkedAxisDemo::configure_plot);
Plot::new("linked_axis_2")
.data_aspect(2.0)
.width(150.0)
.height(250.0)
.link_axis(self.group.clone())
.show(ui, LinkedAxisDemo::configure_plot);
});
Plot::new("linked_axis_3")
.data_aspect(0.5)
.width(250.0)
.height(150.0)
.link_axis(self.group.clone())
.show(ui, LinkedAxisDemo::configure_plot)
.response
}
}
#[derive(PartialEq, Default)]
struct ItemsDemo {
texture: Option<egui::TextureHandle>,
}
impl Widget for &mut ItemsDemo {
fn ui(self, ui: &mut Ui) -> Response {
@ -343,13 +410,15 @@ impl Widget for &mut ItemsDemo {
);
Arrows::new(arrow_origins, arrow_tips)
};
let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
ui.ctx()
.load_texture("plot_demo", egui::ColorImage::example())
});
let image = PlotImage::new(
TextureId::Egui,
texture,
Value::new(0.0, 10.0),
[
ui.fonts().font_image().width as f32 / 100.0,
ui.fonts().font_image().height as f32 / 100.0,
],
5.0 * vec2(texture.aspect_ratio(), 1.0),
);
let plot = Plot::new("items_demo")
@ -376,15 +445,9 @@ impl Widget for &mut ItemsDemo {
}
}
#[derive(PartialEq)]
#[derive(Default, PartialEq)]
struct InteractionDemo {}
impl Default for InteractionDemo {
fn default() -> Self {
Self {}
}
}
impl Widget for &mut InteractionDemo {
fn ui(self, ui: &mut Ui) -> Response {
let plot = Plot::new("interaction_demo").height(300.0);
@ -532,15 +595,40 @@ impl ChartsDemo {
.name("Set 4")
.stack_on(&[&chart1, &chart2, &chart3]);
let mut x_fmt: fn(f64) -> String = |val| {
if val >= 0.0 && val <= 4.0 && is_approx_integer(val) {
// Only label full days from 0 to 4
format!("Day {}", val)
} else {
// Otherwise return empty string (i.e. no label)
String::new()
}
};
let mut y_fmt: fn(f64) -> String = |val| {
let percent = 100.0 * val;
if is_approx_integer(percent) && !is_approx_zero(percent) {
// Only show integer percentages,
// and don't show at Y=0 (label overlaps with X axis label)
format!("{}%", percent)
} else {
String::new()
}
};
if !self.vertical {
chart1 = chart1.horizontal();
chart2 = chart2.horizontal();
chart3 = chart3.horizontal();
chart4 = chart4.horizontal();
std::mem::swap(&mut x_fmt, &mut y_fmt);
}
Plot::new("Stacked Bar Chart Demo")
.legend(Legend::default())
.x_axis_formatter(x_fmt)
.y_axis_formatter(y_fmt)
.data_aspect(1.0)
.show(ui, |plot_ui| {
plot_ui.bar_chart(chart1);
@ -623,11 +711,12 @@ enum Panel {
Charts,
Items,
Interaction,
LinkedAxes,
}
impl Default for Panel {
fn default() -> Self {
Self::Charts
Self::Lines
}
}
@ -639,6 +728,7 @@ pub struct PlotDemo {
charts_demo: ChartsDemo,
items_demo: ItemsDemo,
interaction_demo: InteractionDemo,
linked_axes_demo: LinkedAxisDemo,
open_panel: Panel,
}
@ -647,7 +737,7 @@ impl super::Demo for PlotDemo {
"🗠 Plot"
}
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)
@ -663,6 +753,7 @@ impl super::View for PlotDemo {
egui::reset_button(ui, self);
ui.collapsing("Instructions", |ui| {
ui.label("Pan by dragging, or scroll (+ shift = horizontal).");
ui.label("Box zooming: Right click to zoom in and zoom out using a selection.");
if cfg!(target_arch = "wasm32") {
ui.label("Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture.");
} else if cfg!(target_os = "macos") {
@ -682,6 +773,7 @@ impl super::View for PlotDemo {
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
});
ui.separator();
@ -704,6 +796,17 @@ impl super::View for PlotDemo {
Panel::Interaction => {
ui.add(&mut self.interaction_demo);
}
Panel::LinkedAxes => {
ui.add(&mut self.linked_axes_demo);
}
}
}
}
fn is_approx_zero(val: f64) -> bool {
val.abs() < 1e-6
}
fn is_approx_integer(val: f64) -> bool {
val.fract().abs() < 1e-6
}

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