Merge branch 'master' into feat-window-actions
This commit is contained in:
commit
df19fa87d8
9 changed files with 258 additions and 57 deletions
|
@ -17,8 +17,9 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
|
||||||
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||||
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
|
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||||
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
|
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
|
||||||
* Add `ui.centered`.
|
* Added `ui.centered`.
|
||||||
* Added `Area::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
|
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
|
||||||
|
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
||||||
|
|
|
@ -147,7 +147,7 @@ pub fn handle_app_output(
|
||||||
fullscreen,
|
fullscreen,
|
||||||
drag_window,
|
drag_window,
|
||||||
window_pos,
|
window_pos,
|
||||||
visible,
|
visible: _, // handled in post_present
|
||||||
always_on_top,
|
always_on_top,
|
||||||
minimized,
|
minimized,
|
||||||
maximized,
|
maximized,
|
||||||
|
@ -186,10 +186,6 @@ pub fn handle_app_output(
|
||||||
let _ = window.drag_window();
|
let _ = window.drag_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(visible) = visible {
|
|
||||||
window.set_visible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(always_on_top) = always_on_top {
|
if let Some(always_on_top) = always_on_top {
|
||||||
window.set_always_on_top(always_on_top);
|
window.set_always_on_top(always_on_top);
|
||||||
}
|
}
|
||||||
|
@ -251,7 +247,10 @@ impl EpiIntegration {
|
||||||
native_pixels_per_point: Some(native_pixels_per_point),
|
native_pixels_per_point: Some(native_pixels_per_point),
|
||||||
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
|
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
|
||||||
},
|
},
|
||||||
output: Default::default(),
|
output: epi::backend::AppOutput {
|
||||||
|
visible: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
storage,
|
storage,
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
gl,
|
gl,
|
||||||
|
@ -336,6 +335,7 @@ impl EpiIntegration {
|
||||||
if app_output.close {
|
if app_output.close {
|
||||||
self.close = app.on_close_event();
|
self.close = app.on_close_event();
|
||||||
}
|
}
|
||||||
|
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
||||||
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
|
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,6 +352,12 @@ impl EpiIntegration {
|
||||||
app.post_rendering(window_size_px, &self.frame);
|
app.post_rendering(window_size_px, &self.frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn post_present(&mut self, window: &winit::window::Window) {
|
||||||
|
if let Some(visible) = self.frame.output.visible.take() {
|
||||||
|
window.set_visible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_platform_output(
|
pub fn handle_platform_output(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
|
|
|
@ -22,7 +22,17 @@ pub use epi::NativeOptions;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum EventResult {
|
enum EventResult {
|
||||||
Wait,
|
Wait,
|
||||||
RepaintAsap,
|
/// Causes a synchronous repaint inside the event handler. This should only
|
||||||
|
/// be used in special situations if the window must be repainted while
|
||||||
|
/// handling a specific event. This occurs on Windows when handling resizes.
|
||||||
|
///
|
||||||
|
/// `RepaintNow` creates a new frame synchronously, and should therefore
|
||||||
|
/// only be used for extremely urgent repaints.
|
||||||
|
RepaintNow,
|
||||||
|
/// Queues a repaint for once the event loop handles its next redraw. Exists
|
||||||
|
/// so that multiple input events can be handled in one frame. Does not
|
||||||
|
/// cause any delay like `RepaintNow`.
|
||||||
|
RepaintNext,
|
||||||
RepaintAt(Instant),
|
RepaintAt(Instant),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
@ -103,7 +113,7 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||||
..
|
..
|
||||||
}) => EventResult::RepaintAsap,
|
}) => EventResult::RepaintNext,
|
||||||
|
|
||||||
winit::event::Event::WindowEvent { window_id, .. }
|
winit::event::Event::WindowEvent { window_id, .. }
|
||||||
if winit_app.window().is_none()
|
if winit_app.window().is_none()
|
||||||
|
@ -119,7 +129,12 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||||
|
|
||||||
match event_result {
|
match event_result {
|
||||||
EventResult::Wait => {}
|
EventResult::Wait => {}
|
||||||
EventResult::RepaintAsap => {
|
EventResult::RepaintNow => {
|
||||||
|
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||||
|
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||||
|
winit_app.paint();
|
||||||
|
}
|
||||||
|
EventResult::RepaintNext => {
|
||||||
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||||
next_repaint_time = Instant::now();
|
next_repaint_time = Instant::now();
|
||||||
}
|
}
|
||||||
|
@ -188,14 +203,18 @@ fn run_and_exit(
|
||||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||||
..
|
..
|
||||||
}) => EventResult::RepaintAsap,
|
}) => EventResult::RepaintNext,
|
||||||
|
|
||||||
event => winit_app.on_event(event_loop, &event),
|
event => winit_app.on_event(event_loop, &event),
|
||||||
};
|
};
|
||||||
|
|
||||||
match event_result {
|
match event_result {
|
||||||
EventResult::Wait => {}
|
EventResult::Wait => {}
|
||||||
EventResult::RepaintAsap => {
|
EventResult::RepaintNow => {
|
||||||
|
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||||
|
winit_app.paint();
|
||||||
|
}
|
||||||
|
EventResult::RepaintNext => {
|
||||||
next_repaint_time = Instant::now();
|
next_repaint_time = Instant::now();
|
||||||
}
|
}
|
||||||
EventResult::RepaintAt(repaint_time) => {
|
EventResult::RepaintAt(repaint_time) => {
|
||||||
|
@ -330,8 +349,9 @@ mod glow_integration {
|
||||||
};
|
};
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
|
|
||||||
let window_builder =
|
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
||||||
epi_integration::window_builder(native_options, &window_settings).with_title(title);
|
.with_title(title)
|
||||||
|
.with_visible(false); // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
|
|
||||||
let gl_window = unsafe {
|
let gl_window = unsafe {
|
||||||
glutin::ContextBuilder::new()
|
glutin::ContextBuilder::new()
|
||||||
|
@ -493,10 +513,12 @@ mod glow_integration {
|
||||||
gl_window.swap_buffers().unwrap();
|
gl_window.swap_buffers().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integration.post_present(window);
|
||||||
|
|
||||||
let control_flow = if integration.should_close() {
|
let control_flow = if integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
} else if repaint_after.is_zero() {
|
} else if repaint_after.is_zero() {
|
||||||
EventResult::RepaintAsap
|
EventResult::RepaintNext
|
||||||
} else if let Some(repaint_after_instant) =
|
} else if let Some(repaint_after_instant) =
|
||||||
std::time::Instant::now().checked_add(repaint_after)
|
std::time::Instant::now().checked_add(repaint_after)
|
||||||
{
|
{
|
||||||
|
@ -538,7 +560,7 @@ mod glow_integration {
|
||||||
if self.running.is_none() {
|
if self.running.is_none() {
|
||||||
self.init_run_state(event_loop);
|
self.init_run_state(event_loop);
|
||||||
}
|
}
|
||||||
EventResult::RepaintAsap
|
EventResult::RepaintNow
|
||||||
}
|
}
|
||||||
winit::event::Event::Suspended => {
|
winit::event::Event::Suspended => {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
@ -560,11 +582,28 @@ mod glow_integration {
|
||||||
|
|
||||||
winit::event::Event::WindowEvent { event, .. } => {
|
winit::event::Event::WindowEvent { event, .. } => {
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
|
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||||
|
// event handler.
|
||||||
|
//
|
||||||
|
// If this is not done, the compositor will assume that the window does not want to redraw,
|
||||||
|
// and continue ahead.
|
||||||
|
//
|
||||||
|
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||||
|
// new frames to the compositor in time.
|
||||||
|
//
|
||||||
|
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||||
|
// to resizes anyway, as doing so avoids dropping frames.
|
||||||
|
//
|
||||||
|
// See: https://github.com/emilk/egui/issues/903
|
||||||
|
let mut repaint_asap = false;
|
||||||
|
|
||||||
match &event {
|
match &event {
|
||||||
winit::event::WindowEvent::Focused(new_focused) => {
|
winit::event::WindowEvent::Focused(new_focused) => {
|
||||||
self.is_focused = *new_focused;
|
self.is_focused = *new_focused;
|
||||||
}
|
}
|
||||||
winit::event::WindowEvent::Resized(physical_size) => {
|
winit::event::WindowEvent::Resized(physical_size) => {
|
||||||
|
repaint_asap = true;
|
||||||
|
|
||||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||||
// See: https://github.com/rust-windowing/winit/issues/208
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
// This solves an issue where the app would panic when minimizing on Windows.
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
|
@ -576,6 +615,7 @@ mod glow_integration {
|
||||||
new_inner_size,
|
new_inner_size,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
repaint_asap = true;
|
||||||
running.gl_window.resize(**new_inner_size);
|
running.gl_window.resize(**new_inner_size);
|
||||||
}
|
}
|
||||||
winit::event::WindowEvent::CloseRequested
|
winit::event::WindowEvent::CloseRequested
|
||||||
|
@ -592,7 +632,11 @@ mod glow_integration {
|
||||||
if running.integration.should_close() {
|
if running.integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
} else if event_response.repaint {
|
} else if event_response.repaint {
|
||||||
EventResult::RepaintAsap
|
if repaint_asap {
|
||||||
|
EventResult::RepaintNow
|
||||||
|
} else {
|
||||||
|
EventResult::RepaintNext
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
|
@ -692,6 +736,7 @@ mod wgpu_integration {
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
epi_integration::window_builder(native_options, &window_settings)
|
epi_integration::window_builder(native_options, &window_settings)
|
||||||
.with_title(title)
|
.with_title(title)
|
||||||
|
.with_visible(false) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
.build(event_loop)
|
.build(event_loop)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -850,11 +895,12 @@ mod wgpu_integration {
|
||||||
);
|
);
|
||||||
|
|
||||||
integration.post_rendering(app.as_mut(), window);
|
integration.post_rendering(app.as_mut(), window);
|
||||||
|
integration.post_present(window);
|
||||||
|
|
||||||
let control_flow = if integration.should_close() {
|
let control_flow = if integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
} else if repaint_after.is_zero() {
|
} else if repaint_after.is_zero() {
|
||||||
EventResult::RepaintAsap
|
EventResult::RepaintNext
|
||||||
} else if let Some(repaint_after_instant) =
|
} else if let Some(repaint_after_instant) =
|
||||||
std::time::Instant::now().checked_add(repaint_after)
|
std::time::Instant::now().checked_add(repaint_after)
|
||||||
{
|
{
|
||||||
|
@ -913,7 +959,7 @@ mod wgpu_integration {
|
||||||
);
|
);
|
||||||
self.init_run_state(event_loop, storage, window);
|
self.init_run_state(event_loop, storage, window);
|
||||||
}
|
}
|
||||||
EventResult::RepaintAsap
|
EventResult::RepaintNow
|
||||||
}
|
}
|
||||||
winit::event::Event::Suspended => {
|
winit::event::Event::Suspended => {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
@ -923,11 +969,28 @@ mod wgpu_integration {
|
||||||
|
|
||||||
winit::event::Event::WindowEvent { event, .. } => {
|
winit::event::Event::WindowEvent { event, .. } => {
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
|
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||||
|
// event handler.
|
||||||
|
//
|
||||||
|
// If this is not done, the compositor will assume that the window does not want to redraw,
|
||||||
|
// and continue ahead.
|
||||||
|
//
|
||||||
|
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||||
|
// new frames to the compositor in time.
|
||||||
|
//
|
||||||
|
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||||
|
// to resizes anyway, as doing so avoids dropping frames.
|
||||||
|
//
|
||||||
|
// See: https://github.com/emilk/egui/issues/903
|
||||||
|
let mut repaint_asap = false;
|
||||||
|
|
||||||
match &event {
|
match &event {
|
||||||
winit::event::WindowEvent::Focused(new_focused) => {
|
winit::event::WindowEvent::Focused(new_focused) => {
|
||||||
self.is_focused = *new_focused;
|
self.is_focused = *new_focused;
|
||||||
}
|
}
|
||||||
winit::event::WindowEvent::Resized(physical_size) => {
|
winit::event::WindowEvent::Resized(physical_size) => {
|
||||||
|
repaint_asap = true;
|
||||||
|
|
||||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||||
// See: https://github.com/rust-windowing/winit/issues/208
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
// This solves an issue where the app would panic when minimizing on Windows.
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
|
@ -942,6 +1005,7 @@ mod wgpu_integration {
|
||||||
new_inner_size,
|
new_inner_size,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
repaint_asap = true;
|
||||||
running
|
running
|
||||||
.painter
|
.painter
|
||||||
.on_window_resized(new_inner_size.width, new_inner_size.height);
|
.on_window_resized(new_inner_size.width, new_inner_size.height);
|
||||||
|
@ -959,7 +1023,11 @@ mod wgpu_integration {
|
||||||
if running.integration.should_close() {
|
if running.integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
} else if event_response.repaint {
|
} else if event_response.repaint {
|
||||||
EventResult::RepaintAsap
|
if repaint_asap {
|
||||||
|
EventResult::RepaintNow
|
||||||
|
} else {
|
||||||
|
EventResult::RepaintNext
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ pub struct Area {
|
||||||
constrain: bool,
|
constrain: bool,
|
||||||
order: Order,
|
order: Order,
|
||||||
default_pos: Option<Pos2>,
|
default_pos: Option<Pos2>,
|
||||||
|
pivot: Align2,
|
||||||
anchor: Option<(Align2, Vec2)>,
|
anchor: Option<(Align2, Vec2)>,
|
||||||
new_pos: Option<Pos2>,
|
new_pos: Option<Pos2>,
|
||||||
drag_bounds: Option<Rect>,
|
drag_bounds: Option<Rect>,
|
||||||
|
@ -65,6 +66,7 @@ impl Area {
|
||||||
order: Order::Middle,
|
order: Order::Middle,
|
||||||
default_pos: None,
|
default_pos: None,
|
||||||
new_pos: None,
|
new_pos: None,
|
||||||
|
pivot: Align2::LEFT_TOP,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
drag_bounds: None,
|
drag_bounds: None,
|
||||||
}
|
}
|
||||||
|
@ -122,16 +124,28 @@ impl Area {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Positions the window and prevents it from being moved
|
||||||
|
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
|
||||||
|
self.new_pos = Some(fixed_pos.into());
|
||||||
|
self.movable = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Constrains this area to the screen bounds.
|
/// Constrains this area to the screen bounds.
|
||||||
pub fn constrain(mut self, constrain: bool) -> Self {
|
pub fn constrain(mut self, constrain: bool) -> Self {
|
||||||
self.constrain = constrain;
|
self.constrain = constrain;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Positions the window and prevents it from being moved
|
/// Where the "root" of the area is.
|
||||||
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
|
///
|
||||||
self.new_pos = Some(fixed_pos.into());
|
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
|
||||||
self.movable = false;
|
/// then [`Self::fixed_pos`] will set the position of the right-top
|
||||||
|
/// corner of the area.
|
||||||
|
///
|
||||||
|
/// Default: [`Align2::LEFT_TOP`].
|
||||||
|
pub fn pivot(mut self, pivot: Align2) -> Self {
|
||||||
|
self.pivot = pivot;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +222,7 @@ impl Area {
|
||||||
enabled,
|
enabled,
|
||||||
default_pos,
|
default_pos,
|
||||||
new_pos,
|
new_pos,
|
||||||
|
pivot,
|
||||||
anchor,
|
anchor,
|
||||||
drag_bounds,
|
drag_bounds,
|
||||||
constrain,
|
constrain,
|
||||||
|
@ -229,9 +244,18 @@ impl Area {
|
||||||
state.interactable = interactable;
|
state.interactable = interactable;
|
||||||
let mut temporarily_invisible = false;
|
let mut temporarily_invisible = false;
|
||||||
|
|
||||||
|
if pivot != Align2::LEFT_TOP {
|
||||||
|
if is_new {
|
||||||
|
temporarily_invisible = true; // figure out the size first
|
||||||
|
} else {
|
||||||
|
state.pos.x -= pivot.x().to_factor() * state.size.x;
|
||||||
|
state.pos.y -= pivot.y().to_factor() * state.size.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((anchor, offset)) = anchor {
|
if let Some((anchor, offset)) = anchor {
|
||||||
if is_new {
|
if is_new {
|
||||||
temporarily_invisible = true;
|
temporarily_invisible = true; // figure out the size first
|
||||||
} else {
|
} else {
|
||||||
let screen = ctx.available_rect();
|
let screen = ctx.available_rect();
|
||||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
use crate::{style::WidgetVisuals, *};
|
|
||||||
use epaint::Shape;
|
use epaint::Shape;
|
||||||
|
|
||||||
|
use crate::{style::WidgetVisuals, *};
|
||||||
|
|
||||||
|
/// Indicate wether or not a popup will be shown above or below the box.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum AboveOrBelow {
|
||||||
|
Above,
|
||||||
|
Below,
|
||||||
|
}
|
||||||
|
|
||||||
/// A function that paints the [`ComboBox`] icon
|
/// A function that paints the [`ComboBox`] icon
|
||||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
||||||
|
|
||||||
/// A drop-down selection menu with a descriptive label.
|
/// A drop-down selection menu with a descriptive label.
|
||||||
///
|
///
|
||||||
|
@ -89,6 +97,7 @@ impl ComboBox {
|
||||||
/// rect: egui::Rect,
|
/// rect: egui::Rect,
|
||||||
/// visuals: &egui::style::WidgetVisuals,
|
/// visuals: &egui::style::WidgetVisuals,
|
||||||
/// _is_open: bool,
|
/// _is_open: bool,
|
||||||
|
/// _above_or_below: egui::AboveOrBelow,
|
||||||
/// ) {
|
/// ) {
|
||||||
/// let rect = egui::Rect::from_center_size(
|
/// let rect = egui::Rect::from_center_size(
|
||||||
/// rect.center(),
|
/// rect.center(),
|
||||||
|
@ -107,7 +116,10 @@ impl ComboBox {
|
||||||
/// .show_ui(ui, |_ui| {});
|
/// .show_ui(ui, |_ui| {});
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
pub fn icon(
|
||||||
|
mut self,
|
||||||
|
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
||||||
|
) -> Self {
|
||||||
self.icon = Some(Box::new(icon_fn));
|
self.icon = Some(Box::new(icon_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -213,6 +225,23 @@ fn combo_box_dyn<'c, R>(
|
||||||
let popup_id = button_id.with("popup");
|
let popup_id = button_id.with("popup");
|
||||||
|
|
||||||
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
||||||
|
|
||||||
|
let popup_height = ui
|
||||||
|
.ctx()
|
||||||
|
.memory()
|
||||||
|
.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()
|
||||||
|
{
|
||||||
|
AboveOrBelow::Below
|
||||||
|
} else {
|
||||||
|
AboveOrBelow::Above
|
||||||
|
};
|
||||||
|
|
||||||
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
|
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
|
||||||
// We don't want to change width when user selects something new
|
// We don't want to change width when user selects something new
|
||||||
let full_minimum_width = ui.spacing().slider_width;
|
let full_minimum_width = ui.spacing().slider_width;
|
||||||
|
@ -243,9 +272,15 @@ fn combo_box_dyn<'c, R>(
|
||||||
icon_rect.expand(visuals.expansion),
|
icon_rect.expand(visuals.expansion),
|
||||||
visuals,
|
visuals,
|
||||||
is_popup_open,
|
is_popup_open,
|
||||||
|
above_or_below,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
paint_default_icon(
|
||||||
|
ui.painter(),
|
||||||
|
icon_rect.expand(visuals.expansion),
|
||||||
|
visuals,
|
||||||
|
above_or_below,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||||
|
@ -256,12 +291,18 @@ fn combo_box_dyn<'c, R>(
|
||||||
if button_response.clicked() {
|
if button_response.clicked() {
|
||||||
ui.memory().toggle_popup(popup_id);
|
ui.memory().toggle_popup(popup_id);
|
||||||
}
|
}
|
||||||
let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| {
|
let inner = crate::popup::popup_above_or_below_widget(
|
||||||
ScrollArea::vertical()
|
ui,
|
||||||
.max_height(ui.spacing().combo_height)
|
popup_id,
|
||||||
.show(ui, menu_contents)
|
&button_response,
|
||||||
.inner
|
above_or_below,
|
||||||
});
|
|ui| {
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.max_height(ui.spacing().combo_height)
|
||||||
|
.show(ui, menu_contents)
|
||||||
|
.inner
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
InnerResponse {
|
InnerResponse {
|
||||||
inner,
|
inner,
|
||||||
|
@ -316,13 +357,31 @@ fn button_frame(
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
fn paint_default_icon(
|
||||||
|
painter: &Painter,
|
||||||
|
rect: Rect,
|
||||||
|
visuals: &WidgetVisuals,
|
||||||
|
above_or_below: AboveOrBelow,
|
||||||
|
) {
|
||||||
let rect = Rect::from_center_size(
|
let rect = Rect::from_center_size(
|
||||||
rect.center(),
|
rect.center(),
|
||||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||||
);
|
);
|
||||||
painter.add(Shape::closed_line(
|
|
||||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
match above_or_below {
|
||||||
visuals.fg_stroke,
|
AboveOrBelow::Above => {
|
||||||
));
|
// Upward pointing triangle
|
||||||
|
painter.add(Shape::closed_line(
|
||||||
|
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
AboveOrBelow::Below => {
|
||||||
|
// Downward pointing triangle
|
||||||
|
painter.add(Shape::closed_line(
|
||||||
|
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,7 +294,23 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a popup below another widget.
|
/// Helper for [`popup_above_or_below_widget`].
|
||||||
|
pub fn popup_below_widget<R>(
|
||||||
|
ui: &Ui,
|
||||||
|
popup_id: Id,
|
||||||
|
widget_response: &Response,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
popup_above_or_below_widget(
|
||||||
|
ui,
|
||||||
|
popup_id,
|
||||||
|
widget_response,
|
||||||
|
AboveOrBelow::Below,
|
||||||
|
add_contents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a popup above or below another widget.
|
||||||
///
|
///
|
||||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
||||||
///
|
///
|
||||||
|
@ -309,24 +325,32 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||||
/// if response.clicked() {
|
/// if response.clicked() {
|
||||||
/// ui.memory().toggle_popup(popup_id);
|
/// ui.memory().toggle_popup(popup_id);
|
||||||
/// }
|
/// }
|
||||||
/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| {
|
/// let below = egui::AboveOrBelow::Below;
|
||||||
|
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
|
||||||
/// ui.set_min_width(200.0); // if you want to control the size
|
/// ui.set_min_width(200.0); // if you want to control the size
|
||||||
/// ui.label("Some more info, or things you can select:");
|
/// ui.label("Some more info, or things you can select:");
|
||||||
/// ui.label("…");
|
/// ui.label("…");
|
||||||
/// });
|
/// });
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn popup_below_widget<R>(
|
pub fn popup_above_or_below_widget<R>(
|
||||||
ui: &Ui,
|
ui: &Ui,
|
||||||
popup_id: Id,
|
popup_id: Id,
|
||||||
widget_response: &Response,
|
widget_response: &Response,
|
||||||
|
above_or_below: AboveOrBelow,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
if ui.memory().is_popup_open(popup_id) {
|
if ui.memory().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),
|
||||||
|
};
|
||||||
|
|
||||||
let inner = Area::new(popup_id)
|
let inner = Area::new(popup_id)
|
||||||
.order(Order::Foreground)
|
.order(Order::Foreground)
|
||||||
.constrain(true)
|
.constrain(true)
|
||||||
.fixed_pos(widget_response.rect.left_bottom())
|
.fixed_pos(pos)
|
||||||
|
.pivot(pivot)
|
||||||
.show(ui.ctx(), |ui| {
|
.show(ui.ctx(), |ui| {
|
||||||
// Note: we use a separate clip-rect for this area, so the popup can be outside the parent.
|
// Note: we use a separate clip-rect for this area, so the popup can be outside the parent.
|
||||||
// See https://github.com/emilk/egui/issues/825
|
// See https://github.com/emilk/egui/issues/825
|
||||||
|
|
|
@ -122,6 +122,30 @@ impl<'open> Window<'open> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the window position and prevents it from being dragged around.
|
||||||
|
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
|
||||||
|
self.area = self.area.fixed_pos(pos);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constrains this window to the screen bounds.
|
||||||
|
pub fn constrain(mut self, constrain: bool) -> Self {
|
||||||
|
self.area = self.area.constrain(constrain);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the "root" of the window is.
|
||||||
|
///
|
||||||
|
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
|
||||||
|
/// then [`Self::fixed_pos`] will set the position of the right-top
|
||||||
|
/// corner of the window.
|
||||||
|
///
|
||||||
|
/// Default: [`Align2::LEFT_TOP`].
|
||||||
|
pub fn pivot(mut self, pivot: Align2) -> Self {
|
||||||
|
self.area = self.area.pivot(pivot);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set anchor and distance.
|
/// Set anchor and distance.
|
||||||
///
|
///
|
||||||
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
|
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
|
||||||
|
@ -156,23 +180,17 @@ impl<'open> Window<'open> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set initial position and size of the window.
|
|
||||||
pub fn default_rect(self, rect: Rect) -> Self {
|
|
||||||
self.default_pos(rect.min).default_size(rect.size())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the window position and prevents it from being dragged around.
|
|
||||||
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
|
|
||||||
self.area = self.area.fixed_pos(pos);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the window size and prevents it from being resized by dragging its edges.
|
/// Sets the window size and prevents it from being resized by dragging its edges.
|
||||||
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
|
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
|
||||||
self.resize = self.resize.fixed_size(size);
|
self.resize = self.resize.fixed_size(size);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set initial position and size of the window.
|
||||||
|
pub fn default_rect(self, rect: Rect) -> Self {
|
||||||
|
self.default_pos(rect.min).default_size(rect.size())
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
|
/// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
|
||||||
pub fn fixed_rect(self, rect: Rect) -> Self {
|
pub fn fixed_rect(self, rect: Rect) -> Self {
|
||||||
self.fixed_pos(rect.min).fixed_size(rect.size())
|
self.fixed_pos(rect.min).fixed_size(rect.size())
|
||||||
|
|
|
@ -328,7 +328,7 @@ impl Context {
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let gap = 0.5; // Just to make sure we don't accidentally hover two things at once (a small eps should be sufficient).
|
let gap = 0.1; // Just to make sure we don't accidentally hover two things at once (a small eps should be sufficient).
|
||||||
|
|
||||||
// Make it easier to click things:
|
// Make it easier to click things:
|
||||||
let interact_rect = rect.expand2(
|
let interact_rect = rect.expand2(
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub struct Sense {
|
||||||
|
|
||||||
impl Sense {
|
impl Sense {
|
||||||
/// Senses no clicks or drags. Only senses mouse hover.
|
/// Senses no clicks or drags. Only senses mouse hover.
|
||||||
|
#[doc(alias = "none")]
|
||||||
pub fn hover() -> Self {
|
pub fn hover() -> Self {
|
||||||
Self {
|
Self {
|
||||||
click: false,
|
click: false,
|
||||||
|
|
Loading…
Reference in a new issue