Add `Context::request_discard` (#5059)
* Closes https://github.com/emilk/egui/issues/4976 * Part of #4378 * Implements parts of #843 ### Background Some widgets (like `Grid` and `Table`) needs to know the width of future elements in order to properly size themselves. For instance, the width of the first column of a grid may not be known until all rows of the grid has been added, at which point it is too late. Therefore these widgets store sizes from the previous frame. This leads to "first-frame jitter", were the content is placed in the wrong place for one frame, before being accurately laid out in subsequent frames. ### What This PR adds the function `ctx.request_discard` which discards the visual output and does another _pass_, i.e. calls the whole app UI code once again (in eframe this means calling `App::update` again). This will thus discard the shapes produced by the wrongly placed widgets, and replace it with new shapes. Note that only the visual output is discarded - all other output events are accumulated. Calling `ctx.request_discard` should only be done in very rare circumstances, e.g. when a `Grid` is first shown. Calling it every frame will mean the UI code will become unnecessarily slow. Two safe-guards are in place: * `Options::max_passes` is by default 2, meaning egui will never do more than 2 passes even if `request_discard` is called on every pass * If multiple passes is done for multiple frames in a row, a warning will be printed on the screen in debug builds:  ### Breaking changes A bunch of things that had "frame" in the name now has "pass" in them instead: * Functions called `begin_frame` and `end_frame` are now called `begin_pass` and `end_pass` * `FrameState` is now `PassState` * etc ### TODO * [x] Figure out good names for everything (`ctx.request_discard`) * [x] Add API to query if we're gonna repeat this frame (to early-out from expensive rendering) * [x] Clear up naming confusion (pass vs frame) e.g. for `FrameState` * [x] Figure out when to call this * [x] Show warning on screen when there are several frames in a row with multiple passes * [x] Document * [x] Default on or off? * [x] Change `Context::frame_nr` name/docs * [x] Rename `Context::begin_frame/end_frame` and deprecate the old ones * [x] Test with Rerun * [x] Document breaking changes
This commit is contained in:
parent
08f5eb30a4
commit
66076101e1
|
|
@ -98,8 +98,7 @@ On Fedora Rawhide you need to run:
|
|||
* Portable: the same code works on the web and as a native app
|
||||
* Easy to integrate into any environment
|
||||
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
|
||||
* No callbacks
|
||||
* Pure immediate mode
|
||||
* Pure immediate mode: no callbacks
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||
* Safe: there is no `unsafe` code in egui
|
||||
|
|
@ -113,7 +112,6 @@ egui is *not* a framework. egui is a library you call into, not an environment y
|
|||
|
||||
* Become the most powerful GUI library
|
||||
* Native looking interface
|
||||
* Advanced and flexible layouts (that's fundamentally incompatible with immediate mode)
|
||||
|
||||
## State
|
||||
|
||||
|
|
@ -250,7 +248,8 @@ This is a fundamental shortcoming of immediate mode GUIs, and any attempt to res
|
|||
|
||||
One workaround is to store the size and use it the next frame. This produces a frame-delay for the correct layout, producing occasional flickering the first frame something shows up. `egui` does this for some things such as windows and grid layouts.
|
||||
|
||||
You can also call the layout code twice (once to get the size, once to do the interaction), but that is not only more expensive, it's also complex to implement, and in some cases twice is not enough. `egui` never does this.
|
||||
The "first-frame jitter" can be covered up with an extra _pass_, which egui supports via `Context::request_discard`.
|
||||
The downside of this is the added CPU cost of a second pass, so egui only does this in very rare circumstances (the majority of frames are single-pass).
|
||||
|
||||
For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so centering buttons, labels etc is possible in `egui` without any special workarounds.
|
||||
|
||||
|
|
|
|||
|
|
@ -251,13 +251,13 @@ impl<'app> GlowWinitApp<'app> {
|
|||
.set_request_repaint_callback(move |info| {
|
||||
log::trace!("request_repaint_callback: {info:?}");
|
||||
let when = Instant::now() + info.delay;
|
||||
let frame_nr = info.current_frame_nr;
|
||||
let cumulative_pass_nr = info.current_cumulative_pass_nr;
|
||||
event_loop_proxy
|
||||
.lock()
|
||||
.send_event(UserEvent::RequestRepaint {
|
||||
viewport_id: info.viewport_id,
|
||||
when,
|
||||
frame_nr,
|
||||
cumulative_pass_nr,
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
|
@ -346,10 +346,8 @@ impl<'app> GlowWinitApp<'app> {
|
|||
}
|
||||
|
||||
impl<'app> WinitApp for GlowWinitApp<'app> {
|
||||
fn frame_nr(&self, viewport_id: ViewportId) -> u64 {
|
||||
self.running
|
||||
.as_ref()
|
||||
.map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id))
|
||||
fn egui_ctx(&self) -> Option<&egui::Context> {
|
||||
self.running.as_ref().map(|r| &r.integration.egui_ctx)
|
||||
}
|
||||
|
||||
fn window(&self, window_id: WindowId) -> Option<Arc<Window>> {
|
||||
|
|
@ -712,7 +710,7 @@ impl<'app> GlowWinitRunning<'app> {
|
|||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.frame_nr() == 2 {
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2 {
|
||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
|
|
@ -1397,7 +1395,7 @@ fn render_immediate_viewport(
|
|||
let ImmediateViewport {
|
||||
ids,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut viewport_ui_cb,
|
||||
} = immediate_viewport;
|
||||
|
||||
let viewport_id = ids.this;
|
||||
|
|
|
|||
|
|
@ -228,11 +228,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
|||
let event_result = match event {
|
||||
UserEvent::RequestRepaint {
|
||||
when,
|
||||
frame_nr,
|
||||
cumulative_pass_nr,
|
||||
viewport_id,
|
||||
} => {
|
||||
let current_frame_nr = self.winit_app.frame_nr(viewport_id);
|
||||
if current_frame_nr == frame_nr || current_frame_nr == frame_nr + 1 {
|
||||
let current_pass_nr = self
|
||||
.winit_app
|
||||
.egui_ctx()
|
||||
.map_or(0, |ctx| ctx.cumulative_pass_nr_for(viewport_id));
|
||||
if current_pass_nr == cumulative_pass_nr
|
||||
|| current_pass_nr == cumulative_pass_nr + 1
|
||||
{
|
||||
log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}");
|
||||
if let Some(window_id) =
|
||||
self.winit_app.window_id_from_viewport_id(viewport_id)
|
||||
|
|
|
|||
|
|
@ -223,13 +223,13 @@ impl<'app> WgpuWinitApp<'app> {
|
|||
egui_ctx.set_request_repaint_callback(move |info| {
|
||||
log::trace!("request_repaint_callback: {info:?}");
|
||||
let when = Instant::now() + info.delay;
|
||||
let frame_nr = info.current_frame_nr;
|
||||
let cumulative_pass_nr = info.current_cumulative_pass_nr;
|
||||
|
||||
event_loop_proxy
|
||||
.lock()
|
||||
.send_event(UserEvent::RequestRepaint {
|
||||
when,
|
||||
frame_nr,
|
||||
cumulative_pass_nr,
|
||||
viewport_id: info.viewport_id,
|
||||
})
|
||||
.ok();
|
||||
|
|
@ -324,10 +324,8 @@ impl<'app> WgpuWinitApp<'app> {
|
|||
}
|
||||
|
||||
impl<'app> WinitApp for WgpuWinitApp<'app> {
|
||||
fn frame_nr(&self, viewport_id: ViewportId) -> u64 {
|
||||
self.running
|
||||
.as_ref()
|
||||
.map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id))
|
||||
fn egui_ctx(&self) -> Option<&egui::Context> {
|
||||
self.running.as_ref().map(|r| &r.integration.egui_ctx)
|
||||
}
|
||||
|
||||
fn window(&self, window_id: WindowId) -> Option<Arc<Window>> {
|
||||
|
|
@ -916,7 +914,7 @@ fn render_immediate_viewport(
|
|||
let ImmediateViewport {
|
||||
ids,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut viewport_ui_cb,
|
||||
} = immediate_viewport;
|
||||
|
||||
let input = {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Contex
|
|||
|
||||
egui_ctx.set_embed_viewports(!IS_DESKTOP);
|
||||
|
||||
egui_ctx.options_mut(|o| {
|
||||
// eframe supports multi-pass (Context::request_discard).
|
||||
o.max_passes = 2.try_into().unwrap();
|
||||
});
|
||||
|
||||
let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default();
|
||||
egui_ctx.memory_mut(|mem| *mem = memory);
|
||||
|
||||
|
|
@ -42,8 +47,8 @@ pub enum UserEvent {
|
|||
/// When to repaint.
|
||||
when: Instant,
|
||||
|
||||
/// What the frame number was when the repaint was _requested_.
|
||||
frame_nr: u64,
|
||||
/// What the cumulative pass number was when the repaint was _requested_.
|
||||
cumulative_pass_nr: u64,
|
||||
},
|
||||
|
||||
/// A request related to [`accesskit`](https://accesskit.dev/).
|
||||
|
|
@ -59,8 +64,7 @@ impl From<accesskit_winit::Event> for UserEvent {
|
|||
}
|
||||
|
||||
pub trait WinitApp {
|
||||
/// The current frame number, as reported by egui.
|
||||
fn frame_nr(&self, viewport_id: ViewportId) -> u64;
|
||||
fn egui_ctx(&self) -> Option<&egui::Context>;
|
||||
|
||||
fn window(&self, window_id: WindowId) -> Option<Arc<Window>>;
|
||||
|
||||
|
|
|
|||
|
|
@ -269,6 +269,8 @@ impl AppRunner {
|
|||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
num_completed_passes: _, // handled by `Context::run`
|
||||
requested_discard: _, // handled by `Context::run`
|
||||
} = platform_output;
|
||||
|
||||
super::set_cursor_icon(cursor_icon);
|
||||
|
|
|
|||
|
|
@ -823,6 +823,8 @@ impl State {
|
|||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
num_completed_passes: _, // `egui::Context::run` handles this
|
||||
requested_discard: _, // `egui::Context::run` handles this
|
||||
} = platform_output;
|
||||
|
||||
self.set_cursor_icon(window, cursor_icon);
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBe
|
|||
/// A drop-down selection menu with a descriptive label.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # #[derive(Debug, PartialEq)]
|
||||
/// # enum Enum { First, Second, Third }
|
||||
/// # let mut selected = Enum::First;
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// egui::ComboBox::from_label("Select one!")
|
||||
/// .selected_text(format!("{:?}", selected))
|
||||
/// .show_ui(ui, |ui| {
|
||||
|
|
|
|||
|
|
@ -390,10 +390,10 @@ impl SidePanel {
|
|||
let rect = inner_response.response.rect;
|
||||
|
||||
match side {
|
||||
Side::Left => ctx.frame_state_mut(|state| {
|
||||
Side::Left => ctx.pass_state_mut(|state| {
|
||||
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||
}),
|
||||
Side::Right => ctx.frame_state_mut(|state| {
|
||||
Side::Right => ctx.pass_state_mut(|state| {
|
||||
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||
}),
|
||||
}
|
||||
|
|
@ -885,12 +885,12 @@ impl TopBottomPanel {
|
|||
|
||||
match side {
|
||||
TopBottomSide::Top => {
|
||||
ctx.frame_state_mut(|state| {
|
||||
ctx.pass_state_mut(|state| {
|
||||
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||
});
|
||||
}
|
||||
TopBottomSide::Bottom => {
|
||||
ctx.frame_state_mut(|state| {
|
||||
ctx.pass_state_mut(|state| {
|
||||
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||
});
|
||||
}
|
||||
|
|
@ -1149,7 +1149,7 @@ impl CentralPanel {
|
|||
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_mut(|state| state.allocate_central_panel(inner_response.response.rect));
|
||||
ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
|
||||
|
||||
inner_response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
//! Show popup windows, tooltips, context menus etc.
|
||||
|
||||
use frame_state::PerWidgetTooltipState;
|
||||
use pass_state::PerWidgetTooltipState;
|
||||
|
||||
use crate::{
|
||||
frame_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
|
||||
pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
|
||||
InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2,
|
||||
Widget, WidgetText,
|
||||
};
|
||||
|
|
@ -162,7 +162,7 @@ fn show_tooltip_at_dyn<'c, R>(
|
|||
|
||||
remember_that_tooltip_was_shown(ctx);
|
||||
|
||||
let mut state = ctx.frame_state_mut(|fs| {
|
||||
let mut state = ctx.pass_state_mut(|fs| {
|
||||
// Remember that this is the widget showing the tooltip:
|
||||
fs.layers
|
||||
.entry(parent_layer)
|
||||
|
|
@ -213,14 +213,14 @@ fn show_tooltip_at_dyn<'c, R>(
|
|||
|
||||
state.tooltip_count += 1;
|
||||
state.bounding_rect = state.bounding_rect.union(response.rect);
|
||||
ctx.frame_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
|
||||
ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
/// What is the id of the next tooltip for this widget?
|
||||
pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
|
||||
let tooltip_count = ctx.frame_state(|fs| {
|
||||
let tooltip_count = ctx.pass_state(|fs| {
|
||||
fs.tooltips
|
||||
.widget_tooltips
|
||||
.get(&widget_id)
|
||||
|
|
@ -409,7 +409,7 @@ pub fn popup_above_or_below_widget<R>(
|
|||
let frame_margin = frame.total_margin();
|
||||
let inner_width = widget_response.rect.width() - frame_margin.sum().x;
|
||||
|
||||
parent_ui.ctx().frame_state_mut(|fs| {
|
||||
parent_ui.ctx().pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.entry(parent_ui.layer_id())
|
||||
.or_default()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use crate::{
|
||||
emath, epaint, frame_state, lerp, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
|
||||
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
|
||||
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
||||
};
|
||||
|
||||
|
|
@ -819,10 +819,10 @@ impl Prepared {
|
|||
|
||||
let scroll_delta = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| std::mem::take(&mut state.scroll_delta));
|
||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
|
||||
|
||||
for d in 0..2 {
|
||||
// FrameState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
|
||||
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
|
||||
let mut delta = -scroll_delta.0[d];
|
||||
let mut animation = scroll_delta.1;
|
||||
|
||||
|
|
@ -830,11 +830,11 @@ impl Prepared {
|
|||
// is to avoid them leaking to other scroll areas.
|
||||
let scroll_target = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||
.pass_state_mut(|state| state.scroll_target[d].take());
|
||||
|
||||
if scroll_enabled[d] {
|
||||
if let Some(target) = scroll_target {
|
||||
let frame_state::ScrollTarget {
|
||||
let pass_state::ScrollTarget {
|
||||
range,
|
||||
align,
|
||||
animation: animation_update,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -43,7 +43,7 @@ impl FullOutput {
|
|||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output: viewports,
|
||||
viewport_output,
|
||||
} = newer;
|
||||
|
||||
self.platform_output.append(platform_output);
|
||||
|
|
@ -51,7 +51,7 @@ impl FullOutput {
|
|||
self.shapes = shapes; // Only paint the latest
|
||||
self.pixels_per_point = pixels_per_point; // Use latest
|
||||
|
||||
for (id, new_viewport) in viewports {
|
||||
for (id, new_viewport) in viewport_output {
|
||||
match self.viewport_output.entry(id) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(new_viewport);
|
||||
|
|
@ -123,6 +123,17 @@ pub struct PlatformOutput {
|
|||
/// NOTE: this needs to be per-viewport.
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||
|
||||
/// How many ui passes is this the sum of?
|
||||
///
|
||||
/// See [`crate::Context::request_discard`] for details.
|
||||
///
|
||||
/// This is incremented at the END of each frame,
|
||||
/// so this will be `0` for the first pass.
|
||||
pub num_completed_passes: usize,
|
||||
|
||||
/// Was [`crate::Context::request_discard`] called during the latest pass?
|
||||
pub requested_discard: bool,
|
||||
}
|
||||
|
||||
impl PlatformOutput {
|
||||
|
|
@ -155,6 +166,8 @@ impl PlatformOutput {
|
|||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
num_completed_passes,
|
||||
requested_discard,
|
||||
} = newer;
|
||||
|
||||
self.cursor_icon = cursor_icon;
|
||||
|
|
@ -167,6 +180,8 @@ impl PlatformOutput {
|
|||
self.events.append(&mut events);
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
self.ime = ime.or(self.ime);
|
||||
self.num_completed_passes += num_completed_passes;
|
||||
self.requested_discard |= requested_discard;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,23 +3,23 @@
|
|||
//! A plugin usually consist of a struct that holds some state,
|
||||
//! which is stored using [`Context::data_mut`].
|
||||
//! The plugin registers itself onto a specific [`Context`]
|
||||
//! to get callbacks on certain events ([`Context::on_begin_frame`], [`Context::on_end_frame`]).
|
||||
//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]).
|
||||
|
||||
use crate::{
|
||||
text, Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText,
|
||||
};
|
||||
|
||||
/// Register this plugin on the given egui context,
|
||||
/// so that it will be called every frame.
|
||||
/// so that it will be called every pass.
|
||||
///
|
||||
/// This is a built-in plugin in egui,
|
||||
/// meaning [`Context`] calls this from its `Default` implementation,
|
||||
/// so this is marked as `pub(crate)`.
|
||||
pub(crate) fn register(ctx: &Context) {
|
||||
ctx.on_end_frame("debug_text", std::sync::Arc::new(State::end_frame));
|
||||
ctx.on_end_pass("debug_text", std::sync::Arc::new(State::end_pass));
|
||||
}
|
||||
|
||||
/// Print this text next to the cursor at the end of the frame.
|
||||
/// Print this text next to the cursor at the end of the pass.
|
||||
///
|
||||
/// If you call this multiple times, the text will be appended.
|
||||
///
|
||||
|
|
@ -61,12 +61,12 @@ struct Entry {
|
|||
/// This is a built-in plugin in egui.
|
||||
#[derive(Clone, Default)]
|
||||
struct State {
|
||||
// This gets re-filled every frame.
|
||||
// This gets re-filled every pass.
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn end_frame(ctx: &Context) {
|
||||
fn end_pass(ctx: &Context) {
|
||||
let state = ctx.data_mut(|data| data.remove_temp::<Self>(Id::NULL));
|
||||
if let Some(state) = state {
|
||||
state.paint(ctx);
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ pub struct DragAndDrop {
|
|||
|
||||
impl DragAndDrop {
|
||||
pub(crate) fn register(ctx: &Context) {
|
||||
ctx.on_end_frame("debug_text", std::sync::Arc::new(Self::end_frame));
|
||||
ctx.on_end_pass("debug_text", std::sync::Arc::new(Self::end_pass));
|
||||
}
|
||||
|
||||
fn end_frame(ctx: &Context) {
|
||||
fn end_pass(ctx: &Context) {
|
||||
let abort_dnd =
|
||||
ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape));
|
||||
|
||||
|
|
|
|||
|
|
@ -434,7 +434,14 @@ impl Grid {
|
|||
|
||||
let mut ui_builder = UiBuilder::new().max_rect(max_rect);
|
||||
if prev_state.is_none() {
|
||||
// Hide the ui this frame, and make things as narrow as possible.
|
||||
// The initial frame will be glitchy, because we don't know the sizes of things to come.
|
||||
|
||||
if ui.is_visible() {
|
||||
// Try to cover up the glitchy initial frame:
|
||||
ui.ctx().request_discard();
|
||||
}
|
||||
|
||||
// Hide the ui this frame, and make things as narrow as possible:
|
||||
ui_builder = ui_builder.sizing_pass().invisible();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ impl Default for InputState {
|
|||
|
||||
impl InputState {
|
||||
#[must_use]
|
||||
pub fn begin_frame(
|
||||
pub fn begin_pass(
|
||||
mut self,
|
||||
mut new: RawInput,
|
||||
requested_immediate_repaint_prev_frame: bool,
|
||||
|
|
@ -285,9 +285,9 @@ impl InputState {
|
|||
let screen_rect = new.screen_rect.unwrap_or(self.screen_rect);
|
||||
self.create_touch_states_for_new_devices(&new.events);
|
||||
for touch_state in self.touch_states.values_mut() {
|
||||
touch_state.begin_frame(time, &new, self.pointer.interact_pos);
|
||||
touch_state.begin_pass(time, &new, self.pointer.interact_pos);
|
||||
}
|
||||
let pointer = self.pointer.begin_frame(time, &new, options);
|
||||
let pointer = self.pointer.begin_pass(time, &new, options);
|
||||
|
||||
let mut keys_down = self.keys_down;
|
||||
let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor
|
||||
|
|
@ -900,7 +900,7 @@ impl Default for PointerState {
|
|||
|
||||
impl PointerState {
|
||||
#[must_use]
|
||||
pub(crate) fn begin_frame(
|
||||
pub(crate) fn begin_pass(
|
||||
mut self,
|
||||
time: f64,
|
||||
new: &RawInput,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ impl TouchState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self, time: f64, new: &RawInput, pointer_pos: Option<Pos2>) {
|
||||
pub fn begin_pass(&mut self, time: f64, new: &RawInput, pointer_pos: Option<Pos2>) {
|
||||
let mut added_or_removed_touches = false;
|
||||
for event in &new.events {
|
||||
match *event {
|
||||
|
|
|
|||
|
|
@ -223,6 +223,25 @@
|
|||
//!
|
||||
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
|
||||
//!
|
||||
//! ## Multi-pass immediate mode
|
||||
//! By default, egui usually only does one pass for each rendered frame.
|
||||
//! However, egui supports multi-pass immediate mode.
|
||||
//! Another pass can be requested with [`Context::request_discard`].
|
||||
//!
|
||||
//! This is used by some widgets to cover up "first-frame jitters".
|
||||
//! For instance, the [`Grid`] needs to know the width of all columns before it can properly place the widgets.
|
||||
//! But it cannot know the width of widgets to come.
|
||||
//! So it stores the max widths of previous frames and uses that.
|
||||
//! This means the first time a `Grid` is shown it will _guess_ the widths of the columns, and will usually guess wrong.
|
||||
//! This means the contents of the grid will be wrong for one frame, before settling to the correct places.
|
||||
//! Therefore `Grid` calls [`Context::request_discard`] when it is first shown, so the wrong placement is never
|
||||
//! visible to the end user.
|
||||
//!
|
||||
//! This is an example of a form of multi-pass immediate mode, where earlier passes are used for sizing,
|
||||
//! and later passes for layout.
|
||||
//!
|
||||
//! See [`Context::request_discard`] and [`Options::max_passes`] for more.
|
||||
//!
|
||||
//! # Misc
|
||||
//!
|
||||
//! ## How widgets works
|
||||
|
|
@ -379,7 +398,6 @@ mod context;
|
|||
mod data;
|
||||
pub mod debug_text;
|
||||
mod drag_and_drop;
|
||||
mod frame_state;
|
||||
pub(crate) mod grid;
|
||||
pub mod gui_zoom;
|
||||
mod hit_test;
|
||||
|
|
@ -394,6 +412,7 @@ mod memory;
|
|||
pub mod menu;
|
||||
pub mod os;
|
||||
mod painter;
|
||||
mod pass_state;
|
||||
pub(crate) mod placer;
|
||||
mod response;
|
||||
mod sense;
|
||||
|
|
@ -655,7 +674,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
|||
}
|
||||
|
||||
/// For use in tests; especially doctests.
|
||||
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||
pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run(Default::default(), |ctx| {
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ pub trait BytesLoader {
|
|||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
fn end_pass(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
|
|
@ -367,9 +367,9 @@ pub trait ImageLoader {
|
|||
/// so that all of them may be fully reloaded.
|
||||
fn forget_all(&self);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// Implementations may use this to perform work at the end of a pass,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
fn end_pass(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
|
|
@ -505,9 +505,9 @@ pub trait TextureLoader {
|
|||
/// so that all of them may be fully reloaded.
|
||||
fn forget_all(&self);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// Implementations may use this to perform work at the end of a pass,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
fn end_pass(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ impl TextureLoader for DefaultTextureLoader {
|
|||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn end_frame(&self, _: usize) {}
|
||||
fn end_pass(&self, _: usize) {}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ahash::{HashMap, HashSet};
|
||||
use epaint::emath::TSTransform;
|
||||
|
||||
|
|
@ -228,6 +230,23 @@ pub struct Options {
|
|||
/// (<https://github.com/rerun-io/rerun/issues/5018>).
|
||||
pub repaint_on_widget_change: bool,
|
||||
|
||||
/// Maximum number of passes to run in one frame.
|
||||
///
|
||||
/// Set to `1` for pure single-pass immediate mode.
|
||||
/// Set to something larger than `1` to allow multi-pass when needed.
|
||||
///
|
||||
/// Default is `2`. This means sometimes a frame will cost twice as much,
|
||||
/// but usually only rarely (e.g. when showing a new panel for the first time).
|
||||
///
|
||||
/// egui will usually only ever run one pass, even if `max_passes` is large.
|
||||
///
|
||||
/// If this is `1`, [`crate::Context::request_discard`] will be ignored.
|
||||
///
|
||||
/// Multi-pass is supported by [`crate::Context::run`].
|
||||
///
|
||||
/// See [`crate::Context::request_discard`] for more.
|
||||
pub max_passes: NonZeroUsize,
|
||||
|
||||
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||
///
|
||||
/// The only change to egui is that labels can be focused by pressing tab.
|
||||
|
|
@ -297,6 +316,7 @@ impl Default for Options {
|
|||
zoom_with_keyboard: true,
|
||||
tessellation_options: Default::default(),
|
||||
repaint_on_widget_change: false,
|
||||
max_passes: NonZeroUsize::new(2).unwrap(),
|
||||
screen_reader: false,
|
||||
preload_font_glyphs: true,
|
||||
warn_on_id_clash: cfg!(debug_assertions),
|
||||
|
|
@ -311,7 +331,7 @@ impl Default for Options {
|
|||
}
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput) {
|
||||
pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput) {
|
||||
self.system_theme = new_raw_input.system_theme;
|
||||
}
|
||||
|
||||
|
|
@ -352,6 +372,7 @@ impl Options {
|
|||
zoom_with_keyboard,
|
||||
tessellation_options,
|
||||
repaint_on_widget_change,
|
||||
max_passes,
|
||||
screen_reader: _, // needs to come from the integration
|
||||
preload_font_glyphs: _,
|
||||
warn_on_id_clash,
|
||||
|
|
@ -367,6 +388,11 @@ impl Options {
|
|||
CollapsingHeader::new("⚙ Options")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Max passes:");
|
||||
ui.add(crate::DragValue::new(max_passes).range(0..=10));
|
||||
});
|
||||
|
||||
ui.checkbox(
|
||||
repaint_on_widget_change,
|
||||
"Repaint if any widget moves or changes id",
|
||||
|
|
@ -515,7 +541,7 @@ impl Focus {
|
|||
self.focused_widget.as_ref().map(|w| w.id)
|
||||
}
|
||||
|
||||
fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
|
||||
fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) {
|
||||
self.id_previous_frame = self.focused();
|
||||
if let Some(id) = self.id_next_frame.take() {
|
||||
self.focused_widget = Some(FocusWidget::new(id));
|
||||
|
|
@ -576,7 +602,7 @@ impl Focus {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
|
||||
pub(crate) fn end_pass(&mut self, used_ids: &IdMap<Rect>) {
|
||||
if self.focus_direction.is_cardinal() {
|
||||
if let Some(found_widget) = self.find_widget_in_direction(used_ids) {
|
||||
self.focused_widget = Some(FocusWidget::new(found_widget));
|
||||
|
|
@ -726,7 +752,7 @@ impl Focus {
|
|||
}
|
||||
|
||||
impl Memory {
|
||||
pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
|
||||
pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
|
||||
crate::profile_function!();
|
||||
|
||||
self.viewport_id = new_raw_input.viewport_id;
|
||||
|
|
@ -739,18 +765,18 @@ impl Memory {
|
|||
|
||||
// self.interactions is handled elsewhere
|
||||
|
||||
self.options.begin_frame(new_raw_input);
|
||||
self.options.begin_pass(new_raw_input);
|
||||
|
||||
self.focus
|
||||
.entry(self.viewport_id)
|
||||
.or_default()
|
||||
.begin_frame(new_raw_input);
|
||||
.begin_pass(new_raw_input);
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
|
||||
pub(crate) fn end_pass(&mut self, used_ids: &IdMap<Rect>) {
|
||||
self.caches.update();
|
||||
self.areas_mut().end_frame();
|
||||
self.focus_mut().end_frame(used_ids);
|
||||
self.areas_mut().end_pass();
|
||||
self.focus_mut().end_pass(used_ids);
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
|
||||
|
|
@ -1149,7 +1175,7 @@ impl Areas {
|
|||
.any(|(_, children)| children.contains(layer))
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self) {
|
||||
pub(crate) fn end_pass(&mut self) {
|
||||
let Self {
|
||||
visible_last_frame,
|
||||
visible_current_frame,
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ fn menu_popup<'c, R>(
|
|||
|
||||
let area_id = menu_id.with("__menu");
|
||||
|
||||
ctx.frame_state_mut(|fs| {
|
||||
ctx.pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.entry(parent_layer)
|
||||
.or_default()
|
||||
|
|
|
|||
|
|
@ -112,8 +112,11 @@ impl Painter {
|
|||
self.opacity_factor
|
||||
}
|
||||
|
||||
/// If `false`, nothing you paint will show up.
|
||||
///
|
||||
/// Also checks [`Context::will_discard`].
|
||||
pub(crate) fn is_visible(&self) -> bool {
|
||||
self.fade_to_color != Some(Color32::TRANSPARENT)
|
||||
self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
|
||||
}
|
||||
|
||||
/// If `false`, nothing added to the painter will be visible
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ use crate::{pos2, Align2, Color32, FontId, NumExt, Painter};
|
|||
|
||||
/// Reset at the start of each frame.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TooltipFrameState {
|
||||
pub struct TooltipPassState {
|
||||
/// If a tooltip has been shown this frame, where was it?
|
||||
/// This is used to prevent multiple tooltips to cover each other.
|
||||
pub widget_tooltips: IdMap<PerWidgetTooltipState>,
|
||||
}
|
||||
|
||||
impl TooltipFrameState {
|
||||
impl TooltipPassState {
|
||||
pub fn clear(&mut self) {
|
||||
let Self { widget_tooltips } = self;
|
||||
widget_tooltips.clear();
|
||||
|
|
@ -69,7 +69,7 @@ impl ScrollTarget {
|
|||
|
||||
#[cfg(feature = "accesskit")]
|
||||
#[derive(Clone)]
|
||||
pub struct AccessKitFrameState {
|
||||
pub struct AccessKitPassState {
|
||||
pub node_builders: IdMap<accesskit::NodeBuilder>,
|
||||
pub parent_stack: Vec<Id>,
|
||||
}
|
||||
|
|
@ -167,16 +167,18 @@ impl DebugRect {
|
|||
}
|
||||
}
|
||||
|
||||
/// State that is collected during a frame, then saved for the next frame,
|
||||
/// State that is collected during a pass, then saved for the next pass,
|
||||
/// and then cleared.
|
||||
///
|
||||
/// (NOTE: we usually run only one pass per frame).
|
||||
///
|
||||
/// One per viewport.
|
||||
#[derive(Clone)]
|
||||
pub struct FrameState {
|
||||
/// All [`Id`]s that were used this frame.
|
||||
pub struct PassState {
|
||||
/// All [`Id`]s that were used this pass.
|
||||
pub used_ids: IdMap<Rect>,
|
||||
|
||||
/// All widgets produced this frame.
|
||||
/// All widgets produced this pass.
|
||||
pub widgets: WidgetRects,
|
||||
|
||||
/// Per-layer state.
|
||||
|
|
@ -184,7 +186,7 @@ pub struct FrameState {
|
|||
/// Not all layers registers themselves there though.
|
||||
pub layers: HashMap<LayerId, PerLayerState>,
|
||||
|
||||
pub tooltips: TooltipFrameState,
|
||||
pub tooltips: TooltipPassState,
|
||||
|
||||
/// Starts off as the `screen_rect`, shrinks as panels are added.
|
||||
/// The [`crate::CentralPanel`] does not change this.
|
||||
|
|
@ -213,16 +215,16 @@ pub struct FrameState {
|
|||
pub scroll_delta: (Vec2, style::ScrollAnimation),
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_state: Option<AccessKitFrameState>,
|
||||
pub accesskit_state: Option<AccessKitPassState>,
|
||||
|
||||
/// Highlight these widgets the next frame.
|
||||
pub highlight_next_frame: IdSet,
|
||||
/// Highlight these widgets the next pass.
|
||||
pub highlight_next_pass: IdSet,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_rect: Option<DebugRect>,
|
||||
}
|
||||
|
||||
impl Default for FrameState {
|
||||
impl Default for PassState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
used_ids: Default::default(),
|
||||
|
|
@ -236,7 +238,7 @@ impl Default for FrameState {
|
|||
scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state: None,
|
||||
highlight_next_frame: Default::default(),
|
||||
highlight_next_pass: Default::default(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug_rect: None,
|
||||
|
|
@ -244,8 +246,8 @@ impl Default for FrameState {
|
|||
}
|
||||
}
|
||||
|
||||
impl FrameState {
|
||||
pub(crate) fn begin_frame(&mut self, screen_rect: Rect) {
|
||||
impl PassState {
|
||||
pub(crate) fn begin_pass(&mut self, screen_rect: Rect) {
|
||||
crate::profile_function!();
|
||||
let Self {
|
||||
used_ids,
|
||||
|
|
@ -259,7 +261,7 @@ impl FrameState {
|
|||
scroll_delta,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
highlight_next_frame,
|
||||
highlight_next_pass,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug_rect,
|
||||
|
|
@ -285,7 +287,7 @@ impl FrameState {
|
|||
*accesskit_state = None;
|
||||
}
|
||||
|
||||
highlight_next_frame.clear();
|
||||
highlight_next_pass.clear();
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
||||
|
|
@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc};
|
|||
|
||||
use crate::{
|
||||
emath::{Align, Pos2, Rect, Vec2},
|
||||
frame_state, menu, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense,
|
||||
Ui, WidgetRect, WidgetText,
|
||||
menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui,
|
||||
WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -600,7 +600,7 @@ impl Response {
|
|||
return true;
|
||||
}
|
||||
|
||||
let any_open_popups = self.ctx.prev_frame_state(|fs| {
|
||||
let any_open_popups = self.ctx.prev_pass_state(|fs| {
|
||||
fs.layers
|
||||
.get(&self.layer_id)
|
||||
.map_or(false, |layer| !layer.open_popups.is_empty())
|
||||
|
|
@ -648,7 +648,7 @@ impl Response {
|
|||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||
|
||||
let tooltip_has_interactive_widget = self.ctx.viewport(|vp| {
|
||||
vp.prev_frame
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
|
|
@ -696,7 +696,7 @@ impl Response {
|
|||
}
|
||||
}
|
||||
|
||||
let is_other_tooltip_open = self.ctx.prev_frame_state(|fs| {
|
||||
let is_other_tooltip_open = self.ctx.prev_pass_state(|fs| {
|
||||
if let Some(already_open_tooltip) = fs
|
||||
.layers
|
||||
.get(&self.layer_id)
|
||||
|
|
@ -896,13 +896,13 @@ impl Response {
|
|||
align: Option<Align>,
|
||||
animation: crate::style::ScrollAnimation,
|
||||
) {
|
||||
self.ctx.frame_state_mut(|state| {
|
||||
state.scroll_target[0] = Some(frame_state::ScrollTarget::new(
|
||||
self.ctx.pass_state_mut(|state| {
|
||||
state.scroll_target[0] = Some(pass_state::ScrollTarget::new(
|
||||
self.rect.x_range(),
|
||||
align,
|
||||
animation,
|
||||
));
|
||||
state.scroll_target[1] = Some(frame_state::ScrollTarget::new(
|
||||
state.scroll_target[1] = Some(pass_state::ScrollTarget::new(
|
||||
self.rect.y_range(),
|
||||
align,
|
||||
animation,
|
||||
|
|
|
|||
|
|
@ -118,11 +118,8 @@ impl Default for LabelSelectionState {
|
|||
|
||||
impl LabelSelectionState {
|
||||
pub(crate) fn register(ctx: &Context) {
|
||||
ctx.on_begin_frame(
|
||||
"LabelSelectionState",
|
||||
std::sync::Arc::new(Self::begin_frame),
|
||||
);
|
||||
ctx.on_end_frame("LabelSelectionState", std::sync::Arc::new(Self::end_frame));
|
||||
ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass));
|
||||
ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass));
|
||||
}
|
||||
|
||||
pub fn load(ctx: &Context) -> Self {
|
||||
|
|
@ -138,7 +135,7 @@ impl LabelSelectionState {
|
|||
});
|
||||
}
|
||||
|
||||
fn begin_frame(ctx: &Context) {
|
||||
fn begin_pass(ctx: &Context) {
|
||||
let mut state = Self::load(ctx);
|
||||
|
||||
if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
|
||||
|
|
@ -159,7 +156,7 @@ impl LabelSelectionState {
|
|||
state.store(ctx);
|
||||
}
|
||||
|
||||
fn end_frame(ctx: &Context) {
|
||||
fn end_pass(ctx: &Context) {
|
||||
let mut state = Self::load(ctx);
|
||||
|
||||
if state.is_dragging {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ use crate::{
|
|||
ecolor::Hsva,
|
||||
emath, epaint,
|
||||
epaint::text::Fonts,
|
||||
frame_state, grid,
|
||||
grid,
|
||||
layout::{Direction, Layout},
|
||||
menu,
|
||||
menu::MenuState,
|
||||
pass_state,
|
||||
placer::Placer,
|
||||
pos2, style,
|
||||
util::IdTypeMap,
|
||||
|
|
@ -469,6 +470,9 @@ impl Ui {
|
|||
}
|
||||
|
||||
/// If `false`, any widgets added to the [`Ui`] will be invisible and non-interactive.
|
||||
///
|
||||
/// This is `false` if any parent had [`UiBuilder::invisible`]
|
||||
/// or if [`Context::will_discard`].
|
||||
#[inline]
|
||||
pub fn is_visible(&self) -> bool {
|
||||
self.painter.is_visible()
|
||||
|
|
@ -659,6 +663,9 @@ impl Ui {
|
|||
}
|
||||
|
||||
/// Can be used for culling: if `false`, then no part of `rect` will be visible on screen.
|
||||
///
|
||||
/// This is false if the whole `Ui` is invisible (see [`UiBuilder::invisible`])
|
||||
/// or if [`Context::will_discard`] is true.
|
||||
pub fn is_rect_visible(&self, rect: Rect) -> bool {
|
||||
self.is_visible() && rect.intersects(self.clip_rect())
|
||||
}
|
||||
|
|
@ -1336,9 +1343,9 @@ impl Ui {
|
|||
) {
|
||||
for d in 0..2 {
|
||||
let range = Rangef::new(rect.min[d], rect.max[d]);
|
||||
self.ctx().frame_state_mut(|state| {
|
||||
self.ctx().pass_state_mut(|state| {
|
||||
state.scroll_target[d] =
|
||||
Some(frame_state::ScrollTarget::new(range, align, animation));
|
||||
Some(pass_state::ScrollTarget::new(range, align, animation));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1378,9 +1385,9 @@ impl Ui {
|
|||
let target = self.next_widget_position();
|
||||
for d in 0..2 {
|
||||
let target = Rangef::point(target[d]);
|
||||
self.ctx().frame_state_mut(|state| {
|
||||
self.ctx().pass_state_mut(|state| {
|
||||
state.scroll_target[d] =
|
||||
Some(frame_state::ScrollTarget::new(target, align, animation));
|
||||
Some(pass_state::ScrollTarget::new(target, align, animation));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1420,7 +1427,7 @@ impl Ui {
|
|||
|
||||
/// Same as [`Self::scroll_with_delta`], but allows you to specify the [`style::ScrollAnimation`].
|
||||
pub fn scroll_with_delta_animation(&self, delta: Vec2, animation: style::ScrollAnimation) {
|
||||
self.ctx().frame_state_mut(|state| {
|
||||
self.ctx().pass_state_mut(|state| {
|
||||
state.scroll_delta.0 += delta;
|
||||
state.scroll_delta.1 = animation;
|
||||
});
|
||||
|
|
@ -1456,8 +1463,8 @@ impl Ui {
|
|||
/// See also [`Self::add`] and [`Self::put`].
|
||||
///
|
||||
/// ```
|
||||
/// # let mut my_value = 42;
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let mut my_value = 42;
|
||||
/// ui.add_sized([40.0, 20.0], egui::DragValue::new(&mut my_value));
|
||||
/// # });
|
||||
/// ```
|
||||
|
|
@ -2876,14 +2883,14 @@ fn register_rect(ui: &Ui, rect: Rect) {
|
|||
let callstack = String::default();
|
||||
|
||||
// We only show one debug rectangle, or things get confusing:
|
||||
let debug_rect = frame_state::DebugRect {
|
||||
let debug_rect = pass_state::DebugRect {
|
||||
rect,
|
||||
callstack,
|
||||
is_clicking,
|
||||
};
|
||||
|
||||
let mut kept = false;
|
||||
ui.ctx().frame_state_mut(|fs| {
|
||||
ui.ctx().pass_state_mut(|fs| {
|
||||
if let Some(final_debug_rect) = &mut fs.debug_rect {
|
||||
// or maybe pick the one with deepest callstack?
|
||||
if final_debug_rect.rect.contains_rect(rect) {
|
||||
|
|
|
|||
|
|
@ -1168,5 +1168,5 @@ pub struct ImmediateViewport<'a> {
|
|||
pub builder: ViewportBuilder,
|
||||
|
||||
/// The user-code that shows the GUI.
|
||||
pub viewport_ui_cb: Box<dyn FnOnce(&Context) + 'a>,
|
||||
pub viewport_ui_cb: Box<dyn FnMut(&Context) + 'a>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ fn multiple_disabled_widgets() {
|
|||
);
|
||||
}
|
||||
|
||||
fn accesskit_output_single_egui_frame(run_ui: impl FnOnce(&Context)) -> TreeUpdate {
|
||||
fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&Context)) -> TreeUpdate {
|
||||
let ctx = Context::default();
|
||||
ctx.enable_accesskit();
|
||||
|
||||
|
|
|
|||
|
|
@ -147,8 +147,8 @@ impl BackendPanel {
|
|||
if cfg!(debug_assertions) {
|
||||
ui.collapsing("More…", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Frame number:");
|
||||
ui.monospace(ui.ctx().frame_nr().to_string());
|
||||
ui.label("Total ui passes:");
|
||||
ui.monospace(ui.ctx().cumulative_pass_nr().to_string());
|
||||
});
|
||||
if ui
|
||||
.button("Wait 2s, then request repaint after another 3s")
|
||||
|
|
@ -161,6 +161,16 @@ impl BackendPanel {
|
|||
ctx.request_repaint_after(std::time::Duration::from_secs(3));
|
||||
});
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Request discard").clicked() {
|
||||
ui.ctx().request_discard();
|
||||
|
||||
if !ui.ctx().will_discard() {
|
||||
ui.label("Discard denied!");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@ fn main() -> eframe::Result {
|
|||
|
||||
{
|
||||
// Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206)
|
||||
let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
|
||||
let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| {
|
||||
if cfg!(debug_assertions) {
|
||||
"debug".to_owned()
|
||||
} else {
|
||||
"info".to_owned()
|
||||
}
|
||||
});
|
||||
for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] {
|
||||
if !rust_log.contains(&format!("{loud_crate}=")) {
|
||||
rust_log += &format!(",{loud_crate}=warn");
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
ctx.begin_frame(RawInput::default());
|
||||
ctx.begin_pass(RawInput::default());
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
c.bench_function("Painter::rect", |b| {
|
||||
|
|
@ -81,7 +81,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
});
|
||||
});
|
||||
|
||||
// Don't call `end_frame` to not have to drain the huge paint list
|
||||
// Don't call `end_pass` to not have to drain the huge paint list
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ impl FontDefinitions {
|
|||
///
|
||||
/// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`.
|
||||
///
|
||||
/// You need to call [`Self::begin_frame`] and [`Self::font_image_delta`] once every frame.
|
||||
/// You need to call [`Self::begin_pass`] and [`Self::font_image_delta`] once every frame.
|
||||
#[derive(Clone)]
|
||||
pub struct Fonts(Arc<Mutex<FontsAndCache>>);
|
||||
|
||||
|
|
@ -389,7 +389,7 @@ impl Fonts {
|
|||
///
|
||||
/// This function will react to changes in `pixels_per_point` and `max_texture_side`,
|
||||
/// as well as notice when the font atlas is getting full, and handle that.
|
||||
pub fn begin_frame(&self, pixels_per_point: f32, max_texture_side: usize) {
|
||||
pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) {
|
||||
let mut fonts_and_cache = self.0.lock();
|
||||
|
||||
let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
|
||||
|
|
@ -503,7 +503,7 @@ impl Fonts {
|
|||
/// How full is the font atlas?
|
||||
///
|
||||
/// This increases as new fonts and/or glyphs are used,
|
||||
/// but can also decrease in a call to [`Self::begin_frame`].
|
||||
/// but can also decrease in a call to [`Self::begin_pass`].
|
||||
pub fn font_atlas_fill_ratio(&self) -> f32 {
|
||||
self.lock().fonts.atlas.lock().fill_ratio()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ impl TextWrapping {
|
|||
/// Needs to be recreated if the underlying font atlas texture changes, which
|
||||
/// happens under the following conditions:
|
||||
/// - `pixels_per_point` or `max_texture_size` change. These parameters are set
|
||||
/// in [`crate::text::Fonts::begin_frame`]. When using `egui` they are set
|
||||
/// in [`crate::text::Fonts::begin_pass`]. When using `egui` they are set
|
||||
/// from `egui::InputState` and can change at any time.
|
||||
/// - The atlas has become full. This can happen any time a new glyph is added
|
||||
/// to the atlas, which in turn can happen any time new text is laid out.
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ fn generic_ui(ui: &mut egui::Ui, children: &[Arc<RwLock<ViewportState>>], close_
|
|||
let ctx = ui.ctx().clone();
|
||||
ui.label(format!(
|
||||
"Frame nr: {} (this increases when this viewport is being rendered)",
|
||||
ctx.frame_nr()
|
||||
ctx.cumulative_pass_nr()
|
||||
));
|
||||
ui.horizontal(|ui| {
|
||||
let mut show_spinner =
|
||||
|
|
|
|||
Loading…
Reference in New Issue