Merge branch 'master' into zlayer

# Conflicts:
#	crates/egui/src/containers/area.rs
#	crates/egui/src/containers/popup.rs
#	crates/egui/src/context.rs
#	crates/egui/src/painter.rs
#	crates/egui/src/ui.rs
This commit is contained in:
Emil Ernerfeldt 2023-01-26 14:52:56 +01:00
commit 9ca4d173c5
96 changed files with 2107 additions and 1374 deletions

3
.gitignore vendored
View file

@ -1,6 +1,7 @@
.DS_Store
**/target
**/target_ra
**/target_wasm
/.*.json
/.vscode
/media/*
.DS_Store

View file

@ -5,15 +5,37 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
### Added ⭐
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)).
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
* Add `Response::on_hover_and_drag_cursor`.
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
### Changed 🔧
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
### Fixed 🐛
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
* Fixed rendering of `…` (ellipsis).
* Menus are now moved to fit on the screen.
## 0.20.1 - 2022-12-11 - Fix key-repeat

42
Cargo.lock generated
View file

@ -2171,6 +2171,13 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "hello_world_par"
version = "0.1.0"
dependencies = [
"eframe",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -2298,6 +2305,20 @@ dependencies = [
"walkdir",
]
[[package]]
name = "jni"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
@ -4071,7 +4092,7 @@ dependencies = [
"cocoa-foundation",
"core-foundation",
"dyn-clonable",
"jni",
"jni 0.19.0",
"lazy_static",
"libc",
"log",
@ -4494,18 +4515,19 @@ dependencies = [
[[package]]
name = "webbrowser"
version = "0.8.0"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d62aa75495ab67cdc273d0b95cc76bcedfea2ba28338a4cf9b4137949dfac5"
checksum = "769f1a8831de12cad7bd6f9693b15b1432d93a151557810f617f626af823acae"
dependencies = [
"jni",
"ndk-glue 0.7.0",
"core-foundation",
"dirs",
"jni 0.20.0",
"log",
"ndk-context",
"objc",
"raw-window-handle 0.5.0",
"url",
"web-sys",
"widestring",
"winapi",
]
[[package]]
@ -4647,12 +4669,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "widestring"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -191,6 +191,7 @@ These are the official egui integrations:
* [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc).
* [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan).
* [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13).
* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe).
* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/).
Missing an integration for the thing you're working on? Create one, it's easy!
@ -370,6 +371,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>.
egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock.
### Inspiration
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy.
@ -395,6 +398,7 @@ Notable contributions by:
* [@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).
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).

View file

@ -9,6 +9,9 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
#### Desktop/Native:
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
#### Web:
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
## 0.20.1 - 2022-12-11
* Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).

View file

@ -55,7 +55,7 @@ persistence = [
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`)
screen_reader = ["egui-winit/screen_reader", "tts"]
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.

View file

@ -173,7 +173,7 @@ pub trait App {
}
/// If `true` a warm-up call to [`Self::update`] will be issued where
/// `ctx.memory().everything_is_visible()` will be set to `true`.
/// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`.
///
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
///

View file

@ -257,7 +257,8 @@ impl EpiIntegration {
) -> Self {
let egui_ctx = egui::Context::default();
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
egui_ctx.memory_mut(|mem| *mem = memory);
let native_pixels_per_point = window.scale_factor() as f32;
@ -315,11 +316,12 @@ impl EpiIntegration {
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
self.egui_ctx
.memory_mut(|mem| mem.set_everything_is_visible(true));
let full_output = self.update(app, window);
self.pending_full_output.append(full_output); // Handle it next frame
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
@ -446,7 +448,8 @@ impl EpiIntegration {
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
crate::profile_scope!("App::save");

View file

@ -313,10 +313,11 @@ impl AppRunner {
pub fn warm_up(&mut self) -> Result<(), JsValue> {
if self.app.warm_up_enabled() {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
self.egui_ctx
.memory_mut(|m| m.set_everything_is_visible(true));
self.logic()?;
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
Ok(())
@ -388,7 +389,7 @@ impl AppRunner {
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options().screen_reader {
if self.egui_ctx.options(|o| o.screen_reader) {
self.screen_reader
.speak(&platform_output.events_description());
}

View file

@ -1,6 +1,9 @@
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
struct IsDestroyed(pub bool);
pub fn paint_and_schedule(
@ -64,8 +67,9 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.modifiers = modifiers;
let key = event.key();
let egui_key = translate_key(&key);
if let Some(key) = translate_key(&key) {
if let Some(key) = egui_key {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
@ -85,10 +89,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let prevent_default = if matches!(event.key().as_str(), "Tab") {
#[allow(clippy::if_same_then_else)]
let prevent_default = if egui_key == Some(Key::Tab) {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} else if egui_key == Some(Key::P) {
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
} else if egui_wants_keyboard {
matches!(
event.key().as_str(),
@ -112,6 +119,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
if prevent_default {
event.prevent_default();
// event.stop_propagation();
}
},
)?;
@ -198,15 +206,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{
let prevent_default_events = [
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
"contextmenu",
// Allow users to use ctrl-p for e.g. a command palette
"afterprint",
];
for event_name in prevent_default_events {
let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
event.prevent_default();
// event.stop_propagation();
// tracing::debug!("Preventing event {:?}", event_name);
};
runner_container.add_event_listener(&canvas, event_name, closure)?;

View file

@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
match ron::from_str(&memory_string) {
Ok(memory) => {
*ctx.memory() = memory;
ctx.memory_mut(|m| *m = memory);
}
Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err);
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) {
match ron::to_string(&*ctx.memory()) {
match ctx.memory(|mem| ron::to_string(mem)) {
Ok(ron) => {
local_storage_set("egui_memory_ron", &ron);
}

View file

@ -70,7 +70,7 @@ serde = { version = "1.0", optional = true, features = ["derive"] }
# feature screen_reader
tts = { version = "0.24", optional = true }
webbrowser = { version = "0.8", optional = true }
webbrowser = { version = "0.8.3", optional = true }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.6.3", optional = true }

View file

@ -615,7 +615,7 @@ impl State {
egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput,
) {
if egui_ctx.options().screen_reader {
if egui_ctx.options(|o| o.screen_reader) {
self.screen_reader
.speak(&platform_output.events_description());
}

View file

@ -5,7 +5,7 @@
use crate::*;
/// State that is persisted between frames.
// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be?
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State {
@ -231,7 +231,7 @@ impl Area {
let layer_id = AreaLayerId::new(order, id);
let state = ctx.memory().areas.get(id).copied();
let state = ctx.memory(|mem| mem.areas.get(id).copied());
let is_new = state.is_none();
if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
@ -278,7 +278,7 @@ impl Area {
// Important check - don't try to move e.g. a combobox popup!
if movable {
if move_response.dragged() {
state.pos += ctx.input().pointer.delta();
state.pos += ctx.input(|i| i.pointer.delta());
}
state.pos = ctx
@ -288,9 +288,9 @@ impl Area {
if (move_response.dragged() || move_response.clicked())
|| pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
{
ctx.memory().areas.move_to_top(layer_id);
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
ctx.request_repaint();
}
@ -329,7 +329,7 @@ impl Area {
}
let layer_id = AreaLayerId::new(self.order, self.id);
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
if let Some(area_rect) = area_rect {
let clip_rect = ctx.available_rect();
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
@ -358,7 +358,7 @@ impl Prepared {
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.input().screen_rect();
let screen_rect = ctx.screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds
@ -410,7 +410,7 @@ impl Prepared {
state.size = content_ui.min_rect().size();
ctx.memory().areas.set_state(layer_id, state);
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
move_response
}
@ -418,7 +418,7 @@ impl Prepared {
fn pointer_pressed_on_area(ctx: &Context, layer_id: AreaLayerId) -> bool {
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let any_pressed = ctx.input().pointer.any_pressed();
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
@ -426,13 +426,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: AreaLayerId) -> bool {
}
fn automatic_area_position(ctx: &Context) -> Pos2 {
let mut existing: Vec<Rect> = ctx
.memory()
.areas
.visible_windows()
.into_iter()
.map(State::rect)
.collect();
let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas
.visible_windows()
.into_iter()
.map(State::rect)
.collect()
});
existing.sort_by_key(|r| r.left().round() as i32);
let available_rect = ctx.available_rect();

View file

@ -26,13 +26,14 @@ pub struct CollapsingState {
impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data()
.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
ctx.data_mut(|d| {
d.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
})
}
pub fn store(&self, ctx: &Context) {
ctx.data().insert_persisted(self.id, self.state);
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
}
pub fn id(&self) -> Id {
@ -64,7 +65,7 @@ impl CollapsingState {
/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory().everything_is_visible() {
if ctx.memory(|mem| mem.everything_is_visible()) {
1.0
} else {
ctx.animate_bool(self.id, self.state.open)
@ -555,7 +556,7 @@ impl CollapsingHeader {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
});

View file

@ -162,9 +162,6 @@ impl ComboBox {
let button_id = ui.make_persistent_id(id_source);
ui.horizontal(|ui| {
if let Some(width) = width {
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
}
let mut ir = combo_box_dyn(
ui,
button_id,
@ -172,6 +169,7 @@ impl ComboBox {
menu_contents,
icon,
wrap_enabled,
width,
);
if let Some(label) = label {
ir.response
@ -240,21 +238,17 @@ fn combo_box_dyn<'c, R>(
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>,
wrap_enabled: bool,
width: Option<f32>,
) -> InnerResponse<Option<R>> {
let popup_id = button_id.with("popup");
let is_popup_open = ui.memory().is_popup_open(popup_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui
.ctx()
.memory()
.areas
.get(popup_id)
.map_or(100.0, |state| state.size.y);
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
< ui.ctx().input().screen_rect().bottom()
< ui.ctx().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
@ -263,18 +257,20 @@ fn combo_box_dyn<'c, R>(
let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
let icon_spacing = ui.spacing().icon_spacing;
// We don't want to change width when user selects something new
let full_minimum_width = if wrap_enabled {
// Currently selected value's text will be wrapped if needed, so occupy the available width.
ui.available_width()
} else {
// Occupy at least the minimum width assigned to Slider and ComboBox.
ui.spacing().slider_width - 2.0 * margin.x
// Occupy at least the minimum width assigned to ComboBox.
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
width - 2.0 * margin.x
};
let icon_size = Vec2::splat(ui.spacing().icon_width);
let wrap_width = if wrap_enabled {
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
ui.available_width() - ui.spacing().item_spacing.x - icon_size.x
ui.available_width() - icon_spacing - icon_size.x
} else {
// Use all the width necessary to display the currently selected value's text.
f32::INFINITY
@ -288,7 +284,7 @@ fn combo_box_dyn<'c, R>(
full_minimum_width
} else {
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
galley.size().x + ui.spacing().item_spacing.x + icon_size.x
galley.size().x + icon_spacing + icon_size.x
};
// Case : wrap_enabled : occupy all the available width.
@ -333,7 +329,7 @@ fn combo_box_dyn<'c, R>(
});
if button_response.clicked() {
ui.memory().toggle_popup(popup_id);
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
let inner = crate::popup::popup_above_or_below_widget(
ui,
@ -390,7 +386,7 @@ fn button_frame(
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
},
);

View file

@ -28,7 +28,7 @@ pub struct PanelState {
impl PanelState {
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data().get_persisted(bar_id)
ctx.data_mut(|d| d.get_persisted(bar_id))
}
/// The size of the panel (from previous frame).
@ -37,7 +37,7 @@ impl PanelState {
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_persisted(bar_id, self);
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
}
}
@ -245,11 +245,12 @@ impl SidePanel {
&& (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side;
let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks
if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line {
ui.memory().set_dragged_id(resize_id);
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line
{
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
}
is_resizing = ui.memory().is_being_dragged(resize_id);
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs();
let width =
@ -257,12 +258,12 @@ impl SidePanel {
side.set_rect_width(&mut panel_rect, width);
}
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
let dragging_something_else =
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
}
}
}
@ -334,19 +335,19 @@ impl SidePanel {
let layer_id = AreaLayerId::background();
let side = self.side;
let available_rect = ctx.available_rect();
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
Side::Left => ctx
.frame_state()
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
Side::Right => ctx
.frame_state()
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
Side::Left => ctx.frame_state_mut(|state| {
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
}),
Side::Right => ctx.frame_state_mut(|state| {
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
}),
}
inner_response
}
@ -682,7 +683,7 @@ impl TopBottomPanel {
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
let latest_pos = ui.input().pointer.latest_pos();
let latest_pos = ui.input(|i| i.pointer.latest_pos());
if let Some(pointer) = latest_pos {
let we_are_on_top = ui
.ctx()
@ -695,13 +696,12 @@ impl TopBottomPanel {
&& (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line
{
ui.memory().interaction.drag_id = Some(resize_id);
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id));
}
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone())
@ -709,12 +709,12 @@ impl TopBottomPanel {
side.set_rect_height(&mut panel_rect, height);
}
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
let dragging_something_else =
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeVertical;
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
}
}
}
@ -787,7 +787,7 @@ impl TopBottomPanel {
let available_rect = ctx.available_rect();
let side = self.side;
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
@ -795,12 +795,14 @@ impl TopBottomPanel {
match side {
TopBottomSide::Top => {
ctx.frame_state()
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
ctx.frame_state_mut(|state| {
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
});
}
TopBottomSide::Bottom => {
ctx.frame_state()
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
ctx.frame_state_mut(|state| {
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
});
}
}
@ -1042,14 +1044,13 @@ impl CentralPanel {
let layer_id = AreaLayerId::background();
let id = Id::new("central_panel");
let clip_rect = ctx.input().screen_rect();
let clip_rect = ctx.screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.frame_state()
.allocate_central_panel(inner_response.response.rect);
ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
inner_response
}

View file

@ -13,11 +13,11 @@ pub(crate) struct TooltipState {
impl TooltipState {
pub fn load(ctx: &Context) -> Option<Self> {
ctx.data().get_temp(Id::null())
ctx.data_mut(|d| d.get_temp(Id::null()))
}
fn store(self, ctx: &Context) {
ctx.data().insert_temp(Id::null(), self);
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
}
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let suggested_pos = ctx
.input()
.pointer
.hover_pos()
.input(|i| i.pointer.hover_pos())
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
show_tooltip_at(ctx, id, suggested_pos, add_contents)
}
@ -112,7 +110,7 @@ pub fn show_tooltip_for<R>(
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
let (above, position) = if ctx.input().any_touches() {
let (above, position) = if ctx.input(|i| i.any_touches()) {
(true, expanded_rect.left_top())
} else {
(false, expanded_rect.left_bottom())
@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
let mut frame_state =
ctx.frame_state()
.tooltip_state
ctx.frame_state(|fs| fs.tooltip_state)
.unwrap_or(crate::frame_state::TooltipFrameState {
common_id: individual_id,
rect: Rect::NOTHING,
@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
}
} else if let Some(position) = suggested_position {
position
} else if ctx.memory().everything_is_visible() {
} else if ctx.memory(|mem| mem.everything_is_visible()) {
Pos2::ZERO
} else {
return None; // No good place for a tooltip :(
@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
position.y -= expected_size.y;
}
position = position.at_most(ctx.input().screen_rect().max - expected_size);
position = position.at_most(ctx.screen_rect().max - expected_size);
// check if we intersect the avoid_rect
{
@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
}
}
let position = position.at_least(ctx.input().screen_rect().min);
let position = position.at_least(ctx.screen_rect().min);
let area_id = frame_state.common_id.with(frame_state.count);
@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state().tooltip_state = Some(frame_state);
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
Some(inner)
}
@ -283,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if *individual_id == tooltip_id {
let area_id = common_id.with(count);
let layer_id = AreaLayerId::new(Order::Tooltip, area_id);
if ctx.memory().areas.visible_last_frame(&layer_id) {
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
return true;
}
}
@ -314,6 +311,8 @@ pub fn popup_below_widget<R>(
///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
///
/// The opened popup will have the same width as the parent.
///
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
///
/// Returns `None` if the popup is not open.
@ -323,7 +322,7 @@ pub fn popup_below_widget<R>(
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() {
/// ui.memory().toggle_popup(popup_id);
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// }
/// let below = egui::AboveOrBelow::Below;
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
@ -340,7 +339,7 @@ pub fn popup_above_or_below_widget<R>(
above_or_below: AboveOrBelow,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
if ui.memory().is_popup_open(popup_id) {
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let (pos, pivot) = match above_or_below {
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
@ -368,8 +367,8 @@ pub fn popup_above_or_below_widget<R>(
})
.inner;
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
ui.memory().close_popup();
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
ui.memory_mut(|mem| mem.close_popup());
}
Some(inner)
} else {

View file

@ -18,11 +18,11 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -180,7 +180,7 @@ impl Resize {
.at_least(self.min_size)
.at_most(self.max_size)
.at_most(
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
);
State {
@ -305,7 +305,7 @@ impl Resize {
paint_resize_corner(ui, &corner_response);
if corner_response.hovered() || corner_response.dragged() {
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
}
}

View file

@ -48,11 +48,11 @@ impl Default for State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -97,8 +97,10 @@ pub struct ScrollArea {
id_source: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@ -141,6 +143,7 @@ impl ScrollArea {
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2],
}
}
@ -267,6 +270,18 @@ impl ScrollArea {
self
}
/// Can the user drag the scroll area to scroll?
///
/// This is useful for touch screens.
///
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self
}
/// For each axis, should the containing area shrink if the content is small?
///
/// * If `true`, egui will add blank space outside the scroll area.
@ -336,6 +351,7 @@ impl ScrollArea {
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
stick_to_end,
} = self;
@ -422,7 +438,9 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) {
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
@ -431,8 +449,10 @@ impl ScrollArea {
if content_response.dragged() {
for d in 0..2 {
if has_bar[d] {
state.offset[d] -= ui.input().pointer.delta()[d];
state.vel[d] = ui.input().pointer.velocity()[d];
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d];
});
state.scroll_stuck_to_end[d] = false;
} else {
state.vel[d] = 0.0;
@ -441,7 +461,7 @@ impl ScrollArea {
} else {
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input().unstable_dt;
let dt = ui.input(|i| i.unstable_dt);
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
@ -585,7 +605,9 @@ impl Prepared {
for d in 0..2 {
if has_bar[d] {
// We take the scroll target so only this ScrollArea will use it:
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if let Some((scroll, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
@ -650,8 +672,7 @@ impl Prepared {
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 {
if has_bar[d] {
let mut frame_state = ui.ctx().frame_state();
let scroll_delta = frame_state.scroll_delta;
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
@ -659,7 +680,7 @@ impl Prepared {
if scrolling_up || scrolling_down {
state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it.
frame_state.scroll_delta[d] = 0.0;
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
state.scroll_stuck_to_end[d] = false;
}
}

View file

@ -10,7 +10,7 @@ use super::*;
///
/// You can customize:
/// * title
/// * default, minimum, maximum and/or fixed size
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
/// * if the window has a scroll area (off by default)
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
/// * if there should be a close button (none by default)
@ -30,6 +30,7 @@ pub struct Window<'open> {
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
default_open: bool,
with_title_bar: bool,
}
@ -51,6 +52,7 @@ impl<'open> Window<'open> {
.default_size([340.0, 420.0]), // Default inner size of a window
scroll: ScrollArea::neither(),
collapsible: true,
default_open: true,
with_title_bar: true,
}
}
@ -77,6 +79,18 @@ impl<'open> Window<'open> {
self
}
/// If `false` the window will be non-interactive.
pub fn interactable(mut self, interactable: bool) -> Self {
self.area = self.area.interactable(interactable);
self
}
/// If `false` the window will be immovable.
pub fn movable(mut self, movable: bool) -> Self {
self.area = self.area.movable(movable);
self
}
/// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
// TODO(emilk): I'm not sure this is a good interface for this.
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
@ -162,6 +176,12 @@ impl<'open> Window<'open> {
self
}
/// Set initial collapsed state of the window
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
/// Set initial size of the window.
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.default_size(default_size);
@ -275,12 +295,14 @@ impl<'open> Window<'open> {
resize,
scroll,
collapsible,
default_open,
with_title_bar,
} = self;
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
area.show_open_close_animation(ctx, &frame, is_open);
if !is_open {
@ -291,7 +313,7 @@ impl<'open> Window<'open> {
let area_layer_id = area.layer();
let resize_id = area_id.with("resize");
let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
let is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
@ -318,7 +340,7 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
let style = ctx.style();
title.font_height(&ctx.fonts(), &style) + title_content_spacing
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
} else {
0.0
};
@ -404,7 +426,7 @@ impl<'open> Window<'open> {
ctx.style().visuals.widgets.active,
);
} else if let Some(hover_interaction) = hover_interaction {
if ctx.input().pointer.has_pointer() {
if ctx.input(|i| i.pointer.has_pointer()) {
paint_frame_interaction(
&mut area_content_ui,
outer_rect,
@ -499,13 +521,13 @@ pub(crate) struct WindowInteraction {
impl WindowInteraction {
pub fn set_cursor(&self, ctx: &Context) {
if (self.left && self.top) || (self.right && self.bottom) {
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
} else if (self.right && self.top) || (self.left && self.bottom) {
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
} else if self.left || self.right {
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
} else if self.bottom || self.top {
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
}
}
@ -537,7 +559,7 @@ fn interact(
}
}
ctx.memory().areas.move_to_top(area_layer_id);
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
Some(window_interaction)
}
@ -545,11 +567,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
window_interaction.set_cursor(ctx);
// Only move/resize windows with primary mouse button:
if !ctx.input().pointer.primary_down() {
if !ctx.input(|i| i.pointer.primary_down()) {
return None;
}
let pointer_pos = ctx.input().pointer.interact_pos()?;
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
let mut rect = window_interaction.start_rect; // prevent drift
if window_interaction.is_resize() {
@ -571,8 +593,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
// but we want anything interactive in the window (e.g. slider) to steal
// the drag from us. It is therefor important not to move the window the first frame,
// but instead let other widgets to the steal. HACK.
if !ctx.input().pointer.any_pressed() {
let press_origin = ctx.input().pointer.press_origin()?;
if !ctx.input(|i| i.pointer.any_pressed()) {
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
let delta = pointer_pos - press_origin;
rect = rect.translate(delta);
}
@ -590,30 +612,31 @@ fn window_interaction(
rect: Rect,
) -> Option<WindowInteraction> {
{
let drag_id = ctx.memory().interaction.drag_id;
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
if drag_id.is_some() && drag_id != Some(id) {
return None;
}
}
let mut window_interaction = { ctx.memory().window_interaction };
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
if window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx);
let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks
if any_pressed && ctx.input().pointer.primary_down() {
ctx.memory().interaction.drag_id = Some(id);
ctx.memory().interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction);
ctx.memory().window_interaction = window_interaction;
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
ctx.memory_mut(|mem| {
mem.interaction.drag_id = Some(id);
mem.interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction);
mem.window_interaction = window_interaction;
});
}
}
}
if let Some(window_interaction) = window_interaction {
let is_active = ctx.memory().interaction.drag_id == Some(id);
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
if is_active && window_interaction.area_layer_id == area_layer_id {
return Some(window_interaction);
@ -629,10 +652,9 @@ fn resize_hover(
area_layer_id: AreaLayerId,
rect: Rect,
) -> Option<WindowInteraction> {
let pointer = ctx.input().pointer.interact_pos()?;
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
let any_down = ctx.input().pointer.any_down(); // avoid deadlocks
if any_down && !ctx.input().pointer.any_pressed() {
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
return None; // already dragging (something)
}
@ -642,7 +664,7 @@ fn resize_hover(
}
}
if ctx.memory().interaction.drag_interest {
if ctx.memory(|mem| mem.interaction.drag_interest) {
// Another widget will become active if we drag here
return None;
}
@ -804,8 +826,8 @@ fn show_title_bar(
collapsible: bool,
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = title
.font_height(&ui.fonts(), ui.style())
let height = ui
.fonts(|fonts| title.font_height(fonts, ui.style()))
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);

File diff suppressed because it is too large Load diff

View file

@ -314,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
/// so those key presses are usually not reported to egui.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac).
@ -777,7 +777,7 @@ impl Key {
///
/// Can be used with [`crate::InputState::consume_shortcut`]
/// and [`crate::Context::format_shortcut`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct KeyboardShortcut {
pub modifiers: Modifiers,
pub key: Key,

View file

@ -70,7 +70,7 @@ pub struct PlatformOutput {
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output().copied_text = "some_text".to_string();
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
/// }
/// # });
/// ```

View file

@ -8,14 +8,14 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_temp(id)
ctx.data_mut(|d| d.get_temp(id))
}
pub fn store(self, ctx: &Context, id: Id) {
// We don't persist Grids, because
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
// B) if the code changes, the grid _should_ change, and not remember old sizes
ctx.data().insert_temp(id, self);
ctx.data_mut(|d| d.insert_temp(id, self));
}
fn set_min_col_width(&mut self, col: usize, width: f32) {

View file

@ -26,15 +26,15 @@ pub mod kb_shortcuts {
/// }
/// ```
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_RESET) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
if let Some(native_pixels_per_point) = native_pixels_per_point {
ctx.set_pixels_per_point(native_pixels_per_point);
}
} else {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
zoom_in(ctx);
}
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
zoom_out(ctx);
}
}

View file

@ -368,7 +368,7 @@ impl InputState {
/// # egui::__run_test_ui(|ui| {
/// let mut zoom = 1.0; // no zoom
/// let mut rotation = 0.0; // no rotation
/// let multi_touch = ui.input().multi_touch();
/// let multi_touch = ui.input(|i| i.multi_touch());
/// if let Some(multi_touch) = multi_touch {
/// zoom *= multi_touch.zoom_delta;
/// rotation += multi_touch.rotation_delta;
@ -447,7 +447,6 @@ impl InputState {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Click {
pub pos: Pos2,
pub button: PointerButton,
/// 1 or 2 (double-click) or 3 (triple-click)
pub count: u32,
/// Allows you to check for e.g. shift-click
@ -471,7 +470,10 @@ pub(crate) enum PointerEvent {
position: Pos2,
button: PointerButton,
},
Released(Option<Click>),
Released {
click: Option<Click>,
button: PointerButton,
},
}
impl PointerEvent {
@ -480,11 +482,11 @@ impl PointerEvent {
}
pub fn is_release(&self) -> bool {
matches!(self, PointerEvent::Released(_))
matches!(self, PointerEvent::Released { .. })
}
pub fn is_click(&self) -> bool {
matches!(self, PointerEvent::Released(Some(_click)))
matches!(self, PointerEvent::Released { click: Some(_), .. })
}
}
@ -639,7 +641,6 @@ impl PointerState {
Some(Click {
pos,
button,
count,
modifiers,
})
@ -647,7 +648,8 @@ impl PointerState {
None
};
self.pointer_events.push(PointerEvent::Released(click));
self.pointer_events
.push(PointerEvent::Released { click, button });
self.press_origin = None;
self.press_start_time = None;
@ -775,11 +777,28 @@ impl PointerState {
self.pointer_events.iter().any(|event| event.is_release())
}
/// Was the button given pressed this frame?
pub fn button_pressed(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b))
}
/// Was the button given released this frame?
pub fn button_released(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(event, &PointerEvent::Released(Some(Click{button: b, ..})) if button == b))
.any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b))
}
/// Was the primary button pressed this frame?
pub fn primary_pressed(&self) -> bool {
self.button_pressed(PointerButton::Primary)
}
/// Was the secondary button pressed this frame?
pub fn secondary_pressed(&self) -> bool {
self.button_pressed(PointerButton::Secondary)
}
/// Was the primary button released this frame?
@ -811,16 +830,28 @@ impl PointerState {
/// Was the button given double clicked this frame?
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
self.pointer_events.iter().any(|event| {
matches!(
&event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_double()
)
})
}
/// Was the button given triple clicked this frame?
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
self.pointer_events.iter().any(|event| {
matches!(
&event,
PointerEvent::Released {
click: Some(click),
button: b,
} if *b == button && click.is_triple()
)
})
}
/// Was the primary button clicked this frame?
@ -833,18 +864,6 @@ impl PointerState {
self.button_clicked(PointerButton::Secondary)
}
// /// Was this button pressed (`!down -> down`) this frame?
// /// This can sometimes return `true` even if `any_down() == false`
// /// because a press can be shorted than one frame.
// pub fn button_pressed(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_press())
// }
// /// Was this button released (`down -> !down`) this frame?
// pub fn button_released(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_release())
// }
/// Is this button currently down?
#[inline(always)]
pub fn button_down(&self, button: PointerButton) -> bool {

View file

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

View file

@ -363,7 +363,7 @@ pub use {
input_state::{InputState, MultiTouchInfo, PointerState},
layers::{AreaLayerId, Order},
layout::*,
memory::Memory,
memory::{Memory, Options},
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,

View file

@ -52,9 +52,10 @@ pub struct Memory {
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut ctx = egui::Context::default();
/// let mut memory = ctx.memory();
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ctx.memory_mut(|mem| {
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// });
/// ```
#[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage,
@ -405,7 +406,7 @@ impl Memory {
}
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&mut self, id: Id) -> bool {
pub fn has_lock_focus(&self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {

View file

@ -31,11 +31,11 @@ pub(crate) struct BarState {
impl BarState {
fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_temp(bar_id, self);
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
/// Show a menu at pointer if primary-clicked response.
@ -68,7 +68,7 @@ fn set_menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
@ -100,6 +100,20 @@ pub fn menu_button<R>(
stationary_menu_impl(ui, title, Box::new(add_contents))
}
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_image_button<R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
}
/// Construct a nested sub menu in another menu.
///
/// Opens on hover.
@ -129,9 +143,10 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.interactable(true)
.drag_bounds(Rect::EVERYTHING);
.drag_bounds(ctx.screen_rect());
let inner_response = area.show(ctx, |ui| {
set_menu_style(ui.style_mut());
@ -166,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.bg_fill);
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
@ -177,6 +192,25 @@ fn stationary_menu_impl<'c, R>(
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Build a top level menu with an image button.
///
/// Responds to primary clicks.
fn stationary_menu_image_impl<'c, R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(ImageButton::new(texture_id, image_size));
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub(crate) fn context_menu(
response: &Response,
@ -277,10 +311,9 @@ impl MenuRoot {
root: &mut MenuRootManager,
id: Id,
) -> MenuResponse {
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
let input = response.ctx.input();
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
if (response.clicked() && root.is_menu_open(id))
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
{
// menu open and button clicked or esc pressed
return MenuResponse::Close;
} else if (response.clicked() && !root.is_menu_open(id))
@ -288,12 +321,10 @@ impl MenuRoot {
{
// menu not open and button clicked
// or button hovered while other menu is open
drop(input);
let mut pos = response.rect.left_bottom();
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let screen_rect = response.ctx.input().screen_rect;
let screen_rect = response.ctx.input(|i| i.screen_rect);
if pos.y + menu_rect.height() > screen_rect.max.y {
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
@ -305,8 +336,11 @@ impl MenuRoot {
}
return MenuResponse::Create(pos, id);
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
if let Some(pos) = input.pointer.interact_pos() {
} else if response
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
{
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
if let Some(root) = root.inner.as_mut() {
if root.id == id {
// pressed somewhere while this menu is open
@ -329,26 +363,28 @@ impl MenuRoot {
id: Id,
) -> MenuResponse {
let response = response.interact(Sense::click());
let pointer = &response.ctx.input().pointer;
if pointer.any_pressed() {
if let Some(pos) = pointer.interact_pos() {
let mut destroy = false;
let mut in_old_menu = false;
if let Some(root) = root {
let menu_state = root.menu_state.read();
in_old_menu = menu_state.area_contains(pos);
destroy = root.id == response.id;
}
if !in_old_menu {
if response.hovered() && pointer.secondary_down() {
return MenuResponse::Create(pos, id);
} else if (response.hovered() && pointer.primary_down()) || destroy {
return MenuResponse::Close;
response.ctx.input(|input| {
let pointer = &input.pointer;
if pointer.any_pressed() {
if let Some(pos) = pointer.interact_pos() {
let mut destroy = false;
let mut in_old_menu = false;
if let Some(root) = root {
let menu_state = root.menu_state.read();
in_old_menu = menu_state.area_contains(pos);
destroy = root.id == response.id;
}
if !in_old_menu {
if response.hovered() && pointer.secondary_down() {
return MenuResponse::Create(pos, id);
} else if (response.hovered() && pointer.primary_down()) || destroy {
return MenuResponse::Close;
}
}
}
}
}
MenuResponse::Stay
MenuResponse::Stay
})
}
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
@ -410,7 +446,7 @@ impl SubMenuButton {
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) {
&ui.style().visuals.widgets.hovered
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
@ -439,7 +475,8 @@ impl SubMenuButton {
text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let desired_size = text_and_icon_size + 2.0 * button_padding;
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
@ -459,7 +496,7 @@ impl SubMenuButton {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.rounding,
visuals.bg_fill,
visuals.weak_bg_fill,
);
}
@ -571,15 +608,15 @@ impl MenuState {
/// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
let pointer = &ui.input().pointer.clone();
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(pointer) {
if self.moving_towards_current_submenu(&pointer) {
// ensure to repaint once even when pointer is not moving
ui.ctx().request_repaint();
} else if !open && button.hovered() {
let pos = button.rect.right_top();
self.open_submenu(sub_id, pos);
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
self.close_submenu();
}
}

View file

@ -7,7 +7,6 @@ use crate::{
AreaLayerId, Color32, Context, FontId,
};
use epaint::{
mutex::{RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke,
};
@ -128,10 +127,12 @@ impl Painter {
&self.ctx
}
/// Available fonts.
/// Read-only access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
#[inline(always)]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx.fonts()
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx.fonts(reader)
}
/// Where we paint
@ -186,8 +187,9 @@ impl Painter {
/// ## Low level
impl Painter {
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer.area_layer))
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
self.ctx
.graphics_mut(|g| writer(g.list(self.layer.area_layer)))
}
fn transform_shape(&self, shape: &mut Shape) {
@ -197,17 +199,15 @@ impl Painter {
}
fn add_to_paint_list(&self, shape: Shape) -> ShapeIdx {
self.paint_list()
.add_at_z(self.clip_rect, shape, self.layer.z)
self.paint_list(|l| l.add_at_z(self.clip_rect, shape, self.layer.z))
}
fn extend_paint_list(&self, shapes: impl IntoIterator<Item = Shape>) {
self.paint_list()
.extend_at_z(self.clip_rect, shapes, self.layer.z);
self.paint_list(|l| l.extend_at_z(self.clip_rect, shapes, self.layer.z));
}
fn set_shape_in_paint_list(&self, idx: ShapeIdx, shape: Shape) {
self.paint_list().set(idx, self.clip_rect, shape);
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
}
/// It is up to the caller to make sure there is room for this.
@ -453,7 +453,7 @@ impl Painter {
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, wrap_width)
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
}
/// Will line break at `\n`.
@ -466,7 +466,7 @@ impl Painter {
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, f32::INFINITY)
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
}
/// Paint text that has already been layed out in a [`Galley`].

View file

@ -173,23 +173,25 @@ impl Response {
// We do not use self.clicked(), because we want to catch all clicks within our frame,
// even if we aren't clickable (or even enabled).
// This is important for windows and such that should close then the user clicks elsewhere.
let pointer = &self.ctx.input().pointer;
self.ctx.input(|i| {
let pointer = &i.pointer;
if pointer.any_click() {
// We detect clicks/hover on a "interact_rect" that is slightly larger than
// self.rect. See Context::interact.
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
// hence the extra complexity here.
if self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
if pointer.any_click() {
// We detect clicks/hover on a "interact_rect" that is slightly larger than
// self.rect. See Context::interact.
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
// hence the extra complexity here.
if self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
} else {
false // clicked without a pointer, weird
}
} else {
false // clicked without a pointer, weird
false
}
} else {
false
}
})
}
/// Was the widget enabled?
@ -217,14 +219,12 @@ impl Response {
/// also has the keyboard focus. That makes this function suitable
/// for style choices, e.g. a thicker border around focused widgets.
pub fn has_focus(&self) -> bool {
// Access input and memory in separate statements to prevent deadlock.
let has_global_focus = self.ctx.input().raw.has_focus;
has_global_focus && self.ctx.memory().has_focus(self.id)
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
}
/// True if this widget has keyboard focus this frame, but didn't last frame.
pub fn gained_focus(&self) -> bool {
self.ctx.memory().gained_focus(self.id)
self.ctx.memory(|mem| mem.gained_focus(self.id))
}
/// The widget had keyboard focus and lost it,
@ -236,29 +236,29 @@ impl Response {
/// # let mut my_text = String::new();
/// # fn do_request(_: &str) {}
/// let response = ui.text_edit_singleline(&mut my_text);
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
/// do_request(&my_text);
/// }
/// # });
/// ```
pub fn lost_focus(&self) -> bool {
self.ctx.memory().lost_focus(self.id)
self.ctx.memory(|mem| mem.lost_focus(self.id))
}
/// Request that this widget get keyboard focus.
pub fn request_focus(&self) {
self.ctx.memory().request_focus(self.id);
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
}
/// Surrender keyboard focus for this widget.
pub fn surrender_focus(&self) {
self.ctx.memory().surrender_focus(self.id);
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
}
/// The widgets is being dragged.
///
/// To find out which button(s), query [`crate::PointerState::button_down`]
/// (`ui.input().pointer.button_down(…)`).
/// (`ui.input(|i| i.pointer.button_down(…))`).
///
/// Note that the widget must be sensing drags with [`Sense::drag`].
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
@ -270,12 +270,17 @@ impl Response {
}
pub fn dragged_by(&self, button: PointerButton) -> bool {
self.dragged() && self.ctx.input().pointer.button_down(button)
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
}
/// Did a drag on this widgets begin this frame?
pub fn drag_started(&self) -> bool {
self.dragged && self.ctx.input().pointer.any_pressed()
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
}
/// Did a drag on this widgets by the button begin this frame?
pub fn drag_started_by(&self, button: PointerButton) -> bool {
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
}
/// The widget was being dragged, but now it has been released.
@ -283,10 +288,15 @@ impl Response {
self.drag_released
}
/// The widget was being dragged by the button, but now it has been released.
pub fn drag_released_by(&self, button: PointerButton) -> bool {
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// If dragged, how many points were we dragged and in what direction?
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
self.ctx.input().pointer.delta()
self.ctx.input(|i| i.pointer.delta())
} else {
Vec2::ZERO
}
@ -302,7 +312,7 @@ impl Response {
/// None if the pointer is outside the response area.
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
self.ctx.input().pointer.hover_pos()
self.ctx.input(|i| i.pointer.hover_pos())
} else {
None
}
@ -392,11 +402,11 @@ impl Response {
}
fn should_show_hover_ui(&self) -> bool {
if self.ctx.memory().everything_is_visible() {
if self.ctx.memory(|mem| mem.everything_is_visible()) {
return true;
}
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false;
}
@ -404,8 +414,7 @@ impl Response {
// We only show the tooltip when the mouse pointer is still,
// but once shown we keep showing it until the mouse leaves the parent.
let is_pointer_still = self.ctx.input().pointer.is_still();
if !is_pointer_still && !self.is_tooltip_open() {
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
// wait for mouse to stop
self.ctx.request_repaint();
return false;
@ -414,8 +423,9 @@ impl Response {
// We don't want tooltips of things while we are dragging them,
// but we do want tooltips while holding down on an item on a touch screen.
if self.ctx.input().pointer.any_down()
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
if self
.ctx
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
{
return false;
}
@ -454,7 +464,15 @@ impl Response {
/// When hovered, use this icon for the mouse cursor.
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() {
self.ctx.output().cursor_icon = cursor;
self.ctx.set_cursor_icon(cursor);
}
self
}
/// When hovered or dragged, use this icon for the mouse cursor.
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() || self.dragged() {
self.ctx.set_cursor_icon(cursor);
}
self
}
@ -503,8 +521,10 @@ impl Response {
/// # });
/// ```
pub fn scroll_to_me(&self, align: Option<Align>) {
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
self.ctx.frame_state_mut(|state| {
state.scroll_target[0] = Some((self.rect.x_range(), align));
state.scroll_target[1] = Some((self.rect.y_range(), align));
});
}
/// For accessibility.
@ -529,18 +549,18 @@ impl Response {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(&mut node, make_info());
}
self.ctx.accesskit_node(self.id, |node| {
self.fill_accesskit_node_from_widget_info(node, make_info());
});
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(&mut node, event.widget_info().clone());
}
self.ctx.output().events.push(event);
self.ctx.accesskit_node(self.id, |node| {
self.fill_accesskit_node_from_widget_info(node, event.widget_info().clone());
});
self.ctx.output_mut(|o| o.events.push(event));
}
#[cfg(feature = "accesskit")]
@ -618,9 +638,8 @@ impl Response {
/// ```
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
node.labelled_by.push(id.accesskit_id());
}
self.ctx
.accesskit_node(self.id, |node| node.labelled_by.push(id.accesskit_id()));
#[cfg(not(feature = "accesskit"))]
{
let _ = id;

View file

@ -216,6 +216,7 @@ impl Style {
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
let mut visuals = *self.visuals.widgets.style(response);
if selected {
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
visuals.bg_fill = self.visuals.selection.bg_fill;
// visuals.bg_stroke = self.visuals.selection.stroke;
visuals.fg_stroke = self.visuals.selection.stroke;
@ -264,8 +265,11 @@ pub struct Spacing {
/// Anything clickable should be (at least) this size.
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
pub slider_width: f32, // TODO(emilk): rename big_interact_size ?
/// Default width of a [`Slider`].
pub slider_width: f32,
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
pub combo_width: f32,
/// Default width of a [`TextEdit`].
pub text_edit_width: f32,
@ -533,7 +537,7 @@ impl Visuals {
// TODO(emilk): replace with an alpha
#[inline(always)]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.bg_fill
self.widgets.noninteractive.weak_bg_fill
}
/// Returned a "grayed out" version of the given color.
@ -594,9 +598,17 @@ impl Widgets {
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetVisuals {
/// Background color of widget.
/// Background color of widgets that must have a background fill,
/// such as the slider background, a checkbox background, or a radio button background.
///
/// Must never be [`Color32::TRANSPARENT`].
pub bg_fill: Color32,
/// Background color of widgets that can _optionally_ have a background fill, such as buttons.
///
/// May be [`Color32::TRANSPARENT`].
pub weak_bg_fill: Color32,
/// For surrounding rectangle of things that need it,
/// like buttons, the box of the checkbox, etc.
/// Should maybe be called `frame_stroke`.
@ -684,6 +696,7 @@ impl Default for Spacing {
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
interact_size: vec2(40.0, 18.0),
slider_width: 100.0,
combo_width: 100.0,
text_edit_width: 280.0,
icon_width: 14.0,
icon_width_inner: 8.0,
@ -717,8 +730,8 @@ impl Visuals {
widgets: Widgets::default(),
selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
faint_bg_color: Color32::from_gray(35),
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
code_bg_color: Color32::from_gray(64),
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
error_fg_color: Color32::from_rgb(255, 0, 0), // red
@ -751,8 +764,8 @@ impl Visuals {
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
faint_bg_color: Color32::from_gray(242),
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
code_bg_color: Color32::from_gray(230),
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
error_fg_color: Color32::from_rgb(255, 0, 0), // red
@ -801,6 +814,7 @@ impl Widgets {
pub fn dark() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
@ -808,13 +822,15 @@ impl Widgets {
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(60), // button background
weak_bg_fill: Color32::from_gray(60), // button background
bg_fill: Color32::from_gray(60), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(70),
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
@ -822,6 +838,7 @@ impl Widgets {
expansion: 1.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(55),
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
@ -829,6 +846,7 @@ impl Widgets {
expansion: 1.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
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)),
@ -841,6 +859,7 @@ impl Widgets {
pub fn light() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(248),
bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
@ -848,13 +867,15 @@ impl Widgets {
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(230), // button background
weak_bg_fill: Color32::from_gray(230), // button background
bg_fill: Color32::from_gray(230), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
rounding: Rounding::same(2.0),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::BLACK),
@ -862,6 +883,7 @@ impl Widgets {
expansion: 1.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(165),
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
@ -869,6 +891,7 @@ impl Widgets {
expansion: 1.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
@ -984,6 +1007,7 @@ impl Spacing {
indent,
interact_size,
slider_width,
combo_width,
text_edit_width,
icon_width,
icon_width_inner,
@ -1012,6 +1036,10 @@ impl Spacing {
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
ui.label("Slider width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
ui.label("ComboBox width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
ui.label("TextEdit width");
@ -1185,13 +1213,17 @@ impl Selection {
impl WidgetVisuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
bg_fill,
weak_bg_fill,
bg_fill: mandatory_bg_fill,
bg_stroke,
rounding,
fg_stroke,
expansion,
} = self;
ui_color(ui, bg_fill, "background fill");
ui_color(ui, weak_bg_fill, "optional background fill")
.on_hover_text("For buttons, combo-boxes, etc");
ui_color(ui, mandatory_bg_fill, "mandatory background fill")
.on_hover_text("For checkboxes, sliders, etc");
stroke_ui(ui, bg_stroke, "background stroke");
rounding_ui(ui, rounding);
@ -1270,7 +1302,7 @@ impl Visuals {
} = self;
ui.collapsing("Background Colors", |ui| {
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(

View file

@ -3,11 +3,11 @@
use std::hash::Hash;
use std::sync::Arc;
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use epaint::mutex::RwLock;
use crate::{
containers::*, ecolor::*, epaint::text::Fonts, layers::ZLayer, layout::*, menu::MenuState,
placer::Placer, widgets::*, *,
placer::Placer, util::IdTypeMap, widgets::*, *,
};
// ----------------------------------------------------------------------------
@ -325,84 +325,9 @@ impl Ui {
self.painter().zlayer()
}
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
/// // This is fine!
/// }
///
/// let pos = ui.input().pointer.hover_pos();
/// if let Some(pos) = pos {
/// // This is also fine!
/// }
///
/// if let Some(pos) = ui.input().pointer.hover_pos() {
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
/// }
/// # });
/// ```
#[inline]
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
self.ctx().input()
}
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
/// Equivalent to `.ctx().input_mut()`.
///
/// Note that this locks the [`Context`], so be careful with if-let bindings
/// like for [`Self::input()`].
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
/// # });
/// ```
#[inline]
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
self.ctx().input_mut()
}
/// The [`Memory`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().memory()`.
#[inline]
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
self.ctx().memory()
}
/// Stores superficial widget state.
#[inline]
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
self.ctx().data()
}
/// The [`PlatformOutput`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().output()`.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output().copied_text = "some_text".to_string();
/// }
/// # });
#[inline]
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
self.ctx().output()
}
/// The [`Fonts`] of the [`Context`] associated with this ui.
/// Equivalent to `.ctx().fonts()`.
#[inline]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx().fonts()
}
/// The height of text of this text style
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts().row_height(&style.resolve(self.style()))
self.fonts(|f| f.row_height(&style.resolve(self.style())))
}
/// Screen-space rectangle for clipping what we paint in this ui.
@ -424,6 +349,87 @@ impl Ui {
}
}
/// # Helpers for accessing the underlying [`Context`].
/// These functions all lock the [`Context`] owned by this [`Ui`].
/// Please see the documentation of [`Context`] for how locking works!
impl Ui {
/// Read-only access to the shared [`InputState`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.input(|i| i.key_pressed(egui::Key::A)) {
/// // …
/// }
/// # });
/// ```
#[inline]
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
self.ctx().input(reader)
}
/// Read-write access to the shared [`InputState`].
#[inline]
pub fn input_mut<R>(&self, writer: impl FnOnce(&mut InputState) -> R) -> R {
self.ctx().input_mut(writer)
}
/// Read-only access to the shared [`Memory`].
#[inline]
pub fn memory<R>(&self, reader: impl FnOnce(&Memory) -> R) -> R {
self.ctx().memory(reader)
}
/// Read-write access to the shared [`Memory`].
#[inline]
pub fn memory_mut<R>(&self, writer: impl FnOnce(&mut Memory) -> R) -> R {
self.ctx().memory_mut(writer)
}
/// Read-only access to the shared [`IdTypeMap`], which stores superficial widget state.
#[inline]
pub fn data<R>(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R {
self.ctx().data(reader)
}
/// Read-write access to the shared [`IdTypeMap`], which stores superficial widget state.
#[inline]
pub fn data_mut<R>(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R {
self.ctx().data_mut(writer)
}
/// Read-only access to the shared [`PlatformOutput`].
///
/// This is what egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
/// ```
#[inline]
pub fn output<R>(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R {
self.ctx().output(reader)
}
/// Read-write access to the shared [`PlatformOutput`].
///
/// This is what egui outputs each frame.
///
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
/// ```
#[inline]
pub fn output_mut<R>(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R {
self.ctx().output_mut(writer)
}
/// Read-only access to [`Fonts`].
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx().fonts(reader)
}
}
// ------------------------------------------------------------------------
/// # Sizes etc
@ -972,7 +978,8 @@ impl Ui {
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 {
let range = rect.min[d]..=rect.max[d];
self.ctx().frame_state().scroll_target[d] = Some((range, align));
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
}
}
@ -1001,7 +1008,8 @@ impl Ui {
let target = self.next_widget_position();
for d in 0..2 {
let target = target[d];
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
}
}
@ -1033,7 +1041,8 @@ impl Ui {
/// # });
/// ```
pub fn scroll_with_delta(&self, delta: Vec2) {
self.ctx().frame_state().scroll_delta += delta;
self.ctx()
.frame_state_mut(|state| state.scroll_delta += delta);
}
}
@ -2195,6 +2204,43 @@ impl Ui {
menu::menu_button(self, title, add_contents)
}
}
#[inline]
/// Create a menu button with an image that when clicked will show the given menu.
///
/// If called from within a menu this will instead create a button for a sub-menu.
///
/// ```ignore
/// use egui_extras;
///
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
/// "rss",
/// include_bytes!("rss.svg"),
/// egui_extras::image::FitTo::Size(24, 24),
/// );
///
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
/// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() {
/// ui.close_menu();
/// }
/// });
/// });
/// ```
///
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
pub fn menu_image_button<R>(
&mut self,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() {
menu::submenu_button(self, menu_state, String::new(), add_contents)
} else {
menu::menu_image_button(self, texture_id, image_size, add_contents)
}
}
}
// ----------------------------------------------------------------------------

View file

@ -460,18 +460,18 @@ impl IdTypeMap {
}
#[inline]
pub fn is_empty(&mut self) -> bool {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn len(&mut self) -> usize {
pub fn len(&self) -> usize {
self.0.len()
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&mut self) -> usize {
pub fn count_serialized(&self) -> usize {
self.0
.values()
.filter(|e| matches!(e, Element::Serialized { .. }))
@ -479,7 +479,7 @@ impl IdTypeMap {
}
/// Count the number of values are stored with the given type.
pub fn count<T: 'static>(&mut self) -> usize {
pub fn count<T: 'static>(&self) -> usize {
let key = TypeId::of::<T>();
self.0
.iter()

View file

@ -573,14 +573,14 @@ impl WidgetText {
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(text_job.job),
galley: ui.fonts(|f| f.layout_job(text_job.job)),
galley_has_color: text_job.job_has_color,
}
}
Self::LayoutJob(mut job) => {
job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(job),
galley: ui.fonts(|f| f.layout_job(job)),
galley_has_color: true,
}
}

View file

@ -30,6 +30,7 @@ pub struct Button {
small: bool,
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>,
}
@ -45,6 +46,7 @@ impl Button {
small: false,
frame: None,
min_size: Vec2::ZERO,
rounding: None,
image: None,
}
}
@ -117,6 +119,12 @@ impl Button {
self
}
/// Set the rounding of the button.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = Some(rounding.into());
self
}
/// Show some text on the right side of the button, in weak color.
///
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
@ -140,6 +148,7 @@ impl Widget for Button {
small,
frame,
min_size,
rounding,
image,
} = self;
@ -171,10 +180,10 @@ impl Widget for Button {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
desired_size.y = desired_size.y.max(shortcut_text.size().y);
}
desired_size += 2.0 * button_padding;
if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
}
desired_size += 2.0 * button_padding;
desired_size = desired_size.at_least(min_size);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
@ -184,14 +193,11 @@ impl Widget for Button {
let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.bg_fill);
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
ui.painter().rect(
rect.expand(visuals.expansion),
visuals.rounding,
fill,
stroke,
);
let rounding = rounding.unwrap_or(visuals.rounding);
ui.painter()
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if let Some(image) = image {
@ -221,7 +227,10 @@ impl Widget for Button {
if let Some(image) = image {
let image_rect = Rect::from_min_size(
pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)),
pos2(
rect.min.x + button_padding.x,
rect.center().y - 0.5 - (image.size().y / 2.0),
),
image.size(),
);
image.paint_at(ui, image_rect);
@ -531,7 +540,7 @@ impl Widget for ImageButton {
(
expansion,
visuals.rounding,
visuals.bg_fill,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {

View file

@ -228,9 +228,9 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
if ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque {
ui.output().copied_text = format!("{}, {}, {}", r, g, b);
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
} else {
ui.output().copied_text = format!("{}, {}, {}, {}", r, g, b, a);
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
}
}
@ -341,20 +341,20 @@ 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 popup_id = ui.auto_id_with("popup");
let open = ui.memory().is_popup_open(popup_id);
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");
}
if button_response.clicked() {
ui.memory().toggle_popup(popup_id);
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
const COLOR_SLIDER_WIDTH: f32 = 210.0;
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
if ui.memory().is_popup_open(popup_id) {
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let area_response = Area::new(popup_id)
.order(Order::Foreground)
.fixed_pos(button_response.rect.max)
@ -370,9 +370,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
.response;
if !button_response.clicked()
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
ui.memory().close_popup();
ui.memory_mut(|mem| mem.close_popup());
}
}
@ -436,5 +436,5 @@ fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
f(ctx.data().get_temp_mut_or_default(Id::null()))
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
}

View file

@ -368,33 +368,35 @@ impl<'a> Widget for DragValue<'a> {
custom_parser,
} = self;
let shift = ui.input().modifiers.shift_only();
let shift = ui.input(|i| i.modifiers.shift_only());
// The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id();
let is_slow_speed = shift && ui.memory().is_being_dragged(id);
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
// The following call ensures that when a `DragValue` receives focus,
// The following ensures that when a `DragValue` receives focus,
// it is immediately rendered in edit mode, rather than being rendered
// in button mode for just one frame. This is important for
// screen readers.
ui.memory().interested_in_focus(id);
let is_kb_editing = ui.memory().has_focus(id);
if ui.memory().gained_focus(id) {
ui.memory().drag_value.edit_string = None;
}
let is_kb_editing = ui.memory_mut(|mem| {
mem.interested_in_focus(id);
let is_kb_editing = mem.has_focus(id);
if mem.gained_focus(id) {
mem.drag_value.edit_string = None;
}
is_kb_editing
});
let old_value = get(&mut get_set_value);
let mut value = old_value;
let aim_rad = ui.input().aim_radius() as f64;
let aim_rad = ui.input(|i| i.aim_radius() as f64);
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
let auto_decimals = auto_decimals + is_slow_speed as usize;
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
let change = {
let change = ui.input_mut(|input| {
let mut change = 0.0;
let mut input = ui.input_mut();
if is_kb_editing {
// This deliberately doesn't listen for left and right arrow keys,
@ -418,16 +420,18 @@ impl<'a> Widget for DragValue<'a> {
}
change
};
});
#[cfg(feature = "accesskit")]
{
use accesskit::{Action, ActionData};
for request in ui.input().accesskit_action_requests(id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
value = new_value;
ui.input(|input| {
for request in input.accesskit_action_requests(id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
value = new_value;
}
}
}
});
}
if change != 0.0 {
@ -438,7 +442,7 @@ impl<'a> Widget for DragValue<'a> {
value = clamp_to_range(value, clamp_range.clone());
if old_value != value {
set(&mut get_set_value, value);
ui.memory().drag_value.edit_string = None;
ui.memory_mut(|mem| mem.drag_value.edit_string = None);
}
let value_text = match custom_formatter {
@ -457,10 +461,7 @@ impl<'a> Widget for DragValue<'a> {
let mut response = if is_kb_editing {
let button_width = ui.spacing().interact_size.x;
let mut value_text = ui
.memory()
.drag_value
.edit_string
.take()
.memory_mut(|mem| mem.drag_value.edit_string.take())
.unwrap_or_else(|| value_text.clone());
let response = ui.add(
TextEdit::singleline(&mut value_text)
@ -476,7 +477,7 @@ impl<'a> Widget for DragValue<'a> {
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
set(&mut get_set_value, parsed_value);
}
ui.memory().drag_value.edit_string = Some(value_text);
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text));
response
} else {
let button = Button::new(
@ -499,10 +500,12 @@ impl<'a> Widget for DragValue<'a> {
}
if response.clicked() {
ui.memory().drag_value.edit_string = None;
ui.memory().request_focus(id);
ui.memory_mut(|mem| {
mem.drag_value.edit_string = None;
mem.request_focus(id);
});
} else if response.dragged() {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
let mdelta = response.drag_delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
@ -512,7 +515,7 @@ impl<'a> Widget for DragValue<'a> {
let delta_value = delta_points as f64 * speed;
if delta_value != 0.0 {
let mut drag_state = std::mem::take(&mut ui.memory().drag_value);
let mut drag_state = ui.memory_mut(|mem| std::mem::take(&mut mem.drag_value));
// Since we round the value being dragged, we need to store the full precision value in memory:
let stored_value = (drag_state.last_dragged_id == Some(response.id))
@ -533,7 +536,7 @@ impl<'a> Widget for DragValue<'a> {
drag_state.last_dragged_id = Some(response.id);
drag_state.last_dragged_value = Some(stored_value);
ui.memory().drag_value = drag_state;
ui.memory_mut(|mem| mem.drag_value = drag_state);
}
}
@ -545,7 +548,7 @@ impl<'a> Widget for DragValue<'a> {
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
ui.ctx().accesskit_node(response.id, |node| {
use accesskit::Action;
// If either end of the range is unbounded, it's better
// to leave the corresponding AccessKit field set to None,
@ -589,7 +592,7 @@ impl<'a> Widget for DragValue<'a> {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
node.value = Some(value_text.into());
}
}
});
response
}

View file

@ -38,7 +38,7 @@ impl Widget for Link {
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
}
if ui.is_rect_visible(response.rect) {
@ -110,16 +110,20 @@ impl Widget for Hyperlink {
let response = ui.add(Link::new(text));
if response.clicked() {
let modifiers = ui.ctx().input().modifiers;
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: modifiers.any(),
let modifiers = ui.ctx().input(|i| i.modifiers);
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: modifiers.any(),
});
});
}
if response.middle_clicked() {
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: true,
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: true,
});
});
}
response.on_hover_text(url)

View file

@ -72,7 +72,7 @@ impl Label {
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let sense = self.sense.unwrap_or_else(|| {
// We only want to focus labels if the screen reader is on.
if ui.memory().options.screen_reader {
if ui.memory(|mem| mem.options.screen_reader) {
Sense::focusable_noninteractive()
} else {
Sense::hover()
@ -120,7 +120,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 = ui.fonts(|f| text_job.into_galley(f));
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(
@ -153,7 +153,7 @@ impl Label {
text_job.job.justify = ui.layout().horizontal_justify();
};
let text_galley = text_job.into_galley(&ui.fonts());
let text_galley = ui.fonts(|f| text_job.into_galley(f));
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(),

View file

@ -185,7 +185,11 @@ impl RectElement for Bar {
fn default_values_format(&self, transform: &ScreenTransform) -> String {
let scale = transform.dvalue_dpos();
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
format!("\n{:.*}", y_decimals, self.value)
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
};
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
crate::plot::format_number(self.value, decimals)
}
}

View file

@ -269,9 +269,15 @@ impl RectElement for BoxElem {
fn default_values_format(&self, transform: &ScreenTransform) -> String {
let scale = transform.dvalue_dpos();
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
};
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
.at_most(6)
.at_least(1);
format!(
"\nMax = {max:.decimals$}\
"Max = {max:.decimals$}\
\nQuartile 3 = {q3:.decimals$}\
\nMedian = {med:.decimals$}\
\nQuartile 1 = {q1:.decimals$}\

View file

@ -34,6 +34,7 @@ pub(super) struct PlotConfig<'a> {
pub(super) trait PlotItem {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
/// For plot-items which are generated based on x values (plotting functions).
fn initialize(&mut self, x_range: RangeInclusive<f64>);
fn name(&self) -> &str;
@ -1647,6 +1648,7 @@ fn add_rulers_and_text(
let mut text = elem.name().to_owned(); // could be empty
if show_values {
text.push('\n');
text.push_str(&elem.default_values_format(plot.transform));
}
@ -1656,14 +1658,16 @@ fn add_rulers_and_text(
let font_id = TextStyle::Body.resolve(plot.ui.style());
let corner_value = elem.corner_value();
shapes.push(Shape::text(
&plot.ui.fonts(),
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
plot.ui.fonts(|f| {
shapes.push(Shape::text(
f,
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
});
}
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
@ -1693,8 +1697,8 @@ pub(super) fn rulers_at_value(
let text = {
let scale = plot.transform.dvalue_dpos();
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
if let Some(custom_label) = label_formatter {
custom_label(name, &value)
} else if plot.show_x && plot.show_y {
@ -1712,15 +1716,16 @@ pub(super) fn rulers_at_value(
};
let font_id = TextStyle::Body.resolve(plot.ui.style());
shapes.push(Shape::text(
&plot.ui.fonts(),
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
plot.ui.fonts(|f| {
shapes.push(Shape::text(
f,
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
});
}
fn find_closest_rect<'a, T>(

View file

@ -89,9 +89,7 @@ impl LegendEntry {
let font_id = text_style.resolve(ui.style());
let galley = ui
.fonts()
.layout_delayed_color(text, font_id, f32::INFINITY);
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
let icon_size = galley.size().y;
let icon_spacing = icon_size / 5.0;

View file

@ -108,11 +108,11 @@ struct PlotMemory {
impl PlotMemory {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
@ -291,8 +291,10 @@ pub struct Plot {
legend_config: Option<Legend>,
show_background: bool,
show_axes: [bool; 2],
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl Plot {
@ -331,8 +333,10 @@ impl Plot {
legend_config: None,
show_background: true,
show_axes: [true; 2],
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
clamp_grid: false,
}
}
@ -557,6 +561,14 @@ impl Plot {
self
}
/// Clamp the grid to only be visible at the range of data where we have values.
///
/// Default: `false`.
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
self.clamp_grid = clamp_grid;
self
}
/// Expand bounds to include the given x value.
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
@ -671,6 +683,8 @@ impl Plot {
show_axes,
linked_axes,
linked_cursors,
clamp_grid,
grid_spacers,
sharp_grid_lines,
} = self;
@ -939,9 +953,9 @@ impl Plot {
if let Some(hover_pos) = response.hover_pos() {
if allow_zoom {
let zoom_factor = if data_aspect.is_some() {
Vec2::splat(ui.input().zoom_delta())
Vec2::splat(ui.input(|i| i.zoom_delta()))
} else {
ui.input().zoom_delta_2d()
ui.input(|i| i.zoom_delta_2d())
};
if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos);
@ -949,7 +963,7 @@ impl Plot {
}
}
if allow_scroll {
let scroll_delta = ui.input().scroll_delta;
let scroll_delta = ui.input(|i| i.scroll_delta);
if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta);
bounds_modified = true.into();
@ -971,11 +985,12 @@ impl Plot {
axis_formatters,
show_axes,
transform: transform.clone(),
grid_spacers,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
draw_cursors,
grid_spacers,
sharp_grid_lines,
clamp_grid,
};
let plot_cursors = prepared.ui(ui, &response);
@ -1079,7 +1094,7 @@ impl PlotUi {
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta();
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
let value = self.plot_from_screen(last_pos);
Some(value)
}
@ -1297,11 +1312,13 @@ struct PreparedPlot {
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl PreparedPlot {
@ -1400,10 +1417,13 @@ impl PreparedPlot {
shapes: &mut Vec<(Shape, f32)>,
sharp_grid_lines: bool,
) {
#![allow(clippy::collapsible_else_if)]
let Self {
transform,
axis_formatters,
grid_spacers,
clamp_grid,
..
} = self;
@ -1417,7 +1437,6 @@ impl PreparedPlot {
let font_id = TextStyle::Body.resolve(ui.style());
// Where on the cross-dimension to show the label values
let bounds = transform.bounds();
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
let input = GridInput {
@ -1426,9 +1445,31 @@ impl PreparedPlot {
};
let steps = (grid_spacers[axis])(input);
let clamp_range = clamp_grid.then(|| {
let mut tight_bounds = PlotBounds::NOTHING;
for item in &self.items {
let item_bounds = item.bounds();
tight_bounds.merge_x(&item_bounds);
tight_bounds.merge_y(&item_bounds);
}
tight_bounds
});
for step in steps {
let value_main = step.value;
if let Some(clamp_range) = clamp_range {
if axis == 0 {
if !clamp_range.range_x().contains(&value_main) {
continue;
};
} else {
if !clamp_range.range_y().contains(&value_main) {
continue;
};
}
}
let value = if axis == 0 {
PlotPoint::new(value_main, value_cross)
} else {
@ -1451,11 +1492,23 @@ impl PreparedPlot {
let mut p1 = pos_in_gui;
p0[1 - axis] = transform.frame().min[1 - axis];
p1[1 - axis] = transform.frame().max[1 - axis];
if let Some(clamp_range) = clamp_range {
if axis == 0 {
p0.y = transform.position_from_point_y(clamp_range.min[1]);
p1.y = transform.position_from_point_y(clamp_range.max[1]);
} else {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
}
if sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.ctx().round_pos_to_pixels(p0);
p1 = ui.ctx().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
@ -1594,3 +1647,16 @@ fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64,
});
out.extend(marks_iter);
}
/// Helper for formatting a number so that we always show at least a few decimals,
/// unless it is an integer, in which case we never show any decimals.
pub fn format_number(number: f64, num_decimals: usize) -> String {
let is_integral = number as i64 as f64 == number;
if is_integral {
// perfect integer - show it as such:
format!("{:.0}", number)
} else {
// make sure we tell the user it is not an integer by always showing a decimal or two:
format!("{:.*}", num_decimals.at_least(1), number)
}
}

View file

@ -224,6 +224,7 @@ impl ScreenTransform {
}
}
/// ui-space rectangle.
pub fn frame(&self) -> &Rect {
&self.frame
}
@ -263,18 +264,27 @@ impl ScreenTransform {
}
}
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
let x = remap(
value.x,
pub fn position_from_point_x(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[0]..=self.bounds.max[0],
(self.frame.left() as f64)..=(self.frame.right() as f64),
);
let y = remap(
value.y,
) as f32
}
pub fn position_from_point_y(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[1]..=self.bounds.max[1],
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
);
pos2(x as f32, y as f32)
) as f32
}
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
pos2(
self.position_from_point_x(value.x),
self.position_from_point_y(value.y),
)
}
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {

View file

@ -13,6 +13,7 @@ pub struct ProgressBar {
progress: f32,
desired_width: Option<f32>,
text: Option<ProgressBarText>,
fill: Option<Color32>,
animate: bool,
}
@ -23,6 +24,7 @@ impl ProgressBar {
progress: progress.clamp(0.0, 1.0),
desired_width: None,
text: None,
fill: None,
animate: false,
}
}
@ -33,6 +35,12 @@ impl ProgressBar {
self
}
/// The fill color of the bar.
pub fn fill(mut self, color: Color32) -> Self {
self.fill = Some(color);
self
}
/// A custom text to display on the progress bar.
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
self.text = Some(ProgressBarText::Custom(text.into()));
@ -60,6 +68,7 @@ impl Widget for ProgressBar {
progress,
desired_width,
text,
fill,
animate,
} = self;
@ -90,7 +99,8 @@ impl Widget for ProgressBar {
let (dark, bright) = (0.7, 1.0);
let color_factor = if animate {
lerp(dark..=bright, ui.input().time.cos().abs())
let time = ui.input(|i| i.time);
lerp(dark..=bright, time.cos().abs())
} else {
bright
};
@ -98,14 +108,17 @@ impl Widget for ProgressBar {
ui.painter().rect(
inner_rect,
rounding,
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
Color32::from(
Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
),
Stroke::NONE,
);
if animate {
let n_points = 20;
let start_angle = ui.input().time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
let circle_radius = rounding - 2.0;
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
@ -116,10 +129,8 @@ impl Widget for ProgressBar {
+ vec2(-rounding, 0.0)
})
.collect();
ui.painter().add(Shape::line(
points,
Stroke::new(2.0, visuals.faint_bg_color),
));
ui.painter()
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
}
if let Some(text_kind) = text {

View file

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

View file

@ -540,7 +540,7 @@ impl<'a> Slider<'a> {
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
let position = self.pointer_position(pointer_position_2d);
let new_value = if self.smart_aim {
let aim_radius = ui.input().aim_radius();
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(position - aim_radius, position_range.clone()),
self.value_from_position(position + aim_radius, position_range.clone()),
@ -562,19 +562,19 @@ impl<'a> Slider<'a> {
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
};
decrement += ui.input().num_presses(dec_key);
increment += ui.input().num_presses(inc_key);
ui.input(|input| {
decrement += input.num_presses(dec_key);
increment += input.num_presses(inc_key);
});
}
#[cfg(feature = "accesskit")]
{
use accesskit::Action;
decrement += ui
.input()
.num_accesskit_action_requests(response.id, Action::Decrement);
increment += ui
.input()
.num_accesskit_action_requests(response.id, Action::Increment);
ui.input(|input| {
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
});
}
let kb_step = increment as f32 - decrement as f32;
@ -586,7 +586,7 @@ impl<'a> Slider<'a> {
let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = ui.input().aim_radius();
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()),
@ -600,14 +600,13 @@ impl<'a> Slider<'a> {
#[cfg(feature = "accesskit")]
{
use accesskit::{Action, ActionData};
for request in ui
.input()
.accesskit_action_requests(response.id, Action::SetValue)
{
if let Some(ActionData::NumericValue(new_value)) = request.data {
self.set_value(new_value);
ui.input(|input| {
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
self.set_value(new_value);
}
}
}
});
}
// Paint it:
@ -624,11 +623,7 @@ impl<'a> Slider<'a> {
rect: rail_rect,
rounding: ui.visuals().widgets.inactive.rounding,
fill: ui.visuals().widgets.inactive.bg_fill,
// fill: visuals.bg_fill,
// fill: ui.visuals().extreme_bg_color,
stroke: Default::default(),
// stroke: visuals.bg_stroke,
// stroke: ui.visuals().widgets.inactive.bg_stroke,
});
let center = self.marker_center(position_1d, &rail_rect);
@ -697,14 +692,12 @@ impl<'a> Slider<'a> {
}
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
let change = {
// Hold one lock rather than 4 (see https://github.com/emilk/egui/pull/1380).
let input = ui.input();
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
let change = ui.input(|input| {
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
- input.num_presses(Key::ArrowDown) as i32
- input.num_presses(Key::ArrowLeft) as i32
};
});
let any_change = change != 0;
let speed = if let (Some(step), true) = (self.step, any_change) {
@ -764,7 +757,7 @@ impl<'a> Slider<'a> {
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
ui.ctx().accesskit_node(response.id, |node| {
use accesskit::Action;
node.min_numeric_value = Some(*self.range.start());
node.max_numeric_value = Some(*self.range.end());
@ -777,7 +770,7 @@ impl<'a> Slider<'a> {
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
}
}
});
let slider_response = response.clone();

View file

@ -48,8 +48,9 @@ impl Widget for Spinner {
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let start_angle = ui.input().time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
let points: Vec<Pos2> = (0..n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);

View file

@ -19,7 +19,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
/// if response.changed() {
/// // …
/// }
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
/// // …
/// }
/// # });
@ -206,7 +206,7 @@ impl<'t> TextEdit<'t> {
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
/// layout_job.wrap.max_width = wrap_width;
/// ui.fonts().layout_job(layout_job)
/// ui.fonts(|f| f.layout_job(layout_job))
/// };
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
/// # });
@ -312,7 +312,7 @@ impl<'t> TextEdit<'t> {
output.response |= ui.interact(frame_rect, id, Sense::click());
}
if output.response.clicked() && !output.response.lost_focus() {
ui.memory().request_focus(output.response.id);
ui.memory_mut(|mem| mem.request_focus(output.response.id));
}
if frame {
@ -381,7 +381,7 @@ impl<'t> TextEdit<'t> {
let prev_text = text.as_str().to_owned();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
let row_height = ui.fonts(|f| f.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);
@ -394,11 +394,12 @@ impl<'t> TextEdit<'t> {
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 {
let layout_job = if multiline {
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
})
};
ui.fonts(|f| f.layout_job(layout_job))
};
let layouter = layouter.unwrap_or(&mut default_layouter);
@ -428,8 +429,8 @@ impl<'t> TextEdit<'t> {
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
// Since currently copying selected text in not supported on `eframe` web,
// we prioritize touch-scrolling:
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 allow_drag_to_select =
ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id));
let sense = if interactive {
if allow_drag_to_select {
@ -447,7 +448,7 @@ impl<'t> TextEdit<'t> {
if interactive {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.hovered() && text.is_mutable() {
ui.output().mutable_text_under_cursor = true;
ui.output_mut(|o| o.mutable_text_under_cursor = true);
}
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
@ -457,7 +458,7 @@ impl<'t> TextEdit<'t> {
if ui.visuals().text_cursor_preview
&& response.hovered()
&& ui.input().pointer.is_moving()
&& ui.input(|i| i.pointer.is_moving())
{
// preview:
paint_cursor_end(
@ -487,9 +488,9 @@ impl<'t> TextEdit<'t> {
secondary: galley.from_ccursor(ccursor_range.secondary),
}));
} else if allow_drag_to_select {
if response.hovered() && ui.input().pointer.any_pressed() {
ui.memory().request_focus(id);
if ui.input().modifiers.shift {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
ui.memory_mut(|mem| mem.request_focus(id));
if ui.input(|i| i.modifiers.shift) {
if let Some(mut cursor_range) = state.cursor_range(&galley) {
cursor_range.primary = cursor_at_pointer;
state.set_cursor_range(Some(cursor_range));
@ -499,7 +500,8 @@ impl<'t> TextEdit<'t> {
} else {
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
}
} else if ui.input().pointer.any_down() && response.is_pointer_button_down_on()
} else if ui.input(|i| i.pointer.any_down())
&& response.is_pointer_button_down_on()
{
// drag to select text:
if let Some(mut cursor_range) = state.cursor_range(&galley) {
@ -511,14 +513,14 @@ impl<'t> TextEdit<'t> {
}
}
if response.hovered() && interactive {
ui.output().cursor_icon = CursorIcon::Text;
if interactive && response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor_range(&galley);
if ui.memory().has_focus(id) && interactive {
ui.memory().lock_focus(id, lock_focus);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
let default_cursor_range = if cursor_at_end {
CursorRange::one(galley.end())
@ -549,7 +551,7 @@ impl<'t> TextEdit<'t> {
// Visual clipping for singleline text editor with text larger than width
if !multiline {
let cursor_pos = match (cursor_range, ui.memory().has_focus(id)) {
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
_ => 0.0,
};
@ -594,7 +596,7 @@ impl<'t> TextEdit<'t> {
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
}
if ui.memory().has_focus(id) {
if ui.memory(|mem| mem.has_focus(id)) {
if let Some(cursor_range) = state.cursor_range(&galley) {
// We paint the cursor on top of the text, in case
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
@ -621,9 +623,13 @@ impl<'t> TextEdit<'t> {
// But `winit` and `egui_web` differs in how to set the
// position of IME.
if cfg!(target_arch = "wasm32") {
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
ui.ctx().output_mut(|o| {
o.text_cursor_pos = Some(cursor_pos.left_top());
});
} else {
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_bottom());
ui.ctx().output_mut(|o| {
o.text_cursor_pos = Some(cursor_pos.left_bottom());
});
}
}
}
@ -659,89 +665,98 @@ impl<'t> TextEdit<'t> {
}
#[cfg(feature = "accesskit")]
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
{
let parent_id = ui.ctx().accesskit_node(response.id, |node| {
use accesskit::{TextPosition, TextSelection};
let parent_id = response.id;
let parent_id = response.id;
if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
node.text_selection = Some(TextSelection {
anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
},
focus: TextPosition {
node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column,
},
if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
node.text_selection = Some(TextSelection {
anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
},
focus: TextPosition {
node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column,
},
});
}
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
node.multiline = self.multiline;
parent_id
});
if let Some(parent_id) = parent_id {
// drop ctx lock before further processing
use accesskit::{Role, TextDirection};
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
ui.ctx().accesskit_node(id, |node| {
node.role = Role::InlineTextBox;
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
node.text_direction = Some(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let glyph_count = row.glyphs.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::new();
character_lengths.reserve(glyph_count);
let mut character_positions = Vec::<f32>::new();
character_positions.reserve(glyph_count);
let mut character_widths = Vec::<f32>::new();
character_widths.reserve(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths
.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.rect.min.x);
character_widths.push(glyph.size.x);
}
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.rect.max.x - row.rect.min.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
node.value = Some(value.into());
node.character_lengths = character_lengths.into();
node.character_positions = Some(character_positions.into());
node.character_widths = Some(character_widths.into());
node.word_lengths = word_lengths.into();
});
}
});
}
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
node.multiline = self.multiline;
drop(node);
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
let mut node = ui.ctx().accesskit_node(id).unwrap();
node.role = Role::InlineTextBox;
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
node.text_direction = Some(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let glyph_count = row.glyphs.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::new();
character_lengths.reserve(glyph_count);
let mut character_positions = Vec::<f32>::new();
character_positions.reserve(glyph_count);
let mut character_widths = Vec::<f32>::new();
character_widths.reserve(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.rect.min.x);
character_widths.push(glyph.size.x);
}
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.rect.max.x - row.rect.min.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
node.value = Some(value.into());
node.character_lengths = character_lengths.into();
node.character_positions = Some(character_positions.into());
node.character_widths = Some(character_widths.into());
node.word_lengths = word_lengths.into();
}
});
}
TextEditOutput {
@ -812,19 +827,19 @@ fn events(
// We feed state to the undoer both before and after handling input
// so that the undoer creates automatic saves even when there are no events for a while.
state.undoer.lock().feed_state(
ui.input().time,
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
);
let copy_if_not_password = |ui: &Ui, text: String| {
if !password {
ui.ctx().output().copied_text = text;
ui.ctx().output_mut(|o| o.copied_text = text);
}
};
let mut any_change = false;
let events = ui.input().events.clone(); // avoid dead-lock by cloning. TODO(emilk): optimize
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
for event in &events {
let did_mutate_text = match event {
Event::Copy => {
@ -869,7 +884,7 @@ fn events(
modifiers,
..
} => {
if multiline && ui.memory().has_lock_focus(id) {
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
let mut ccursor = delete_selected(text, &cursor_range);
if modifiers.shift {
// TODO(emilk): support removing indentation over a selection?
@ -893,7 +908,7 @@ fn events(
// TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
Some(CCursorRange::one(ccursor))
} else {
ui.memory().surrender_focus(id); // End input with enter
ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
break;
}
}
@ -997,7 +1012,7 @@ fn events(
state.set_cursor_range(Some(cursor_range));
state.undoer.lock().feed_state(
ui.input().time,
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
);

View file

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

View file

@ -35,7 +35,7 @@ impl Default for FractalClock {
impl FractalClock {
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if !self.paused {
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time));
ui.ctx().request_repaint();
}

View file

@ -131,7 +131,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
trigger_fetch = true;
}
if ui.button("Random image").clicked() {
let seed = ui.input().time;
let seed = ui.input(|i| i.time);
let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
trigger_fetch = true;
@ -187,7 +187,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
if let Some(text) = &text {
let tooltip = "Click to copy the response body";
if ui.button("📋").on_hover_text(tooltip).clicked() {
ui.output().copied_text = text.clone();
ui.output_mut(|o| o.copied_text = text.clone());
}
ui.separator();
}
@ -245,7 +245,7 @@ impl ColoredText {
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
let mut layout_job = self.0.clone();
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
ui.fonts(|f| f.layout_job(layout_job))
};
let mut text = self.0.text.as_str();
@ -258,7 +258,7 @@ impl ColoredText {
} else {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts().layout_job(job);
let galley = ui.fonts(|f| f.layout_job(job));
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
painter.add(egui::Shape::galley(response.rect.min, galley));
}

View file

@ -81,7 +81,7 @@ impl Default for BackendPanel {
impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
match self.run_mode {
RunMode::Continuous => {
@ -131,9 +131,9 @@ impl BackendPanel {
ui.separator();
{
let mut screen_reader = ui.ctx().options().screen_reader;
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options().screen_reader = screen_reader;
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
}
#[cfg(not(target_arch = "wasm32"))]
@ -339,9 +339,11 @@ impl EguiWindows {
output_event_history,
} = self;
for event in &ctx.output().events {
output_event_history.push_back(event.clone());
}
ctx.output(|o| {
for event in &o.events {
output_event_history.push_back(event.clone());
}
});
while output_event_history.len() > 1000 {
output_event_history.pop_front();
}

View file

@ -95,19 +95,21 @@ impl FrameHistory {
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(Shape::text(
&ui.fonts(),
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
));
shapes.push(ui.fonts(|f| {
Shape::text(
f,
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
)
}));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input().time; // Time at right side of screen
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;

View file

@ -191,10 +191,7 @@ impl eframe::App for WrapApp {
}
#[cfg(not(target_arch = "wasm32"))]
if ctx
.input_mut()
.consume_key(egui::Modifiers::NONE, egui::Key::F11)
{
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) {
frame.set_fullscreen(!frame.info().window_info.fullscreen);
}
@ -241,7 +238,8 @@ impl WrapApp {
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// The backend-panel can be toggled on/off.
// We show a little animation when the user switches it.
let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible();
let is_open =
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
egui::SidePanel::left("backend_panel")
.resizable(false)
@ -266,13 +264,13 @@ impl WrapApp {
.on_hover_text("Forget scroll, positions, sizes etc")
.clicked()
{
*ui.ctx().memory() = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
if ui.button("Reset everything").clicked() {
self.state = Default::default();
*ui.ctx().memory() = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
});
@ -282,7 +280,7 @@ impl WrapApp {
let mut found_anchor = false;
let selected_anchor = self.state.selected_anchor.clone();
for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
app.update(ctx, frame);
found_anchor = true;
}
@ -316,7 +314,7 @@ impl WrapApp {
{
selected_anchor = anchor.to_owned();
if frame.is_web() {
ui.output().open_url(format!("#{}", anchor));
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
}
}
}
@ -328,7 +326,7 @@ impl WrapApp {
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
self.state.selected_anchor = "clock".to_owned();
if frame.is_web() {
ui.output().open_url("#clock");
ui.output_mut(|o| o.open_url("#clock"));
}
}
}
@ -342,24 +340,27 @@ impl WrapApp {
use std::fmt::Write as _;
// Preview hovering files:
if !ctx.input().raw.hovered_files.is_empty() {
let mut text = "Dropping files:\n".to_owned();
for file in &ctx.input().raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
text += "\n???";
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
let text = ctx.input(|i| {
let mut text = "Dropping files:\n".to_owned();
for file in &i.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
text += "\n???";
}
}
}
text
});
let painter = ctx.layer_painter(AreaLayerId::new(
Order::Foreground,
Id::new("file_drop_target"),
));
let screen_rect = ctx.input().screen_rect();
let screen_rect = ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
@ -371,9 +372,11 @@ impl WrapApp {
}
// Collect dropped files:
if !ctx.input().raw.dropped_files.is_empty() {
self.dropped_files = ctx.input().raw.dropped_files.clone();
}
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
self.dropped_files = i.raw.dropped_files.clone();
}
});
// Show dropped files (if any):
if !self.dropped_files.is_empty() {

View file

@ -38,7 +38,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
if false {
let ctx = egui::Context::default();
ctx.memory().set_everything_is_visible(true); // give us everything
ctx.memory_mut(|m| m.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(|| {

View file

@ -79,7 +79,7 @@ impl super::View for CodeEditor {
let mut layout_job =
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
ui.fonts(|f| f.layout_job(layout_job))
};
egui::ScrollArea::vertical().show(ui, |ui| {

View file

@ -103,7 +103,7 @@ impl CodeExample {
ui.horizontal(|ui| {
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
let indentation = 8.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
let item_spacing = ui.spacing_mut().item_spacing;
ui.add_space(indentation - item_spacing.x);

View file

@ -30,7 +30,7 @@ impl super::View for DancingStrings {
Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint();
let time = ui.input().time;
let time = ui.input(|i| i.time);
let desired_size = ui.available_width() * vec2(1.0, 0.35);
let (_id, rect) = ui.allocate_space(desired_size);

View file

@ -177,7 +177,7 @@ impl DemoWindows {
fn mobile_ui(&mut self, ctx: &Context) {
if self.about_is_open {
let screen_size = ctx.input().screen_rect.size();
let screen_size = ctx.input(|i| i.screen_rect.size());
let default_width = (screen_size.x - 20.0).min(400.0);
let mut close = false;
@ -216,7 +216,7 @@ impl DemoWindows {
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
self.demo_list_ui(ui);
if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
ui.close_menu();
}
});
@ -291,7 +291,7 @@ impl DemoWindows {
ui.separator();
if ui.button("Organize windows").clicked() {
ui.ctx().memory().reset_areas();
ui.ctx().memory_mut(|mem| mem.reset_areas());
}
});
});
@ -309,12 +309,12 @@ fn file_menu_button(ui: &mut Ui) {
// NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu,
// or else they would only be checked if the "File" menu was actually open!
if ui.input_mut().consume_shortcut(&organize_shortcut) {
ui.ctx().memory().reset_areas();
if ui.input_mut(|i| i.consume_shortcut(&organize_shortcut)) {
ui.ctx().memory_mut(|mem| mem.reset_areas());
}
if ui.input_mut().consume_shortcut(&reset_shortcut) {
*ui.ctx().memory() = Default::default();
if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) {
ui.ctx().memory_mut(|mem| *mem = Default::default());
}
ui.menu_button("File", |ui| {
@ -335,7 +335,7 @@ fn file_menu_button(ui: &mut Ui) {
)
.clicked()
{
ui.ctx().memory().reset_areas();
ui.ctx().memory_mut(|mem| mem.reset_areas());
ui.close_menu();
}
@ -347,7 +347,7 @@ fn file_menu_button(ui: &mut Ui) {
.on_hover_text("Forget scroll, positions, sizes etc")
.clicked()
{
*ui.ctx().memory() = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
});

View file

@ -1,7 +1,7 @@
use egui::*;
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
let is_being_dragged = ui.memory().is_being_dragged(id);
let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id));
if !is_being_dragged {
let response = ui.scope(body).response;
@ -9,10 +9,10 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
// Check for drags:
let response = ui.interact(response.rect, id, Sense::drag());
if response.hovered() {
ui.output().cursor_icon = CursorIcon::Grab;
ui.ctx().set_cursor_icon(CursorIcon::Grab);
}
} else {
ui.output().cursor_icon = CursorIcon::Grabbing;
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
// Paint the body to a new layer:
let layer_id = AreaLayerId::new(Order::Tooltip, id);
@ -37,7 +37,7 @@ pub fn drop_target<R>(
can_accept_what_is_being_dragged: bool,
body: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let is_being_dragged = ui.memory().is_anything_being_dragged();
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
let margin = Vec2::splat(4.0);
@ -139,7 +139,7 @@ impl super::View for DragAndDropDemo {
});
});
if ui.memory().is_being_dragged(item_id) {
if ui.memory(|mem| mem.is_being_dragged(item_id)) {
source_col_row = Some((col_idx, row_idx));
}
}
@ -153,7 +153,7 @@ impl super::View for DragAndDropDemo {
}
});
let is_being_dragged = ui.memory().is_anything_being_dragged();
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
drop_col = Some(col_idx);
}
@ -162,7 +162,7 @@ impl super::View for DragAndDropDemo {
if let Some((source_col, source_row)) = source_col_row {
if let Some(drop_col) = drop_col {
if ui.input().pointer.any_released() {
if ui.input(|i| i.pointer.any_released()) {
// do the drop:
let item = self.columns[source_col].remove(source_row);
self.columns[drop_col].push(item);

View file

@ -93,7 +93,7 @@ impl super::View for FontBook {
};
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
ui.output().copied_text = chr.to_string();
ui.output_mut(|o| o.copied_text = chr.to_string());
}
}
}
@ -103,15 +103,16 @@ 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()
ui.fonts(|f| {
f.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 {

View file

@ -202,7 +202,7 @@ impl Widgets {
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' ');
let width = ui.fonts(|f|f.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)));

View file

@ -49,7 +49,7 @@ impl super::View for MultiTouch {
ui.separator();
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
let num_touches = ui.input().multi_touch().map_or(0, |mt| mt.num_touches);
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
ui.label(format!("Current touches: {}", num_touches));
let color = if ui.visuals().dark_mode {
@ -93,7 +93,7 @@ impl super::View for MultiTouch {
// touch pressure will make the arrow thicker (not all touch devices support this):
stroke_width += 10. * multi_touch.force;
self.last_touch_time = ui.input().time;
self.last_touch_time = ui.input(|i| i.time);
} else {
self.slowly_reset(ui);
}
@ -118,7 +118,7 @@ impl MultiTouch {
// This has nothing to do with the touch gesture. It just smoothly brings the
// painted arrow back into its original position, for a nice visual effect:
let time_since_last_touch = (ui.input().time - self.last_touch_time) as f32;
let time_since_last_touch = (ui.input(|i| i.time) - self.last_touch_time) as f32;
let delay = 0.5;
if time_since_last_touch < delay {
@ -133,7 +133,7 @@ impl MultiTouch {
self.rotation = 0.0;
self.translation = Vec2::ZERO;
} else {
let dt = ui.input().unstable_dt;
let dt = ui.input(|i| i.unstable_dt);
let half_life_factor = (-(2_f32.ln()) / half_life * dt).exp();
self.zoom = 1. + ((self.zoom - 1.) * half_life_factor);
self.rotation *= half_life_factor;

View file

@ -53,11 +53,10 @@ impl PaintBezier {
});
ui.collapsing("Global tessellation options", |ui| {
let mut tessellation_options = *(ui.ctx().tessellation_options());
let tessellation_options = &mut tessellation_options;
let mut tessellation_options = ui.ctx().tessellation_options(|to| *to);
tessellation_options.ui(ui);
let mut new_tessellation_options = ui.ctx().tessellation_options();
*new_tessellation_options = *tessellation_options;
ui.ctx()
.tessellation_options_mut(|to| *to = tessellation_options);
});
ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");

View file

@ -20,7 +20,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.data().get_temp::<bool>(state_id).unwrap_or(false);
let mut show_plaintext = ui.data_mut(|d| d.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
@ -43,7 +43,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
});
// Store the (possibly changed) state:
ui.data().insert_temp(state_id, show_plaintext);
ui.data_mut(|d| d.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

@ -11,6 +11,124 @@ use plot::{
// ----------------------------------------------------------------------------
#[derive(PartialEq, Eq)]
enum Panel {
Lines,
Markers,
Legend,
Charts,
Items,
Interaction,
CustomAxes,
LinkedAxes,
}
impl Default for Panel {
fn default() -> Self {
Self::Lines
}
}
// ----------------------------------------------------------------------------
#[derive(PartialEq, Default)]
pub struct PlotDemo {
line_demo: LineDemo,
marker_demo: MarkerDemo,
legend_demo: LegendDemo,
charts_demo: ChartsDemo,
items_demo: ItemsDemo,
interaction_demo: InteractionDemo,
custom_axes_demo: CustomAxisDemo,
linked_axes_demo: LinkedAxisDemo,
open_panel: Panel,
}
impl super::Demo for PlotDemo {
fn name(&self) -> &'static str {
"🗠 Plot"
}
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)
.default_size(vec2(400.0, 400.0))
.vscroll(false)
.show(ctx, |ui| self.ui(ui));
}
}
impl super::View for PlotDemo {
fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
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") {
ui.label("Zoom with ctrl / ⌘ + scroll.");
} else {
ui.label("Zoom with ctrl + scroll.");
}
ui.label("Reset view with double-click.");
ui.add(crate::egui_github_link_file!());
});
});
ui.separator();
ui.horizontal(|ui| {
ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines");
ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers");
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
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::CustomAxes, "Custom Axes");
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
});
ui.separator();
match self.open_panel {
Panel::Lines => {
self.line_demo.ui(ui);
}
Panel::Markers => {
self.marker_demo.ui(ui);
}
Panel::Legend => {
self.legend_demo.ui(ui);
}
Panel::Charts => {
self.charts_demo.ui(ui);
}
Panel::Items => {
self.items_demo.ui(ui);
}
Panel::Interaction => {
self.interaction_demo.ui(ui);
}
Panel::CustomAxes => {
self.custom_axes_demo.ui(ui);
}
Panel::LinkedAxes => {
self.linked_axes_demo.ui(ui);
}
}
}
}
fn is_approx_zero(val: f64) -> bool {
val.abs() < 1e-6
}
fn is_approx_integer(val: f64) -> bool {
val.fract().abs() < 1e-6
}
// ----------------------------------------------------------------------------
#[derive(PartialEq)]
struct LineDemo {
animate: bool,
@ -152,7 +270,7 @@ impl LineDemo {
self.options_ui(ui);
if self.animate {
ui.ctx().request_repaint();
self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
};
let mut plot = Plot::new("lines_demo").legend(Legend::default());
if self.square {
@ -754,7 +872,7 @@ impl ChartsDemo {
Plot::new("Normal Distribution Demo")
.legend(Legend::default())
.data_aspect(1.0)
.clamp_grid(true)
.show(ui, |plot_ui| plot_ui.bar_chart(chart))
.response
}
@ -864,121 +982,3 @@ impl ChartsDemo {
.response
}
}
// ----------------------------------------------------------------------------
#[derive(PartialEq, Eq)]
enum Panel {
Lines,
Markers,
Legend,
Charts,
Items,
Interaction,
CustomAxes,
LinkedAxes,
}
impl Default for Panel {
fn default() -> Self {
Self::Lines
}
}
// ----------------------------------------------------------------------------
#[derive(PartialEq, Default)]
pub struct PlotDemo {
line_demo: LineDemo,
marker_demo: MarkerDemo,
legend_demo: LegendDemo,
charts_demo: ChartsDemo,
items_demo: ItemsDemo,
interaction_demo: InteractionDemo,
custom_axes_demo: CustomAxisDemo,
linked_axes_demo: LinkedAxisDemo,
open_panel: Panel,
}
impl super::Demo for PlotDemo {
fn name(&self) -> &'static str {
"🗠 Plot"
}
fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)
.default_size(vec2(400.0, 400.0))
.vscroll(false)
.show(ctx, |ui| self.ui(ui));
}
}
impl super::View for PlotDemo {
fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
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") {
ui.label("Zoom with ctrl / ⌘ + scroll.");
} else {
ui.label("Zoom with ctrl + scroll.");
}
ui.label("Reset view with double-click.");
ui.add(crate::egui_github_link_file!());
});
});
ui.separator();
ui.horizontal(|ui| {
ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines");
ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers");
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
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::CustomAxes, "Custom Axes");
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
});
ui.separator();
match self.open_panel {
Panel::Lines => {
self.line_demo.ui(ui);
}
Panel::Markers => {
self.marker_demo.ui(ui);
}
Panel::Legend => {
self.legend_demo.ui(ui);
}
Panel::Charts => {
self.charts_demo.ui(ui);
}
Panel::Items => {
self.items_demo.ui(ui);
}
Panel::Interaction => {
self.interaction_demo.ui(ui);
}
Panel::CustomAxes => {
self.custom_axes_demo.ui(ui);
}
Panel::LinkedAxes => {
self.linked_axes_demo.ui(ui);
}
}
}
}
fn is_approx_zero(val: f64) -> bool {
val.abs() < 1e-6
}
fn is_approx_integer(val: f64) -> bool {
val.fract().abs() < 1e-6
}

View file

@ -112,7 +112,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
ui.add_space(4.0);
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id) + ui.spacing().item_spacing.y;
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
let num_rows = 10_000;
ScrollArea::vertical()

View file

@ -65,10 +65,7 @@ impl super::View for TextEdit {
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
);
if ui
.input_mut()
.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)
{
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) {
if let Some(text_cursor_range) = output.cursor_range {
use egui::TextBuffer as _;
let selected_chars = text_cursor_range.as_sorted_char_range();
@ -93,7 +90,7 @@ impl super::View for TextEdit {
let ccursor = egui::text::CCursor::new(0);
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
state.store(ui.ctx(), text_edit_id);
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
}
}
@ -103,7 +100,7 @@ impl super::View for TextEdit {
let ccursor = egui::text::CCursor::new(text.chars().count());
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
state.store(ui.ctx(), text_edit_id);
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
}
}
});

View file

@ -175,6 +175,8 @@ impl WidgetGallery {
egui::ComboBox::from_label("Take your pick")
.selected_text(format!("{:?}", radio))
.show_ui(ui, |ui| {
ui.style_mut().wrap = Some(false);
ui.set_min_width(60.0);
ui.selectable_value(radio, Enum::First, "First");
ui.selectable_value(radio, Enum::Second, "Second");
ui.selectable_value(radio, Enum::Third, "Third");

View file

@ -50,7 +50,7 @@ impl super::Demo for WindowOptions {
anchor_offset,
} = self.clone();
let enabled = ctx.input().time - disabled_time > 2.0;
let enabled = ctx.input(|i| i.time) - disabled_time > 2.0;
if !enabled {
ctx.request_repaint();
}
@ -132,7 +132,7 @@ impl super::View for WindowOptions {
ui.horizontal(|ui| {
if ui.button("Disable for 2 seconds").clicked() {
self.disabled_time = ui.input().time;
self.disabled_time = ui.input(|i| i.time);
}
egui::reset_button(ui, self);
ui.add(crate::egui_github_link_file!());

View file

@ -81,7 +81,7 @@ impl EasyMarkEditor {
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
let mut layout_job = highlighter.highlight(ui.style(), easymark);
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
ui.fonts(|f| f.layout_job(layout_job))
};
ui.add(
@ -141,7 +141,7 @@ fn nested_hotkeys_ui(ui: &mut egui::Ui) {
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
let mut any_change = false;
if ui.input_mut().consume_shortcut(&SHORTCUT_INDENT) {
if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) {
// This is a placeholder till we can indent the active line
any_change = true;
let [primary, _secondary] = ccursor_range.sorted();
@ -160,7 +160,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
(SHORTCUT_STRIKETHROUGH, "~"),
(SHORTCUT_UNDERLINE, "_"),
] {
if ui.input_mut().consume_shortcut(&shortcut) {
if ui.input_mut(|i| i.consume_shortcut(&shortcut)) {
any_change = true;
toggle_surrounding(code, ccursor_range, surrounding);
};

View file

@ -144,7 +144,7 @@ fn bullet_point(ui: &mut Ui, width: f32) -> Response {
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
let row_height = ui.fonts(|f| f.row_height(&font_id));
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
let text = format!("{}.", number);
let text_color = ui.visuals().strong_text_color();

View file

@ -102,6 +102,6 @@ fn test_egui_zero_window_size() {
/// Detect narrow screens. This is used to show a simpler UI on mobile devices,
/// especially for the web demo at <https://egui.rs>.
pub fn is_mobile(ctx: &egui::Context) -> bool {
let screen_size = ctx.input().screen_rect().size();
let screen_size = ctx.screen_rect().size();
screen_size.x < 550.0
}

View file

@ -8,7 +8,7 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
let layout_job = highlight(ui.ctx(), &theme, string, language);
// layout_job.wrap.max_width = wrap_width; // no wrapping
ui.fonts().layout_job(layout_job)
ui.fonts(|f| f.layout_job(layout_job))
};
ui.add(
@ -31,9 +31,11 @@ pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
let mut memory = ctx.memory();
let highlight_cache = memory.caches.cache::<HighlightCache>();
highlight_cache.get((theme, code, language))
ctx.memory_mut(|mem| {
mem.caches
.cache::<HighlightCache>()
.get((theme, code, language))
})
}
// ----------------------------------------------------------------------------
@ -146,21 +148,23 @@ impl CodeTheme {
pub fn from_memory(ctx: &egui::Context) -> Self {
if ctx.style().visuals.dark_mode {
ctx.data()
.get_persisted(egui::Id::new("dark"))
.unwrap_or_else(CodeTheme::dark)
ctx.data_mut(|d| {
d.get_persisted(egui::Id::new("dark"))
.unwrap_or_else(CodeTheme::dark)
})
} else {
ctx.data()
.get_persisted(egui::Id::new("light"))
.unwrap_or_else(CodeTheme::light)
ctx.data_mut(|d| {
d.get_persisted(egui::Id::new("light"))
.unwrap_or_else(CodeTheme::light)
})
}
}
pub fn store_in_memory(self, ctx: &egui::Context) {
if self.dark_mode {
ctx.data().insert_persisted(egui::Id::new("dark"), self);
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
} else {
ctx.data().insert_persisted(egui::Id::new("light"), self);
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
}
}
}
@ -230,9 +234,8 @@ impl CodeTheme {
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_top(|ui| {
let selected_id = egui::Id::null();
let mut selected_tt: TokenType = *ui
.data()
.get_persisted_mut_or(selected_id, TokenType::Comment);
let mut selected_tt: TokenType =
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment));
ui.vertical(|ui| {
ui.set_width(150.0);
@ -274,7 +277,7 @@ impl CodeTheme {
ui.add_space(16.0);
ui.data().insert_persisted(selected_id, selected_tt);
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt));
egui::Frame::group(ui.style())
.inner_margin(egui::Vec2::splat(2.0))

View file

@ -64,9 +64,7 @@ impl<'a> Widget for DatePickerButton<'a> {
fn ui(self, ui: &mut Ui) -> egui::Response {
let id = ui.make_persistent_id(self.id_source);
let mut button_state = ui
.memory()
.data
.get_persisted::<DatePickerButtonState>(id)
.memory_mut(|mem| mem.data.get_persisted::<DatePickerButtonState>(id))
.unwrap_or_default();
let mut text = RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d")));
@ -76,12 +74,12 @@ impl<'a> Widget for DatePickerButton<'a> {
}
let mut button = Button::new(text);
if button_state.picker_visible {
button = button.fill(visuals.bg_fill).stroke(visuals.bg_stroke);
button = button.fill(visuals.weak_bg_fill).stroke(visuals.bg_stroke);
}
let mut button_response = ui.add(button);
if button_response.clicked() {
button_state.picker_visible = true;
ui.memory().data.insert_persisted(id, button_state.clone());
ui.memory_mut(|mem| mem.data.insert_persisted(id, button_state.clone()));
}
if button_state.picker_visible {
@ -131,10 +129,10 @@ impl<'a> Widget for DatePickerButton<'a> {
}
if !button_response.clicked()
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
button_state.picker_visible = false;
ui.memory().data.insert_persisted(id, button_state);
ui.memory_mut(|mem| mem.data.insert_persisted(id, button_state));
}
}

View file

@ -41,16 +41,14 @@ impl<'a> DatePickerPopup<'a> {
let id = ui.make_persistent_id("date_picker");
let today = chrono::offset::Utc::now().date_naive();
let mut popup_state = ui
.memory()
.data
.get_persisted::<DatePickerPopupState>(id)
.memory_mut(|mem| mem.data.get_persisted::<DatePickerPopupState>(id))
.unwrap_or_default();
if !popup_state.setup {
popup_state.year = self.selection.year();
popup_state.month = self.selection.month();
popup_state.day = self.selection.day();
popup_state.setup = true;
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| mem.data.insert_persisted(id, popup_state.clone()));
}
let weeks = month_data(popup_state.year, popup_state.month);
@ -93,9 +91,10 @@ impl<'a> DatePickerPopup<'a> {
popup_state.day = popup_state
.day
.min(popup_state.last_day_of_month());
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data
.insert_persisted(id, popup_state.clone());
});
}
}
});
@ -116,9 +115,10 @@ impl<'a> DatePickerPopup<'a> {
popup_state.day = popup_state
.day
.min(popup_state.last_day_of_month());
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data
.insert_persisted(id, popup_state.clone());
});
}
}
});
@ -136,9 +136,10 @@ impl<'a> DatePickerPopup<'a> {
)
.changed()
{
ui.memory()
.data
.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data
.insert_persisted(id, popup_state.clone());
});
}
}
});
@ -160,7 +161,9 @@ impl<'a> DatePickerPopup<'a> {
popup_state.year -= 1;
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -178,7 +181,9 @@ impl<'a> DatePickerPopup<'a> {
}
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -194,7 +199,9 @@ impl<'a> DatePickerPopup<'a> {
}
popup_state.day = popup_state.last_day_of_month();
}
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -210,7 +217,9 @@ impl<'a> DatePickerPopup<'a> {
popup_state.year += 1;
}
}
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -224,7 +233,9 @@ impl<'a> DatePickerPopup<'a> {
}
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -234,7 +245,9 @@ impl<'a> DatePickerPopup<'a> {
popup_state.year += 1;
popup_state.day =
popup_state.day.min(popup_state.last_day_of_month());
ui.memory().data.insert_persisted(id, popup_state.clone());
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state.clone());
});
}
});
});
@ -342,10 +355,12 @@ impl<'a> DatePickerPopup<'a> {
popup_state.year = day.year();
popup_state.month = day.month();
popup_state.day = day.day();
ui.memory().data.insert_persisted(
id,
popup_state.clone(),
);
ui.memory_mut(|mem| {
mem.data.insert_persisted(
id,
popup_state.clone(),
);
});
}
},
);
@ -387,12 +402,12 @@ impl<'a> DatePickerPopup<'a> {
if close {
popup_state.setup = false;
ui.memory().data.insert_persisted(id, popup_state);
ui.memory()
.data
.get_persisted_mut_or_default::<DatePickerButtonState>(self.button_id)
.picker_visible = false;
ui.memory_mut(|mem| {
mem.data.insert_persisted(id, popup_state);
mem.data
.get_persisted_mut_or_default::<DatePickerButtonState>(self.button_id)
.picker_visible = false;
});
}
saved && close

View file

@ -144,6 +144,7 @@ pub struct Strip<'a, 'b> {
}
impl<'a, 'b> Strip<'a, 'b> {
#[cfg_attr(debug_assertions, track_caller)]
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
let size = if let Some(size) = self.sizes.get(self.size_index) {
self.size_index += 1;
@ -163,6 +164,7 @@ impl<'a, 'b> Strip<'a, 'b> {
}
/// Add cell contents.
#[cfg_attr(debug_assertions, track_caller)]
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
let (width, height) = self.next_cell_size();
let striped = false;
@ -171,6 +173,7 @@ impl<'a, 'b> Strip<'a, 'b> {
}
/// Add an empty cell.
#[cfg_attr(debug_assertions, track_caller)]
pub fn empty(&mut self) {
let (width, height) = self.next_cell_size();
self.layout.empty(width, height);

View file

@ -479,7 +479,7 @@ impl TableState {
let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
ui.ctx().check_for_id_clash(state_id, rect, "Table");
if let Some(state) = ui.data().get_persisted::<Self>(state_id) {
if let Some(state) = ui.data_mut(|d| d.get_persisted::<Self>(state_id)) {
// make sure that the stored widths aren't out-dated
if state.column_widths.len() == default_widths.len() {
return (true, state);
@ -495,7 +495,7 @@ impl TableState {
}
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
ui.data().insert_persisted(state_id, self);
ui.data_mut(|d| d.insert_persisted(state_id, self));
}
}
@ -680,14 +680,12 @@ impl<'a> Table<'a> {
}
}
let dragging_something_else = {
let pointer = &ui.input().pointer;
pointer.any_down() || pointer.any_pressed()
};
let dragging_something_else =
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
let resize_hover = resize_response.hovered() && !dragging_something_else;
if resize_hover || resize_response.dragged() {
ui.output().cursor_icon = egui::CursorIcon::ResizeColumn;
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeColumn);
}
let stroke = if resize_response.dragged() {
@ -1026,6 +1024,7 @@ impl<'a, 'b> TableRow<'a, 'b> {
/// Add the contents of a column.
///
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
#[cfg_attr(debug_assertions, track_caller)]
pub fn col(&mut self, add_cell_contents: impl FnOnce(&mut Ui)) -> (Rect, Response) {
let col_index = self.col_index;

View file

@ -87,6 +87,14 @@ impl Real for f64 {}
// ----------------------------------------------------------------------------
/// Linear interpolation.
///
/// ```
/// # use emath::lerp;
/// assert_eq!(lerp(1.0..=5.0, 0.0), 1.0);
/// assert_eq!(lerp(1.0..=5.0, 0.5), 3.0);
/// assert_eq!(lerp(1.0..=5.0, 1.0), 5.0);
/// assert_eq!(lerp(1.0..=5.0, 2.0), 9.0);
/// ```
#[inline(always)]
pub fn lerp<R, T>(range: RangeInclusive<R>, t: T) -> R
where
@ -96,6 +104,34 @@ where
(T::one() - t) * *range.start() + t * *range.end()
}
/// Where in the range is this value? Returns 0-1 if within the range.
///
/// Returns <0 if before and >1 if after.
///
/// Returns `None` if the input range is zero-width.
///
/// ```
/// # use emath::inverse_lerp;
/// assert_eq!(inverse_lerp(1.0..=5.0, 1.0), Some(0.0));
/// assert_eq!(inverse_lerp(1.0..=5.0, 3.0), Some(0.5));
/// assert_eq!(inverse_lerp(1.0..=5.0, 5.0), Some(1.0));
/// assert_eq!(inverse_lerp(1.0..=5.0, 9.0), Some(2.0));
/// assert_eq!(inverse_lerp(1.0..=1.0, 3.0), None);
/// ```
#[inline]
pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
where
R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
{
let min = *range.start();
let max = *range.end();
if min == max {
None
} else {
Some((value - min) / (max - min))
}
}
/// Linearly remap a value from one range to another,
/// so that when `x == from.start()` returns `to.start()`
/// and when `x == from.end()` returns `to.end()`.

View file

@ -768,7 +768,7 @@ pub struct ViewportInPixels {
/// Viewport width in physical pixels.
pub width_px: f32,
/// Viewport width in physical pixels.
/// Viewport height in physical pixels.
pub height_px: f32,
}

View file

@ -1316,16 +1316,18 @@ impl Tessellator {
self.tessellate_line(line, Stroke::new(rect.width(), fill), out);
}
if !stroke.is_empty() {
self.tessellate_line(line, stroke, out);
self.tessellate_line(line, stroke, out); // back…
self.tessellate_line(line, stroke, out); // …and forth
}
} else if rect.height() < self.feathering {
// Very thin - approximate by a horizontal line-segment:
let line = [rect.left_center(), rect.right_center()];
if fill != Color32::TRANSPARENT {
self.tessellate_line(line, Stroke::new(rect.width(), fill), out);
self.tessellate_line(line, Stroke::new(rect.height(), fill), out);
}
if !stroke.is_empty() {
self.tessellate_line(line, stroke, out);
self.tessellate_line(line, stroke, out); // back…
self.tessellate_line(line, stroke, out); // …and forth
}
} else {
let path = &mut self.scratchpad_path;
@ -1505,6 +1507,10 @@ impl Tessellator {
stroke: Stroke,
out: &mut Mesh,
) {
if points.len() < 2 {
return;
}
self.scratchpad_path.clear();
if closed {
self.scratchpad_path.add_line_loop(points);

View file

@ -392,7 +392,38 @@ fn invisible_char(c: char) -> bool {
// See https://github.com/emilk/egui/issues/336
// From https://www.fileformat.info/info/unicode/category/Cf/list.htm
('\u{200B}'..='\u{206F}').contains(&c) // TODO(emilk): heed bidi characters
// TODO(emilk): heed bidi characters
matches!(
c,
'\u{200B}' // ZERO WIDTH SPACE
| '\u{200C}' // ZERO WIDTH NON-JOINER
| '\u{200D}' // ZERO WIDTH JOINER
| '\u{200E}' // LEFT-TO-RIGHT MARK
| '\u{200F}' // RIGHT-TO-LEFT MARK
| '\u{202A}' // LEFT-TO-RIGHT EMBEDDING
| '\u{202B}' // RIGHT-TO-LEFT EMBEDDING
| '\u{202C}' // POP DIRECTIONAL FORMATTING
| '\u{202D}' // LEFT-TO-RIGHT OVERRIDE
| '\u{202E}' // RIGHT-TO-LEFT OVERRIDE
| '\u{2060}' // WORD JOINER
| '\u{2061}' // FUNCTION APPLICATION
| '\u{2062}' // INVISIBLE TIMES
| '\u{2063}' // INVISIBLE SEPARATOR
| '\u{2064}' // INVISIBLE PLUS
| '\u{2066}' // LEFT-TO-RIGHT ISOLATE
| '\u{2067}' // RIGHT-TO-LEFT ISOLATE
| '\u{2068}' // FIRST STRONG ISOLATE
| '\u{2069}' // POP DIRECTIONAL ISOLATE
| '\u{206A}' // INHIBIT SYMMETRIC SWAPPING
| '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING
| '\u{206C}' // INHIBIT ARABIC FORM SHAPING
| '\u{206D}' // ACTIVATE ARABIC FORM SHAPING
| '\u{206E}' // NATIONAL DIGIT SHAPES
| '\u{206F}' // NOMINAL DIGIT SHAPES
| '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
)
}
fn allocate_glyph(

View file

@ -40,6 +40,7 @@ skip-tree = [
{ name = "glium" }, # legacy crate, lots of old dependencies
{ name = "rfd" }, # example dependency
{ name = "three-d" }, # example dependency
{ name = "tts" }, # we are migrating away from tts to accesskit
]

View file

@ -84,15 +84,6 @@ fn custom_window_frame(
Stroke::new(1.0, text_color),
);
// Add the close button:
let close_response = ui.put(
Rect::from_min_size(rect.left_top(), Vec2::splat(height)),
Button::new(RichText::new("").size(height - 4.0)).frame(false),
);
if close_response.clicked() {
frame.close();
}
// Interact with the title bar (drag to move window):
let title_bar_rect = {
let mut rect = rect;
@ -105,6 +96,15 @@ fn custom_window_frame(
frame.drag_window();
}
// Add the close button:
let close_response = ui.put(
Rect::from_min_size(rect.left_top(), Vec2::splat(height)),
Button::new(RichText::new("").size(height - 4.0)).frame(false),
);
if close_response.clicked() {
frame.close();
}
// Add the contents:
let content_rect = {
let mut rect = rect;

View file

@ -65,9 +65,11 @@ impl eframe::App for MyApp {
preview_files_being_dropped(ctx);
// Collect dropped files:
if !ctx.input().raw.dropped_files.is_empty() {
self.dropped_files = ctx.input().raw.dropped_files.clone();
}
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
self.dropped_files = i.raw.dropped_files.clone();
}
});
}
}
@ -76,24 +78,27 @@ fn preview_files_being_dropped(ctx: &egui::Context) {
use egui::*;
use std::fmt::Write as _;
if !ctx.input().raw.hovered_files.is_empty() {
let mut text = "Dropping files:\n".to_owned();
for file in &ctx.input().raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
text += "\n???";
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
let text = ctx.input(|i| {
let mut text = "Dropping files:\n".to_owned();
for file in &i.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
text += "\n???";
}
}
}
text
});
let painter = ctx.layer_painter(AreaLayerId::new(
Order::Foreground,
Id::new("file_drop_target"),
));
let screen_rect = ctx.input().screen_rect();
let screen_rect = ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),

View file

@ -0,0 +1,16 @@
[package]
name = "hello_world_par"
version = "0.1.0"
authors = ["Maxim Osipenko <maxim1999max@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.65"
publish = false
[dependencies]
eframe = { path = "../../crates/eframe", default-features = false, features = [
# accesskit struggles with threading
"default_fonts",
"wgpu",
] }

View file

@ -0,0 +1,5 @@
This example shows that you can use egui in parallel from multiple threads.
```sh
cargo run -p hello_world_par
```

View file

@ -0,0 +1,132 @@
//! This example shows that you can use egui in parallel from multiple threads.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::sync::mpsc;
use std::thread::JoinHandle;
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(1024.0, 768.0)),
..Default::default()
};
eframe::run_native(
"My parallel egui App",
options,
Box::new(|_cc| Box::new(MyApp::new())),
)
}
/// State per thread.
struct ThreadState {
thread_nr: usize,
title: String,
name: String,
age: u32,
}
impl ThreadState {
fn new(thread_nr: usize) -> Self {
let title = format!("Background thread {thread_nr}");
Self {
thread_nr,
title,
name: "Arthur".into(),
age: 12 + thread_nr as u32 * 10,
}
}
fn show(&mut self, ctx: &egui::Context) {
let pos = egui::pos2(16.0, 128.0 * (self.thread_nr as f32 + 1.0));
egui::Window::new(&self.title)
.default_pos(pos)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));
});
}
}
fn new_worker(
thread_nr: usize,
on_done_tx: mpsc::SyncSender<()>,
) -> (JoinHandle<()>, mpsc::SyncSender<egui::Context>) {
let (show_tx, show_rc) = mpsc::sync_channel(0);
let handle = std::thread::Builder::new()
.name(format!("EguiPanelWorker {}", thread_nr))
.spawn(move || {
let mut state = ThreadState::new(thread_nr);
while let Ok(ctx) = show_rc.recv() {
state.show(&ctx);
let _ = on_done_tx.send(());
}
})
.expect("failed to spawn thread");
(handle, show_tx)
}
struct MyApp {
threads: Vec<(JoinHandle<()>, mpsc::SyncSender<egui::Context>)>,
on_done_tx: mpsc::SyncSender<()>,
on_done_rc: mpsc::Receiver<()>,
}
impl MyApp {
fn new() -> Self {
let threads = Vec::with_capacity(3);
let (on_done_tx, on_done_rc) = mpsc::sync_channel(0);
let mut slf = Self {
threads,
on_done_tx,
on_done_rc,
};
slf.spawn_thread();
slf.spawn_thread();
slf
}
fn spawn_thread(&mut self) {
let thread_nr = self.threads.len();
self.threads
.push(new_worker(thread_nr, self.on_done_tx.clone()));
}
}
impl std::ops::Drop for MyApp {
fn drop(&mut self) {
for (handle, show_tx) in self.threads.drain(..) {
std::mem::drop(show_tx);
handle.join().unwrap();
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::Window::new("Main thread").show(ctx, |ui| {
if ui.button("Spawn another thread").clicked() {
self.spawn_thread();
}
});
for (_handle, show_tx) in &self.threads {
let _ = show_tx.send(ctx.clone());
}
for _ in 0..self.threads.len() {
let _ = self.on_done_rc.recv();
}
}
}

View file

@ -1,5 +1,5 @@
```sh
cargo run -p hello_world
cargo run -p keyboard_events
```
![](screenshot.png)

View file

@ -33,14 +33,14 @@ impl eframe::App for Content {
ui.label(&self.text);
});
if ctx.input().key_pressed(Key::A) {
if ctx.input(|i| i.key_pressed(Key::A)) {
self.text.push_str("\nPressed");
}
if ctx.input().key_down(Key::A) {
if ctx.input(|i| i.key_down(Key::A)) {
self.text.push_str("\nHeld");
ui.ctx().request_repaint(); // make sure we note the holding.
}
if ctx.input().key_released(Key::A) {
if ctx.input(|i| i.key_released(Key::A)) {
self.text.push_str("\nReleased");
}
});

View file

@ -28,7 +28,7 @@ impl eframe::App for MyApp {
ui.horizontal(|ui| {
ui.monospace(cmd);
if ui.small_button("📋").clicked() {
ui.output().copied_text = cmd.into();
ui.output_mut(|o| o.copied_text = cmd.into());
}
});