mirror of
https://github.com/emilk/egui.git
synced 2026-06-28 07:23:13 -04:00
Merge branch 'master' of https://github.com/emilk/egui
This commit is contained in:
@@ -7,6 +7,13 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### ⚠️ BREAKING
|
||||
* `Response::clicked*` and `Response::dragged*` may lock the `Context`, so don't call it from a `Context`-locking closure.
|
||||
* `Response::clicked_by` will no longer be true if clicked with keyboard. Use `Response::clicked` instead.
|
||||
|
||||
|
||||
## 0.26.2 - 2024-02-14
|
||||
* Avoid interacting twice when not required [#4041](https://github.com/emilk/egui/pull/4041) (thanks [@abey79](https://github.com/abey79)!)
|
||||
|
||||
|
||||
@@ -67,6 +67,10 @@ pub struct CreationContext<'s> {
|
||||
#[cfg(feature = "glow")]
|
||||
pub gl: Option<std::sync::Arc<glow::Context>>,
|
||||
|
||||
/// The `get_proc_address` wrapper of underlying GL context
|
||||
#[cfg(feature = "glow")]
|
||||
pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>,
|
||||
|
||||
/// The underlying WGPU render state.
|
||||
///
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
@@ -714,7 +718,7 @@ pub struct WebInfo {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Location {
|
||||
/// The full URL (`location.href`) without the hash.
|
||||
/// The full URL (`location.href`) without the hash, percent-decoded.
|
||||
///
|
||||
/// Example: `"http://www.example.com:80/index.html?foo=bar"`.
|
||||
pub url: String,
|
||||
@@ -754,8 +758,8 @@ pub struct Location {
|
||||
|
||||
/// The parsed "query" part of "www.example.com/index.html?query#fragment".
|
||||
///
|
||||
/// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}`
|
||||
pub query_map: std::collections::BTreeMap<String, String>,
|
||||
/// "foo=hello&bar%20&foo=world" is parsed as `{"bar ": [""], "foo": ["hello", "world"]}`
|
||||
pub query_map: std::collections::BTreeMap<String, Vec<String>>,
|
||||
|
||||
/// `location.origin`
|
||||
///
|
||||
|
||||
@@ -284,12 +284,14 @@ impl GlowWinitApp {
|
||||
// Use latest raw_window_handle for eframe compatibility
|
||||
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
|
||||
|
||||
let get_proc_address = |addr: &_| glutin.get_proc_address(addr);
|
||||
let window = glutin.window(ViewportId::ROOT);
|
||||
let cc = CreationContext {
|
||||
egui_ctx: integration.egui_ctx.clone(),
|
||||
integration_info: integration.frame.info().clone(),
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl),
|
||||
get_proc_address: Some(&get_proc_address),
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state: None,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
|
||||
@@ -262,6 +262,8 @@ impl WgpuWinitApp {
|
||||
storage: integration.frame.storage(),
|
||||
#[cfg(feature = "glow")]
|
||||
gl: None,
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
wgpu_render_state,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
|
||||
@@ -76,6 +76,9 @@ impl AppRunner {
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
@@ -162,8 +165,8 @@ impl AppRunner {
|
||||
self.last_save_time = now_sec();
|
||||
}
|
||||
|
||||
pub fn canvas_id(&self) -> &str {
|
||||
self.painter.canvas_id()
|
||||
pub fn canvas(&self) -> &web_sys::HtmlCanvasElement {
|
||||
self.painter.canvas()
|
||||
}
|
||||
|
||||
pub fn destroy(mut self) {
|
||||
@@ -179,8 +182,8 @@ impl AppRunner {
|
||||
///
|
||||
/// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`].
|
||||
pub fn logic(&mut self) {
|
||||
super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points);
|
||||
let canvas_size = super::canvas_size_in_points(self.canvas_id());
|
||||
super::resize_canvas_to_screen_size(self.canvas(), self.web_options.max_size_points);
|
||||
let canvas_size = super::canvas_size_in_points(self.canvas());
|
||||
let raw_input = self.input.new_frame(canvas_size);
|
||||
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
@@ -265,7 +268,7 @@ impl AppRunner {
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
|
||||
if self.ime != ime {
|
||||
super::text_agent::move_text_cursor(ime, self.canvas_id());
|
||||
super::text_agent::move_text_cursor(ime, self.canvas());
|
||||
self.ime = ime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,44 +99,45 @@ pub fn web_location() -> epi::Location {
|
||||
.search()
|
||||
.unwrap_or_default()
|
||||
.strip_prefix('?')
|
||||
.map(percent_decode)
|
||||
.unwrap_or_default();
|
||||
|
||||
let query_map = parse_query_map(&query)
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
|
||||
.collect();
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
epi::Location {
|
||||
// TODO(emilk): should we really percent-decode the url? 🤷♂️
|
||||
url: percent_decode(&location.href().unwrap_or_default()),
|
||||
protocol: percent_decode(&location.protocol().unwrap_or_default()),
|
||||
host: percent_decode(&location.host().unwrap_or_default()),
|
||||
hostname: percent_decode(&location.hostname().unwrap_or_default()),
|
||||
port: percent_decode(&location.port().unwrap_or_default()),
|
||||
hash,
|
||||
query_map: parse_query_map(&query),
|
||||
query,
|
||||
query_map,
|
||||
origin: percent_decode(&location.origin().unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
|
||||
query
|
||||
.split('&')
|
||||
.filter_map(|pair| {
|
||||
if pair.is_empty() {
|
||||
None
|
||||
/// query is percent-encoded
|
||||
fn parse_query_map(query: &str) -> BTreeMap<String, Vec<String>> {
|
||||
let mut map: BTreeMap<String, Vec<String>> = Default::default();
|
||||
|
||||
for pair in query.split('&') {
|
||||
if !pair.is_empty() {
|
||||
if let Some((key, value)) = pair.split_once('=') {
|
||||
map.entry(percent_decode(key))
|
||||
.or_default()
|
||||
.push(percent_decode(value));
|
||||
} else {
|
||||
Some(if let Some((key, value)) = pair.split_once('=') {
|
||||
(key, value)
|
||||
} else {
|
||||
(pair, "")
|
||||
})
|
||||
map.entry(percent_decode(pair))
|
||||
.or_default()
|
||||
.push(String::new());
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
// TODO(emilk): this test is never acgtually run, because this whole module is wasm32 only 🤦♂️
|
||||
#[test]
|
||||
fn test_parse_query() {
|
||||
assert_eq!(parse_query_map(""), BTreeMap::default());
|
||||
@@ -157,4 +158,11 @@ fn test_parse_query() {
|
||||
parse_query_map("foo&baz&&"),
|
||||
BTreeMap::from_iter([("foo", ""), ("baz", "")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("badger=data.rrd%3Fparam1%3Dfoo%26param2%3Dbar&mushroom=snake"),
|
||||
BTreeMap::from_iter([
|
||||
("badger", "data.rrd?param1=foo¶m2=bar"),
|
||||
("mushroom", "snake")
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ use super::*;
|
||||
/// Calls `request_animation_frame` to schedule repaint.
|
||||
///
|
||||
/// It will only paint if needed, but will always call `request_animation_frame` immediately.
|
||||
fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> {
|
||||
pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> {
|
||||
// Only paint and schedule if there has been no panic
|
||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||
paint_if_needed(&mut runner_lock);
|
||||
drop(runner_lock);
|
||||
request_animation_frame(runner_ref.clone())?;
|
||||
runner_ref.request_animation_frame()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -45,14 +45,6 @@ fn paint_if_needed(runner: &mut AppRunner) {
|
||||
runner.auto_save_if_needed();
|
||||
}
|
||||
|
||||
pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once(move || paint_and_schedule(&runner_ref));
|
||||
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
closure.forget(); // We must forget it, or else the callback is canceled on drop
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> {
|
||||
@@ -275,7 +267,7 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul
|
||||
}
|
||||
|
||||
pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> {
|
||||
let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap();
|
||||
let canvas = runner_ref.try_lock().unwrap().canvas().clone();
|
||||
|
||||
{
|
||||
let prevent_default_events = [
|
||||
@@ -304,7 +296,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event);
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
runner.input.raw.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
@@ -331,7 +323,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
|event: web_sys::MouseEvent, runner| {
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event);
|
||||
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
|
||||
runner.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
@@ -343,7 +335,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event);
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
runner.input.raw.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
@@ -381,7 +373,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
"touchstart",
|
||||
|event: web_sys::TouchEvent, runner| {
|
||||
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
|
||||
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id);
|
||||
runner.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner.input.latest_touch_pos = Some(pos);
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
@@ -404,7 +396,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
"touchmove",
|
||||
|event: web_sys::TouchEvent, runner| {
|
||||
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
|
||||
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id);
|
||||
runner.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner.input.latest_touch_pos = Some(pos);
|
||||
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
|
||||
@@ -467,7 +459,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|
||||
});
|
||||
|
||||
let scroll_multiplier = match unit {
|
||||
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y,
|
||||
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas()).y,
|
||||
egui::MouseWheelUnit::Line => {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::{canvas_element, canvas_origin, AppRunner};
|
||||
use super::{canvas_origin, AppRunner};
|
||||
|
||||
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
pub fn pos_from_mouse_event(
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
event: &web_sys::MouseEvent,
|
||||
) -> egui::Pos2 {
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
egui::Pos2 {
|
||||
x: event.client_x() as f32 - rect.left() as f32,
|
||||
@@ -27,7 +29,7 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::Poin
|
||||
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
|
||||
/// pointer position.
|
||||
pub fn pos_from_touch_event(
|
||||
canvas_id: &str,
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
event: &web_sys::TouchEvent,
|
||||
touch_id_for_pos: &mut Option<egui::TouchId>,
|
||||
) -> egui::Pos2 {
|
||||
@@ -47,7 +49,7 @@ pub fn pos_from_touch_event(
|
||||
.or_else(|| event.touches().get(0))
|
||||
.map_or(Default::default(), |touch| {
|
||||
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
|
||||
pos_from_touch(canvas_origin(canvas_id), &touch)
|
||||
pos_from_touch(canvas_origin(canvas), &touch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Po
|
||||
}
|
||||
|
||||
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
|
||||
let canvas_origin = canvas_origin(runner.canvas_id());
|
||||
let canvas_origin = canvas_origin(runner.canvas());
|
||||
for touch_idx in 0..event.changed_touches().length() {
|
||||
if let Some(touch) = event.changed_touches().item(touch_idx) {
|
||||
runner.input.raw.events.push(egui::Event::Touch {
|
||||
|
||||
@@ -100,26 +100,23 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme {
|
||||
}
|
||||
}
|
||||
|
||||
fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||
fn get_canvas_element_by_id(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||
let document = web_sys::window()?.document()?;
|
||||
let canvas = document.get_element_by_id(canvas_id)?;
|
||||
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
||||
}
|
||||
|
||||
fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||
canvas_element(canvas_id)
|
||||
fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||
get_canvas_element_by_id(canvas_id)
|
||||
.unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}"))
|
||||
}
|
||||
|
||||
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
||||
let rect = canvas_element(canvas_id)
|
||||
.unwrap()
|
||||
.get_bounding_client_rect();
|
||||
fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 {
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
egui::pos2(rect.left() as f32, rect.top() as f32)
|
||||
}
|
||||
|
||||
fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement) -> egui::Vec2 {
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
egui::vec2(
|
||||
canvas.width() as f32 / pixels_per_point,
|
||||
@@ -127,8 +124,10 @@ fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||
)
|
||||
}
|
||||
|
||||
fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
fn resize_canvas_to_screen_size(
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
max_size_points: egui::Vec2,
|
||||
) -> Option<()> {
|
||||
let parent = canvas.parent_element()?;
|
||||
|
||||
// Prefer the client width and height so that if the parent
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::{canvas_element, AppRunner, WebRunner};
|
||||
use super::{AppRunner, WebRunner};
|
||||
|
||||
static AGENT_ID: &str = "egui_text_agent";
|
||||
|
||||
@@ -121,7 +121,7 @@ pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> {
|
||||
let window = web_sys::window()?;
|
||||
let document = window.document()?;
|
||||
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
|
||||
let canvas_style = canvas_element(runner.canvas_id())?.style();
|
||||
let canvas_style = runner.canvas().style();
|
||||
|
||||
if runner.mutable_text_under_cursor {
|
||||
let is_already_editing = input.hidden();
|
||||
@@ -205,14 +205,16 @@ fn is_mobile() -> Option<bool> {
|
||||
// candidate window moves following text element (agent),
|
||||
// so it appears that the IME candidate window moves with text cursor.
|
||||
// On mobile devices, there is no need to do that.
|
||||
pub fn move_text_cursor(ime: Option<egui::output::IMEOutput>, canvas_id: &str) -> Option<()> {
|
||||
pub fn move_text_cursor(
|
||||
ime: Option<egui::output::IMEOutput>,
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
) -> Option<()> {
|
||||
let style = text_agent().style();
|
||||
// Note: moving agent on mobile devices will lead to unpredictable scroll.
|
||||
if is_mobile() == Some(false) {
|
||||
ime.as_ref().and_then(|ime| {
|
||||
let egui::Pos2 { x, y } = ime.cursor_rect.left_top();
|
||||
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let bounding_rect = text_agent().get_bounding_client_rect();
|
||||
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
|
||||
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
|
||||
|
||||
@@ -9,8 +9,8 @@ pub(crate) trait WebPainter {
|
||||
// where
|
||||
// Self: Sized;
|
||||
|
||||
/// Id of the canvas in use.
|
||||
fn canvas_id(&self) -> &str;
|
||||
/// Reference to the canvas in use.
|
||||
fn canvas(&self) -> &web_sys::HtmlCanvasElement;
|
||||
|
||||
/// Maximum size of a texture in one direction.
|
||||
fn max_texture_side(&self) -> usize;
|
||||
|
||||
@@ -10,7 +10,6 @@ use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterGlow {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
painter: egui_glow::Painter,
|
||||
}
|
||||
|
||||
@@ -20,7 +19,7 @@ impl WebPainterGlow {
|
||||
}
|
||||
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
let canvas = super::get_canvas_element_by_id_or_die(canvas_id);
|
||||
|
||||
let (gl, shader_prefix) =
|
||||
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
|
||||
@@ -30,11 +29,7 @@ impl WebPainterGlow {
|
||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
|
||||
.map_err(|err| format!("Error starting glow painter: {err}"))?;
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
painter,
|
||||
})
|
||||
Ok(Self { canvas, painter })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +38,8 @@ impl WebPainter for WebPainterGlow {
|
||||
self.painter.max_texture_side()
|
||||
}
|
||||
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
fn canvas(&self) -> &HtmlCanvasElement {
|
||||
&self.canvas
|
||||
}
|
||||
|
||||
fn paint_and_update_textures(
|
||||
|
||||
@@ -41,7 +41,6 @@ impl HasDisplayHandle for EguiWebWindow {
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
render_state: Option<RenderState>,
|
||||
@@ -163,7 +162,7 @@ impl WebPainterWgpu {
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
let canvas = super::get_canvas_element_by_id_or_die(canvas_id);
|
||||
let surface = instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
@@ -188,7 +187,6 @@ impl WebPainterWgpu {
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
@@ -200,8 +198,8 @@ impl WebPainterWgpu {
|
||||
}
|
||||
|
||||
impl WebPainter for WebPainterWgpu {
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
fn canvas(&self) -> &HtmlCanvasElement {
|
||||
&self.canvas
|
||||
}
|
||||
|
||||
fn max_texture_side(&self) -> usize {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -24,6 +27,9 @@ pub struct WebRunner {
|
||||
/// They have to be in a separate `Rc` so that we don't need to pass them to
|
||||
/// the panic handler, since they aren't `Send`.
|
||||
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
|
||||
|
||||
/// Used in `destroy` to cancel a pending frame.
|
||||
request_animation_frame_id: Cell<Option<i32>>,
|
||||
}
|
||||
|
||||
impl WebRunner {
|
||||
@@ -41,6 +47,7 @@ impl WebRunner {
|
||||
panic_handler,
|
||||
runner: Rc::new(RefCell::new(None)),
|
||||
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
|
||||
request_animation_frame_id: Cell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +78,7 @@ impl WebRunner {
|
||||
events::install_color_scheme_change_event(self)?;
|
||||
}
|
||||
|
||||
events::request_animation_frame(self.clone())?;
|
||||
self.request_animation_frame()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -108,6 +115,11 @@ impl WebRunner {
|
||||
pub fn destroy(&self) {
|
||||
self.unsubscribe_from_all_events();
|
||||
|
||||
if let Some(id) = self.request_animation_frame_id.get() {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.cancel_animation_frame(id).ok();
|
||||
}
|
||||
|
||||
if let Some(runner) = self.runner.replace(None) {
|
||||
runner.destroy();
|
||||
}
|
||||
@@ -179,6 +191,18 @@ impl WebRunner {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once({
|
||||
let runner_ref = self.clone();
|
||||
move || events::paint_and_schedule(&runner_ref)
|
||||
});
|
||||
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
self.request_animation_frame_id.set(Some(id));
|
||||
closure.forget(); // We must forget it, or else the callback is canceled on drop
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -376,9 +376,6 @@ impl State {
|
||||
}
|
||||
WindowEvent::Focused(focused) => {
|
||||
self.egui_input.focused = *focused;
|
||||
// We will not be given a KeyboardInput event when the modifiers are released while
|
||||
// the window does not have focus. Unset all modifier state to be safe.
|
||||
self.egui_input.modifiers = egui::Modifiers::default();
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::WindowFocused(*focused));
|
||||
@@ -1287,7 +1284,7 @@ fn process_viewport_command(
|
||||
|
||||
use winit::window::ResizeDirection;
|
||||
|
||||
log::debug!("Processing ViewportCommand::{command:?}");
|
||||
log::trace!("Processing ViewportCommand::{command:?}");
|
||||
|
||||
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
||||
|
||||
@@ -1543,6 +1540,9 @@ pub fn create_winit_window_builder<T>(
|
||||
// wayland:
|
||||
app_id: _app_id,
|
||||
|
||||
// x11
|
||||
window_type: _window_type,
|
||||
|
||||
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
|
||||
} = viewport_builder;
|
||||
|
||||
@@ -1615,6 +1615,30 @@ pub fn create_winit_window_builder<T>(
|
||||
window_builder = window_builder.with_name(app_id, "");
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "x11", target_os = "linux"))]
|
||||
{
|
||||
if let Some(window_type) = _window_type {
|
||||
use winit::platform::x11::WindowBuilderExtX11 as _;
|
||||
use winit::platform::x11::XWindowType;
|
||||
window_builder = window_builder.with_x11_window_type(vec![match window_type {
|
||||
egui::X11WindowType::Normal => XWindowType::Normal,
|
||||
egui::X11WindowType::Utility => XWindowType::Utility,
|
||||
egui::X11WindowType::Dock => XWindowType::Dock,
|
||||
egui::X11WindowType::Desktop => XWindowType::Desktop,
|
||||
egui::X11WindowType::Toolbar => XWindowType::Toolbar,
|
||||
egui::X11WindowType::Menu => XWindowType::Menu,
|
||||
egui::X11WindowType::Splash => XWindowType::Splash,
|
||||
egui::X11WindowType::Dialog => XWindowType::Dialog,
|
||||
egui::X11WindowType::DropdownMenu => XWindowType::DropdownMenu,
|
||||
egui::X11WindowType::PopupMenu => XWindowType::PopupMenu,
|
||||
egui::X11WindowType::Tooltip => XWindowType::Tooltip,
|
||||
egui::X11WindowType::Notification => XWindowType::Notification,
|
||||
egui::X11WindowType::Combo => XWindowType::Combo,
|
||||
egui::X11WindowType::Dnd => XWindowType::Dnd,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use winit::platform::windows::WindowBuilderExtWindows as _;
|
||||
|
||||
@@ -272,6 +272,18 @@ pub struct HeaderResponse<'ui, HeaderRet> {
|
||||
}
|
||||
|
||||
impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.state.is_open()
|
||||
}
|
||||
|
||||
pub fn set_open(&mut self, open: bool) {
|
||||
self.state.set_open(open);
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self) {
|
||||
self.state.toggle(self.ui);
|
||||
}
|
||||
|
||||
/// Returns the response of the collapsing button, the custom header, and the custom body.
|
||||
pub fn body<BodyRet>(
|
||||
mut self,
|
||||
|
||||
@@ -553,6 +553,7 @@ impl ScrollArea {
|
||||
}
|
||||
|
||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||
|
||||
if (scrolling_enabled && drag_to_scroll)
|
||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
||||
@@ -577,48 +578,50 @@ impl ScrollArea {
|
||||
}
|
||||
} else {
|
||||
for d in 0..2 {
|
||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
|
||||
if let Some(scroll_target) = state.offset_target[d] {
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
|
||||
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
|
||||
// Arrived
|
||||
state.offset[d] = scroll_target.target_offset;
|
||||
state.offset_target[d] = None;
|
||||
} else {
|
||||
// Move towards target
|
||||
let t = emath::interpolation_factor(
|
||||
scroll_target.animation_time_span,
|
||||
ui.input(|i| i.time),
|
||||
dt,
|
||||
emath::ease_in_ease_out,
|
||||
);
|
||||
if t < 1.0 {
|
||||
state.offset[d] =
|
||||
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
// Arrived
|
||||
state.offset[d] = scroll_target.target_offset;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
} else {
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||
// above).
|
||||
for d in 0..2 {
|
||||
if let Some(scroll_target) = state.offset_target[d] {
|
||||
state.vel[d] = 0.0;
|
||||
|
||||
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
|
||||
// Arrived
|
||||
state.offset[d] = scroll_target.target_offset;
|
||||
state.offset_target[d] = None;
|
||||
} else {
|
||||
// Move towards target
|
||||
let t = emath::interpolation_factor(
|
||||
scroll_target.animation_time_span,
|
||||
ui.input(|i| i.time),
|
||||
dt,
|
||||
emath::ease_in_ease_out,
|
||||
);
|
||||
if t < 1.0 {
|
||||
state.offset[d] =
|
||||
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
// Arrived
|
||||
state.offset[d] = scroll_target.target_offset;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,11 +756,13 @@ impl Prepared {
|
||||
let content_size = content_ui.min_size();
|
||||
|
||||
for d in 0..2 {
|
||||
// We always take both scroll targets regardless of which scroll axes are enabled. This
|
||||
// is to avoid them leaking to other scroll areas.
|
||||
let scroll_target = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||
|
||||
if scroll_enabled[d] {
|
||||
// We take the scroll target so only this ScrollArea will use it:
|
||||
let scroll_target = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||
if let Some((target_range, align)) = scroll_target {
|
||||
let min = content_ui.min_rect().min[d];
|
||||
let clip_rect = content_ui.clip_rect();
|
||||
|
||||
@@ -431,6 +431,16 @@ impl<'open> Window<'open> {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
{
|
||||
// Prevent window from becoming larger than the constraint rect and/or screen rect.
|
||||
let screen_rect = ctx.screen_rect();
|
||||
let max_rect = area.constrain_rect().unwrap_or(screen_rect);
|
||||
let max_width = max_rect.width();
|
||||
let max_height = max_rect.height() - title_bar_height;
|
||||
resize.max_size.x = resize.max_size.x.min(max_width);
|
||||
resize.max_size.y = resize.max_size.y.min(max_height);
|
||||
}
|
||||
|
||||
// First check for resize to avoid frame delay:
|
||||
let last_frame_outer_rect = area.state().rect();
|
||||
let resize_interaction =
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::{
|
||||
emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *,
|
||||
};
|
||||
@@ -421,18 +420,17 @@ impl ContextImpl {
|
||||
// but the `screen_rect` is the most important part.
|
||||
}
|
||||
}
|
||||
let pixels_per_point = self.memory.options.zoom_factor
|
||||
* new_raw_input
|
||||
.viewport()
|
||||
.native_pixels_per_point
|
||||
.unwrap_or(1.0);
|
||||
let native_pixels_per_point = new_raw_input
|
||||
.viewport()
|
||||
.native_pixels_per_point
|
||||
.unwrap_or(1.0);
|
||||
let pixels_per_point = self.memory.options.zoom_factor * native_pixels_per_point;
|
||||
|
||||
let all_viewport_ids: ViewportIdSet = self.all_viewport_ids();
|
||||
|
||||
let viewport = self.viewports.entry(self.viewport_id()).or_default();
|
||||
|
||||
self.memory
|
||||
.begin_frame(&viewport.input, &new_raw_input, &all_viewport_ids);
|
||||
self.memory.begin_frame(&new_raw_input, &all_viewport_ids);
|
||||
|
||||
viewport.input = std::mem::take(&mut viewport.input).begin_frame(
|
||||
new_raw_input,
|
||||
@@ -440,17 +438,12 @@ impl ContextImpl {
|
||||
pixels_per_point,
|
||||
);
|
||||
|
||||
viewport.frame_state.begin_frame(&viewport.input);
|
||||
let screen_rect = viewport.input.screen_rect;
|
||||
|
||||
viewport.frame_state.begin_frame(screen_rect);
|
||||
|
||||
{
|
||||
let area_order: HashMap<LayerId, usize> = self
|
||||
.memory
|
||||
.areas()
|
||||
.order()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
let area_order = self.memory.areas().order_map();
|
||||
|
||||
let mut layers: Vec<LayerId> = viewport.widgets_prev_frame.layer_ids().collect();
|
||||
|
||||
@@ -488,7 +481,6 @@ impl ContextImpl {
|
||||
}
|
||||
|
||||
// Ensure we register the background area so panels and background ui can catch clicks:
|
||||
let screen_rect = viewport.input.screen_rect();
|
||||
self.memory.areas_mut().set_state(
|
||||
LayerId::background(),
|
||||
containers::area::State {
|
||||
@@ -600,11 +592,11 @@ impl ContextImpl {
|
||||
///
|
||||
/// For the root viewport this will return [`ViewportId::ROOT`].
|
||||
pub(crate) fn parent_viewport_id(&self) -> ViewportId {
|
||||
self.viewport_stack
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.parent
|
||||
let viewport_id = self.viewport_id();
|
||||
*self
|
||||
.viewport_parents
|
||||
.get(&viewport_id)
|
||||
.unwrap_or(&ViewportId::ROOT)
|
||||
}
|
||||
|
||||
fn all_viewport_ids(&self) -> ViewportIdSet {
|
||||
@@ -1104,9 +1096,9 @@ impl Context {
|
||||
contains_pointer: false,
|
||||
hovered: false,
|
||||
highlighted,
|
||||
clicked: Default::default(),
|
||||
double_clicked: Default::default(),
|
||||
triple_clicked: Default::default(),
|
||||
clicked: false,
|
||||
fake_primary_click: false,
|
||||
long_touched: false,
|
||||
drag_started: false,
|
||||
dragged: false,
|
||||
drag_stopped: false,
|
||||
@@ -1131,7 +1123,7 @@ impl Context {
|
||||
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
|
||||
{
|
||||
// Space/enter works like a primary click for e.g. selected buttons
|
||||
res.clicked[PointerButton::Primary as usize] = true;
|
||||
res.fake_primary_click = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
@@ -1139,7 +1131,11 @@ impl Context {
|
||||
&& sense.click
|
||||
&& input.has_accesskit_action_request(id, accesskit::Action::Default)
|
||||
{
|
||||
res.clicked[PointerButton::Primary as usize] = true;
|
||||
res.fake_primary_click = true;
|
||||
}
|
||||
|
||||
if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched {
|
||||
res.long_touched = true;
|
||||
}
|
||||
|
||||
let interaction = memory.interaction();
|
||||
@@ -1157,13 +1153,9 @@ impl Context {
|
||||
let clicked = Some(id) == viewport.interact_widgets.clicked;
|
||||
|
||||
for pointer_event in &input.pointer.pointer_events {
|
||||
if let PointerEvent::Released { click, button } = pointer_event {
|
||||
if enabled && sense.click && clicked {
|
||||
if let Some(click) = click {
|
||||
res.clicked[*button as usize] = true;
|
||||
res.double_clicked[*button as usize] = click.is_double();
|
||||
res.triple_clicked[*button as usize] = click.is_triple();
|
||||
}
|
||||
if let PointerEvent::Released { click, .. } = pointer_event {
|
||||
if enabled && sense.click && clicked && click.is_some() {
|
||||
res.clicked = true;
|
||||
}
|
||||
|
||||
res.is_pointer_button_down_on = false;
|
||||
@@ -1173,7 +1165,8 @@ impl Context {
|
||||
|
||||
// is_pointer_button_down_on is false when released, but we want interact_pointer_pos
|
||||
// to still work.
|
||||
let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_stopped;
|
||||
let is_interacted_with =
|
||||
res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped;
|
||||
if is_interacted_with {
|
||||
res.interact_pointer_pos = input.pointer.interact_pos();
|
||||
if let (Some(transform), Some(pos)) = (
|
||||
@@ -1184,7 +1177,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
if input.pointer.any_down() && !res.is_pointer_button_down_on {
|
||||
if input.pointer.any_down() && !is_interacted_with {
|
||||
// We don't hover widgets while interacting with *other* widgets:
|
||||
res.hovered = false;
|
||||
}
|
||||
@@ -1852,6 +1845,7 @@ impl Context {
|
||||
let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone());
|
||||
let InteractionSnapshot {
|
||||
clicked,
|
||||
long_touched: _,
|
||||
drag_started: _,
|
||||
dragged,
|
||||
drag_stopped: _,
|
||||
@@ -1961,7 +1955,10 @@ impl ContextImpl {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let focus_id = self.memory.focus().map_or(root_id, |id| id.accesskit_id());
|
||||
let focus_id = self
|
||||
.memory
|
||||
.focused()
|
||||
.map_or(root_id, |id| id.accesskit_id());
|
||||
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
|
||||
nodes,
|
||||
tree: Some(accesskit::Tree::new(root_id)),
|
||||
@@ -2226,7 +2223,7 @@ impl Context {
|
||||
|
||||
/// If `true`, egui is currently listening on text input (e.g. typing text in a [`TextEdit`]).
|
||||
pub fn wants_keyboard_input(&self) -> bool {
|
||||
self.memory(|m| m.interaction().focus.focused().is_some())
|
||||
self.memory(|m| m.focused().is_some())
|
||||
}
|
||||
|
||||
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
|
||||
@@ -2486,7 +2483,7 @@ impl Context {
|
||||
.on_hover_text("Is egui currently listening for text input?");
|
||||
ui.label(format!(
|
||||
"Keyboard focus widget: {}",
|
||||
self.memory(|m| m.interaction().focus.focused())
|
||||
self.memory(|m| m.focused())
|
||||
.as_ref()
|
||||
.map(Id::short_debug_format)
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Default for FrameState {
|
||||
}
|
||||
|
||||
impl FrameState {
|
||||
pub(crate) fn begin_frame(&mut self, input: &InputState) {
|
||||
pub(crate) fn begin_frame(&mut self, screen_rect: Rect) {
|
||||
crate::profile_function!();
|
||||
let Self {
|
||||
used_ids,
|
||||
@@ -94,8 +94,8 @@ impl FrameState {
|
||||
} = self;
|
||||
|
||||
used_ids.clear();
|
||||
*available_rect = input.screen_rect();
|
||||
*unused_rect = input.screen_rect();
|
||||
*available_rect = screen_rect;
|
||||
*unused_rect = screen_rect;
|
||||
*used_by_panels = Rect::NOTHING;
|
||||
*tooltip_state = None;
|
||||
*scroll_target = [None, None];
|
||||
|
||||
@@ -11,8 +11,13 @@ use touch_state::TouchState;
|
||||
/// If the pointer moves more than this, it won't become a click (but it is still a drag)
|
||||
const MAX_CLICK_DIST: f32 = 6.0; // TODO(emilk): move to settings
|
||||
|
||||
/// If the pointer is down for longer than this, it won't become a click (but it is still a drag)
|
||||
const MAX_CLICK_DURATION: f64 = 0.6; // TODO(emilk): move to settings
|
||||
/// If the pointer is down for longer than this it will no longer register as a click.
|
||||
///
|
||||
/// If a touch is held for this many seconds while still,
|
||||
/// then it will register as a "long-touch" which is equivalent to a secondary click.
|
||||
///
|
||||
/// This is to support "press and hold for context menu" on touch screens.
|
||||
const MAX_CLICK_DURATION: f64 = 0.8; // TODO(emilk): move to settings
|
||||
|
||||
/// The new pointer press must come within this many seconds from previous pointer release
|
||||
const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings
|
||||
@@ -244,20 +249,6 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
let mut modifiers = new.modifiers;
|
||||
|
||||
let focused_changed = self.focused != new.focused
|
||||
|| new
|
||||
.events
|
||||
.iter()
|
||||
.any(|e| matches!(e, Event::WindowFocused(_)));
|
||||
if focused_changed {
|
||||
// It is very common for keys to become stuck when we alt-tab, or a save-dialog opens by Ctrl+S.
|
||||
// Therefore we clear all the modifiers and down keys here to avoid that.
|
||||
modifiers = Default::default();
|
||||
keys_down = Default::default();
|
||||
}
|
||||
|
||||
Self {
|
||||
pointer,
|
||||
touch_states: self.touch_states,
|
||||
@@ -273,7 +264,7 @@ impl InputState {
|
||||
predicted_dt: new.predicted_dt,
|
||||
stable_dt,
|
||||
focused: new.focused,
|
||||
modifiers,
|
||||
modifiers: new.modifiers,
|
||||
keys_down,
|
||||
events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events
|
||||
raw: new,
|
||||
@@ -489,11 +480,7 @@ impl InputState {
|
||||
/// delivers a synthetic zoom factor based on ctrl-scroll events, as a fallback.
|
||||
pub fn multi_touch(&self) -> Option<MultiTouchInfo> {
|
||||
// In case of multiple touch devices simply pick the touch_state of the first active device
|
||||
if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) {
|
||||
touch_state.info()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.touch_states.values().find_map(|t| t.info())
|
||||
}
|
||||
|
||||
/// True if there currently are any fingers touching egui.
|
||||
@@ -548,6 +535,14 @@ impl InputState {
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A long press is something we detect on touch screens
|
||||
/// to trigger a secondary click (context menu).
|
||||
///
|
||||
/// Returns `true` only on one frame.
|
||||
pub(crate) fn is_long_touch(&self) -> bool {
|
||||
self.any_touches() && self.pointer.is_long_press()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -655,6 +650,8 @@ pub struct PointerState {
|
||||
pub(crate) has_moved_too_much_for_a_click: bool,
|
||||
|
||||
/// Did [`Self::is_decidedly_dragging`] go from `false` to `true` this frame?
|
||||
///
|
||||
/// This could also be the trigger point for a long-touch.
|
||||
pub(crate) started_decidedly_dragging: bool,
|
||||
|
||||
/// When did the pointer get click last?
|
||||
@@ -755,6 +752,7 @@ impl PointerState {
|
||||
button,
|
||||
});
|
||||
} else {
|
||||
// Released
|
||||
let clicked = self.could_any_button_be_click();
|
||||
|
||||
let click = if clicked {
|
||||
@@ -976,11 +974,13 @@ impl PointerState {
|
||||
self.pointer_events.iter().any(|event| event.is_click())
|
||||
}
|
||||
|
||||
/// Was the button given clicked this frame?
|
||||
/// Was the given pointer button given clicked this frame?
|
||||
///
|
||||
/// Returns true on double- and triple- clicks too.
|
||||
pub fn button_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Pressed { button: b, .. } if button == b))
|
||||
.any(|event| matches!(event, &PointerEvent::Released { button: b, click: Some(_) } if button == b))
|
||||
}
|
||||
|
||||
/// Was the button given double clicked this frame?
|
||||
@@ -1029,21 +1029,21 @@ impl PointerState {
|
||||
///
|
||||
/// See also [`Self::is_decidedly_dragging`].
|
||||
pub fn could_any_button_be_click(&self) -> bool {
|
||||
if !self.any_down() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.has_moved_too_much_for_a_click {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(press_start_time) = self.press_start_time {
|
||||
if self.time - press_start_time > MAX_CLICK_DURATION {
|
||||
if self.any_down() || self.any_released() {
|
||||
if self.has_moved_too_much_for_a_click {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
if let Some(press_start_time) = self.press_start_time {
|
||||
if self.time - press_start_time > MAX_CLICK_DURATION {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Just because the mouse is down doesn't mean we are dragging.
|
||||
@@ -1062,6 +1062,19 @@ impl PointerState {
|
||||
&& !self.any_click()
|
||||
}
|
||||
|
||||
/// A long press is something we detect on touch screens
|
||||
/// to trigger a secondary click (context menu).
|
||||
///
|
||||
/// Returns `true` only on one frame.
|
||||
pub(crate) fn is_long_press(&self) -> bool {
|
||||
self.started_decidedly_dragging
|
||||
&& !self.has_moved_too_much_for_a_click
|
||||
&& self.button_down(PointerButton::Primary)
|
||||
&& self.press_start_time.map_or(false, |press_start_time| {
|
||||
self.time - press_start_time > MAX_CLICK_DURATION
|
||||
})
|
||||
}
|
||||
|
||||
/// Is the primary button currently down?
|
||||
#[inline(always)]
|
||||
pub fn primary_down(&self) -> bool {
|
||||
|
||||
@@ -163,6 +163,7 @@ impl TouchState {
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// This needs to be called each frame, even if there are no new touch events.
|
||||
// Otherwise, we would send the same old delta information multiple times:
|
||||
self.update_gesture(time, pointer_pos);
|
||||
@@ -176,10 +177,6 @@ impl TouchState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.gesture_state.is_some()
|
||||
}
|
||||
|
||||
pub fn info(&self) -> Option<MultiTouchInfo> {
|
||||
self.gesture_state.as_ref().map(|state| {
|
||||
// state.previous can be `None` when the number of simultaneous touches has just
|
||||
|
||||
@@ -14,6 +14,10 @@ pub struct InteractionSnapshot {
|
||||
/// The widget that got clicked this frame.
|
||||
pub clicked: Option<Id>,
|
||||
|
||||
/// This widget was long-pressed on a touch screen,
|
||||
/// so trigger a secondary click on it (context menu).
|
||||
pub long_touched: Option<Id>,
|
||||
|
||||
/// Drag started on this widget this frame.
|
||||
///
|
||||
/// This will also be found in `dragged` this frame.
|
||||
@@ -56,6 +60,7 @@ impl InteractionSnapshot {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
clicked,
|
||||
long_touched,
|
||||
drag_started,
|
||||
dragged,
|
||||
drag_stopped,
|
||||
@@ -74,6 +79,10 @@ impl InteractionSnapshot {
|
||||
id_ui(ui, clicked);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("long_touched");
|
||||
id_ui(ui, long_touched);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("drag_started");
|
||||
id_ui(ui, drag_started);
|
||||
ui.end_row();
|
||||
@@ -123,6 +132,21 @@ pub(crate) fn interact(
|
||||
|
||||
let mut clicked = None;
|
||||
let mut dragged = prev_snapshot.dragged;
|
||||
let mut long_touched = None;
|
||||
|
||||
if input.is_long_touch() {
|
||||
// We implement "press-and-hold for context menu" on touch screens here
|
||||
if let Some(widget) = interaction
|
||||
.potential_click_id
|
||||
.and_then(|id| widgets.get(id))
|
||||
{
|
||||
dragged = None;
|
||||
clicked = Some(widget.id);
|
||||
long_touched = Some(widget.id);
|
||||
interaction.potential_click_id = None;
|
||||
interaction.potential_drag_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: in the current code a press-release in the same frame is NOT considered a drag.
|
||||
for pointer_event in &input.pointer.pointer_events {
|
||||
@@ -142,7 +166,7 @@ pub(crate) fn interact(
|
||||
}
|
||||
|
||||
PointerEvent::Released { click, button: _ } => {
|
||||
if click.is_some() {
|
||||
if click.is_some() && !input.pointer.is_decidedly_dragging() {
|
||||
if let Some(widget) = interaction
|
||||
.potential_click_id
|
||||
.and_then(|id| widgets.get(id))
|
||||
@@ -179,6 +203,15 @@ pub(crate) fn interact(
|
||||
}
|
||||
}
|
||||
|
||||
if !input.pointer.could_any_button_be_click() {
|
||||
interaction.potential_click_id = None;
|
||||
}
|
||||
|
||||
if !input.pointer.any_down() || input.pointer.latest_pos().is_none() {
|
||||
interaction.potential_click_id = None;
|
||||
interaction.potential_drag_id = None;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
let drag_changed = dragged != prev_snapshot.dragged;
|
||||
@@ -201,9 +234,14 @@ pub(crate) fn interact(
|
||||
.map(|w| w.id)
|
||||
.collect();
|
||||
|
||||
let hovered = if clicked.is_some() || dragged.is_some() {
|
||||
// If currently clicking or dragging, nothing else is hovered.
|
||||
clicked.iter().chain(&dragged).copied().collect()
|
||||
let hovered = if clicked.is_some() || dragged.is_some() || long_touched.is_some() {
|
||||
// If currently clicking or dragging, only that and nothing else is hovered.
|
||||
clicked
|
||||
.iter()
|
||||
.chain(&dragged)
|
||||
.chain(&long_touched)
|
||||
.copied()
|
||||
.collect()
|
||||
} else if hits.click.is_some() || hits.drag.is_some() {
|
||||
// We are hovering over an interactive widget or two.
|
||||
hits.click.iter().chain(&hits.drag).map(|w| w.id).collect()
|
||||
@@ -220,6 +258,7 @@ pub(crate) fn interact(
|
||||
|
||||
InteractionSnapshot {
|
||||
clicked,
|
||||
long_touched,
|
||||
drag_started,
|
||||
dragged,
|
||||
drag_stopped,
|
||||
|
||||
@@ -194,7 +194,6 @@ impl Widget for &memory::InteractionState {
|
||||
let memory::InteractionState {
|
||||
potential_click_id,
|
||||
potential_drag_id,
|
||||
focus: _,
|
||||
} = self;
|
||||
|
||||
ui.vertical(|ui| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use ahash::HashMap;
|
||||
use epaint::emath::TSTransform;
|
||||
|
||||
use crate::{
|
||||
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2,
|
||||
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2,
|
||||
ViewportId, ViewportIdMap, ViewportIdSet,
|
||||
};
|
||||
|
||||
@@ -95,6 +95,9 @@ pub struct Memory {
|
||||
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
pub(crate) interactions: ViewportIdMap<InteractionState>,
|
||||
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
pub(crate) focus: ViewportIdMap<Focus>,
|
||||
}
|
||||
|
||||
impl Default for Memory {
|
||||
@@ -105,6 +108,7 @@ impl Default for Memory {
|
||||
caches: Default::default(),
|
||||
new_font_definitions: Default::default(),
|
||||
interactions: Default::default(),
|
||||
focus: Default::default(),
|
||||
viewport_id: Default::default(),
|
||||
areas: Default::default(),
|
||||
layer_transforms: Default::default(),
|
||||
@@ -308,8 +312,6 @@ pub(crate) struct InteractionState {
|
||||
/// as that can only happen after the mouse has moved a bit
|
||||
/// (at least if the widget is interesated in both clicks and drags).
|
||||
pub potential_drag_id: Option<Id>,
|
||||
|
||||
pub focus: Focus,
|
||||
}
|
||||
|
||||
/// Keeps tracks of what widget has keyboard focus
|
||||
@@ -362,24 +364,6 @@ impl InteractionState {
|
||||
pub fn is_using_pointer(&self) -> bool {
|
||||
self.potential_click_id.is_some() || self.potential_drag_id.is_some()
|
||||
}
|
||||
|
||||
fn begin_frame(
|
||||
&mut self,
|
||||
prev_input: &crate::input_state::InputState,
|
||||
new_input: &crate::data::input::RawInput,
|
||||
) {
|
||||
if !prev_input.pointer.could_any_button_be_click() {
|
||||
self.potential_click_id = None;
|
||||
}
|
||||
|
||||
if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
|
||||
// pointer button was not down last frame
|
||||
self.potential_click_id = None;
|
||||
self.potential_drag_id = None;
|
||||
}
|
||||
|
||||
self.focus.begin_frame(new_input);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focus {
|
||||
@@ -603,30 +587,29 @@ impl Focus {
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
pub(crate) fn begin_frame(
|
||||
&mut self,
|
||||
prev_input: &crate::input_state::InputState,
|
||||
new_input: &crate::data::input::RawInput,
|
||||
viewports: &ViewportIdSet,
|
||||
) {
|
||||
pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
|
||||
crate::profile_function!();
|
||||
|
||||
self.viewport_id = new_raw_input.viewport_id;
|
||||
|
||||
// Cleanup
|
||||
self.interactions.retain(|id, _| viewports.contains(id));
|
||||
self.areas.retain(|id, _| viewports.contains(id));
|
||||
|
||||
self.viewport_id = new_input.viewport_id;
|
||||
self.interactions
|
||||
self.areas.entry(self.viewport_id).or_default();
|
||||
|
||||
// self.interactions is handled elsewhere
|
||||
|
||||
self.focus
|
||||
.entry(self.viewport_id)
|
||||
.or_default()
|
||||
.begin_frame(prev_input, new_input);
|
||||
self.areas.entry(self.viewport_id).or_default();
|
||||
.begin_frame(new_raw_input);
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
|
||||
self.caches.update();
|
||||
self.areas_mut().end_frame();
|
||||
self.interaction_mut().focus.end_frame(used_ids);
|
||||
self.focus_mut().end_frame(used_ids);
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
|
||||
@@ -656,7 +639,7 @@ impl Memory {
|
||||
}
|
||||
|
||||
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
|
||||
self.interaction().focus.id_previous_frame == Some(id)
|
||||
self.focus().id_previous_frame == Some(id)
|
||||
}
|
||||
|
||||
/// True if the given widget had keyboard focus last frame, but not this one.
|
||||
@@ -677,12 +660,12 @@ impl Memory {
|
||||
/// from the window and back.
|
||||
#[inline(always)]
|
||||
pub fn has_focus(&self, id: Id) -> bool {
|
||||
self.interaction().focus.focused() == Some(id)
|
||||
self.focused() == Some(id)
|
||||
}
|
||||
|
||||
/// Which widget has keyboard focus?
|
||||
pub fn focus(&self) -> Option<Id> {
|
||||
self.interaction().focus.focused()
|
||||
pub fn focused(&self) -> Option<Id> {
|
||||
self.focus().focused()
|
||||
}
|
||||
|
||||
/// Set an event filter for a widget.
|
||||
@@ -693,7 +676,7 @@ impl Memory {
|
||||
/// You must first give focus to the widget before calling this.
|
||||
pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) {
|
||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||
if let Some(focused) = &mut self.interaction_mut().focus.focused_widget {
|
||||
if let Some(focused) = &mut self.focus_mut().focused_widget {
|
||||
if focused.id == id {
|
||||
focused.filter = event_filter;
|
||||
}
|
||||
@@ -705,16 +688,16 @@ impl Memory {
|
||||
/// See also [`crate::Response::request_focus`].
|
||||
#[inline(always)]
|
||||
pub fn request_focus(&mut self, id: Id) {
|
||||
self.interaction_mut().focus.focused_widget = Some(FocusWidget::new(id));
|
||||
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
|
||||
}
|
||||
|
||||
/// Surrender keyboard focus for a specific widget.
|
||||
/// See also [`crate::Response::surrender_focus`].
|
||||
#[inline(always)]
|
||||
pub fn surrender_focus(&mut self, id: Id) {
|
||||
let interaction = self.interaction_mut();
|
||||
if interaction.focus.focused() == Some(id) {
|
||||
interaction.focus.focused_widget = None;
|
||||
let focus = self.focus_mut();
|
||||
if focus.focused() == Some(id) {
|
||||
focus.focused_widget = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,13 +710,13 @@ impl Memory {
|
||||
/// and rendered correctly in a single frame.
|
||||
#[inline(always)]
|
||||
pub fn interested_in_focus(&mut self, id: Id) {
|
||||
self.interaction_mut().focus.interested_in_focus(id);
|
||||
self.focus_mut().interested_in_focus(id);
|
||||
}
|
||||
|
||||
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
|
||||
#[inline(always)]
|
||||
pub fn stop_text_input(&mut self) {
|
||||
self.interaction_mut().focus.focused_widget = None;
|
||||
self.focus_mut().focused_widget = None;
|
||||
}
|
||||
|
||||
/// Is any widget being dragged?
|
||||
@@ -813,6 +796,16 @@ impl Memory {
|
||||
pub(crate) fn interaction_mut(&mut self) -> &mut InteractionState {
|
||||
self.interactions.entry(self.viewport_id).or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn focus(&self) -> &Focus {
|
||||
self.focus
|
||||
.get(&self.viewport_id)
|
||||
.expect("Failed to get focus")
|
||||
}
|
||||
|
||||
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
|
||||
self.focus.entry(self.viewport_id).or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Popups
|
||||
@@ -908,6 +901,15 @@ impl Areas {
|
||||
&self.order
|
||||
}
|
||||
|
||||
/// For each layer, which order is it in [`Self::order`]?
|
||||
pub(crate) fn order_map(&self) -> HashMap<LayerId, usize> {
|
||||
self.order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::State) {
|
||||
self.visible_current_frame.insert(layer_id);
|
||||
self.areas.insert(layer_id.id, state);
|
||||
|
||||
@@ -367,6 +367,9 @@ impl MenuRoot {
|
||||
/// Interaction with a context menu (secondary click).
|
||||
fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
|
||||
let response = response.interact(Sense::click());
|
||||
let hovered = response.hovered();
|
||||
let secondary_clicked = response.secondary_clicked();
|
||||
|
||||
response.ctx.input(|input| {
|
||||
let pointer = &input.pointer;
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
@@ -377,9 +380,9 @@ impl MenuRoot {
|
||||
destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
if response.hovered() && response.secondary_clicked() {
|
||||
if hovered && secondary_clicked {
|
||||
return MenuResponse::Create(pos, response.id);
|
||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||
} else if destroy || hovered && pointer.primary_down() {
|
||||
return MenuResponse::Close;
|
||||
}
|
||||
}
|
||||
@@ -613,30 +616,42 @@ impl MenuState {
|
||||
let pointer = ui.input(|i| i.pointer.clone());
|
||||
let open = self.is_open(sub_id);
|
||||
if self.moving_towards_current_submenu(&pointer) {
|
||||
// We don't close the submenu if the pointer is on its way to hover it.
|
||||
// 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
|
||||
&& ui.interact_bg(Sense::hover()).contains_pointer()
|
||||
&& !button.hovered()
|
||||
&& !self.hovering_current_submenu(&pointer)
|
||||
{
|
||||
// We are hovering something else in the menu, so close the submenu.
|
||||
self.close_submenu();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if `dir` points from `pos` towards left side of `rect`.
|
||||
fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool {
|
||||
let vel_a = dir.angle();
|
||||
let top_a = (rect.left_top() - pos).angle();
|
||||
let bottom_a = (rect.left_bottom() - pos).angle();
|
||||
bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0
|
||||
}
|
||||
|
||||
/// Check if pointer is moving towards current submenu.
|
||||
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if pointer.is_still() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(sub_menu) = self.current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect);
|
||||
let rect = sub_menu.read().rect;
|
||||
return rect.intersects_ray(pos, pointer.velocity().normalized());
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if pointer is hovering current submenu.
|
||||
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if let Some(sub_menu) = self.current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return sub_menu.read().area_contains(pos);
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -673,4 +688,8 @@ impl MenuState {
|
||||
self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos)))));
|
||||
}
|
||||
}
|
||||
|
||||
fn close_submenu(&mut self) {
|
||||
self.sub_menu = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::{any::Any, sync::Arc};
|
||||
use crate::{
|
||||
emath::{Align, Pos2, Rect, Vec2},
|
||||
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetRect, WidgetText,
|
||||
NUM_POINTER_BUTTONS,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -15,7 +14,10 @@ use crate::{
|
||||
///
|
||||
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
||||
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
||||
// TODO(emilk): we should be using bit sets instead of so many bools
|
||||
///
|
||||
/// ⚠️ The `Response` contains a clone of [`Context`], and many methods lock the `Context`.
|
||||
/// It can therefor be a deadlock to use `Context` from within a context-locking closures,
|
||||
/// such as [`Context::input`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Response {
|
||||
// CONTEXT:
|
||||
@@ -69,18 +71,27 @@ pub struct Response {
|
||||
#[doc(hidden)]
|
||||
pub highlighted: bool,
|
||||
|
||||
/// The pointer clicked this thing this frame.
|
||||
/// This widget was clicked this frame.
|
||||
///
|
||||
/// Which pointer and how many times we don't know,
|
||||
/// and ask [`crate::InputState`] about at runtime.
|
||||
///
|
||||
/// This is only set to true if the widget was clicked
|
||||
/// by an actual mouse.
|
||||
#[doc(hidden)]
|
||||
pub clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
pub clicked: bool,
|
||||
|
||||
// TODO(emilk): `released` for sliders
|
||||
/// The thing was double-clicked.
|
||||
/// This widget should act as if clicked due
|
||||
/// to something else than a click.
|
||||
///
|
||||
/// This is set to true if the widget has keyboard focus and
|
||||
/// the user hit the Space or Enter key.
|
||||
#[doc(hidden)]
|
||||
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
pub fake_primary_click: bool,
|
||||
|
||||
/// The thing was triple-clicked.
|
||||
/// This widget was long-pressed on a touch screen to simulate a secondary click.
|
||||
#[doc(hidden)]
|
||||
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
pub long_touched: bool,
|
||||
|
||||
/// The widget started being dragged this frame.
|
||||
#[doc(hidden)]
|
||||
@@ -118,55 +129,75 @@ impl Response {
|
||||
/// A click is registered when the mouse or touch is released within
|
||||
/// a certain amount of time and distance from when and where it was pressed.
|
||||
///
|
||||
/// This will also return true if the widget was clicked via accessibility integration,
|
||||
/// or if the widget had keyboard focus and the use pressed Space/Enter.
|
||||
///
|
||||
/// Note that the widget must be sensing clicks with [`Sense::click`].
|
||||
/// [`crate::Button`] senses clicks; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
||||
///
|
||||
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
|
||||
#[inline(always)]
|
||||
pub fn clicked(&self) -> bool {
|
||||
self.clicked[PointerButton::Primary as usize]
|
||||
self.fake_primary_click || self.clicked_by(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the given button.
|
||||
/// Returns true if this widget was clicked this frame by the given mouse button.
|
||||
///
|
||||
/// This will NOT return true if the widget was "clicked" via
|
||||
/// some accessibility integration, or if the widget had keyboard focus and the
|
||||
/// user pressed Space/Enter. For that, use [`Self::clicked`] instead.
|
||||
///
|
||||
/// This will likewise ignore the press-and-hold action on touch screens.
|
||||
/// Use [`Self::secondary_clicked`] instead to also detect that.
|
||||
#[inline]
|
||||
pub fn clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.clicked[button as usize]
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button))
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
|
||||
///
|
||||
/// This also returns true if the widget was pressed-and-held on a touch screen.
|
||||
#[inline]
|
||||
pub fn secondary_clicked(&self) -> bool {
|
||||
self.clicked[PointerButton::Secondary as usize]
|
||||
self.long_touched || self.clicked_by(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Was this long-pressed on a touch screen?
|
||||
///
|
||||
/// Usually you want to check [`Self::secondary_clicked`] instead.
|
||||
#[inline]
|
||||
pub fn long_touched(&self) -> bool {
|
||||
self.long_touched
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the middle mouse button.
|
||||
#[inline]
|
||||
pub fn middle_clicked(&self) -> bool {
|
||||
self.clicked[PointerButton::Middle as usize]
|
||||
self.clicked_by(PointerButton::Middle)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was double-clicked this frame by the primary button.
|
||||
#[inline]
|
||||
pub fn double_clicked(&self) -> bool {
|
||||
self.double_clicked[PointerButton::Primary as usize]
|
||||
self.double_clicked_by(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was triple-clicked this frame by the primary button.
|
||||
#[inline]
|
||||
pub fn triple_clicked(&self) -> bool {
|
||||
self.triple_clicked[PointerButton::Primary as usize]
|
||||
self.triple_clicked_by(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was double-clicked this frame by the given button.
|
||||
#[inline]
|
||||
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.double_clicked[button as usize]
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_double_clicked(button))
|
||||
}
|
||||
|
||||
/// Returns true if this widget was triple-clicked this frame by the given button.
|
||||
#[inline]
|
||||
pub fn triple_clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.triple_clicked[button as usize]
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_triple_clicked(button))
|
||||
}
|
||||
|
||||
/// `true` if there was a click *outside* this widget this frame.
|
||||
@@ -917,27 +948,9 @@ impl Response {
|
||||
contains_pointer: self.contains_pointer || other.contains_pointer,
|
||||
hovered: self.hovered || other.hovered,
|
||||
highlighted: self.highlighted || other.highlighted,
|
||||
clicked: [
|
||||
self.clicked[0] || other.clicked[0],
|
||||
self.clicked[1] || other.clicked[1],
|
||||
self.clicked[2] || other.clicked[2],
|
||||
self.clicked[3] || other.clicked[3],
|
||||
self.clicked[4] || other.clicked[4],
|
||||
],
|
||||
double_clicked: [
|
||||
self.double_clicked[0] || other.double_clicked[0],
|
||||
self.double_clicked[1] || other.double_clicked[1],
|
||||
self.double_clicked[2] || other.double_clicked[2],
|
||||
self.double_clicked[3] || other.double_clicked[3],
|
||||
self.double_clicked[4] || other.double_clicked[4],
|
||||
],
|
||||
triple_clicked: [
|
||||
self.triple_clicked[0] || other.triple_clicked[0],
|
||||
self.triple_clicked[1] || other.triple_clicked[1],
|
||||
self.triple_clicked[2] || other.triple_clicked[2],
|
||||
self.triple_clicked[3] || other.triple_clicked[3],
|
||||
self.triple_clicked[4] || other.triple_clicked[4],
|
||||
],
|
||||
clicked: self.clicked || other.clicked,
|
||||
fake_primary_click: self.fake_primary_click || other.fake_primary_click,
|
||||
long_touched: self.long_touched || other.long_touched,
|
||||
drag_started: self.drag_started || other.drag_started,
|
||||
dragged: self.dragged || other.dragged,
|
||||
drag_stopped: self.drag_stopped || other.drag_stopped,
|
||||
|
||||
@@ -281,6 +281,9 @@ pub struct Spacing {
|
||||
/// Default width of a [`Slider`].
|
||||
pub slider_width: f32,
|
||||
|
||||
/// Default rail height of a [`Slider`].
|
||||
pub slider_rail_height: f32,
|
||||
|
||||
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
|
||||
pub combo_width: f32,
|
||||
|
||||
@@ -1224,6 +1227,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,
|
||||
slider_rail_height: 8.0,
|
||||
combo_width: 100.0,
|
||||
text_edit_width: 280.0,
|
||||
icon_width: 14.0,
|
||||
@@ -1573,6 +1577,7 @@ impl Spacing {
|
||||
indent,
|
||||
interact_size,
|
||||
slider_width,
|
||||
slider_rail_height,
|
||||
combo_width,
|
||||
text_edit_width,
|
||||
icon_width,
|
||||
@@ -1601,6 +1606,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(slider_rail_height).clamp_range(0.0..=50.0));
|
||||
ui.label("Slider rail height");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("ComboBox width");
|
||||
|
||||
@@ -633,8 +633,13 @@ impl Ui {
|
||||
.rect_contains_pointer(self.layer_id(), self.clip_rect().intersect(rect))
|
||||
}
|
||||
|
||||
/// Is the pointer (mouse/touch) above this [`Ui`]?
|
||||
/// Is the pointer (mouse/touch) above the current [`Ui`]?
|
||||
///
|
||||
/// Equivalent to `ui.rect_contains_pointer(ui.min_rect())`
|
||||
///
|
||||
/// Note that this tests against the _current_ [`Ui::min_rect`].
|
||||
/// If you want to test against the final `min_rect`,
|
||||
/// use [`Self::interact_bg`] instead.
|
||||
pub fn ui_contains_pointer(&self) -> bool {
|
||||
self.rect_contains_pointer(self.min_rect())
|
||||
}
|
||||
|
||||
@@ -301,6 +301,9 @@ pub struct ViewportBuilder {
|
||||
pub window_level: Option<WindowLevel>,
|
||||
|
||||
pub mouse_passthrough: Option<bool>,
|
||||
|
||||
// X11
|
||||
pub window_type: Option<X11WindowType>,
|
||||
}
|
||||
|
||||
impl ViewportBuilder {
|
||||
@@ -583,6 +586,15 @@ impl ViewportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// ### On X11
|
||||
/// This sets the window type.
|
||||
/// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
|
||||
#[inline]
|
||||
pub fn with_window_type(mut self, value: X11WindowType) -> Self {
|
||||
self.window_type = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Update this `ViewportBuilder` with a delta,
|
||||
/// returning a list of commands and a bool indicating if the window needs to be recreated.
|
||||
#[must_use]
|
||||
@@ -613,6 +625,7 @@ impl ViewportBuilder {
|
||||
window_level: new_window_level,
|
||||
mouse_passthrough: new_mouse_passthrough,
|
||||
taskbar: new_taskbar,
|
||||
window_type: new_window_type,
|
||||
} = new_vp_builder;
|
||||
|
||||
let mut commands = Vec::new();
|
||||
@@ -786,6 +799,11 @@ impl ViewportBuilder {
|
||||
recreate_window = true;
|
||||
}
|
||||
|
||||
if new_window_type.is_some() && self.window_type != new_window_type {
|
||||
self.window_type = new_window_type;
|
||||
recreate_window = true;
|
||||
}
|
||||
|
||||
(commands, recreate_window)
|
||||
}
|
||||
}
|
||||
@@ -799,6 +817,61 @@ pub enum WindowLevel {
|
||||
AlwaysOnTop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum X11WindowType {
|
||||
/// This is a normal, top-level window.
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
/// A desktop feature. This can include a single window containing desktop icons with the same dimensions as the
|
||||
/// screen, allowing the desktop environment to have full control of the desktop, without the need for proxying
|
||||
/// root window clicks.
|
||||
Desktop,
|
||||
|
||||
/// A dock or panel feature. Typically a Window Manager would keep such windows on top of all other windows.
|
||||
Dock,
|
||||
|
||||
/// Toolbar windows. "Torn off" from the main application.
|
||||
Toolbar,
|
||||
|
||||
/// Pinnable menu windows. "Torn off" from the main application.
|
||||
Menu,
|
||||
|
||||
/// A small persistent utility window, such as a palette or toolbox.
|
||||
Utility,
|
||||
|
||||
/// The window is a splash screen displayed as an application is starting up.
|
||||
Splash,
|
||||
|
||||
/// This is a dialog window.
|
||||
Dialog,
|
||||
|
||||
/// A dropdown menu that usually appears when the user clicks on an item in a menu bar.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
DropdownMenu,
|
||||
|
||||
/// A popup menu that usually appears when the user right clicks on an object.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
PopupMenu,
|
||||
|
||||
/// A tooltip window. Usually used to show additional information when hovering over an object with the cursor.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Tooltip,
|
||||
|
||||
/// The window is a notification.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Notification,
|
||||
|
||||
/// This should be used on the windows that are popped up by combo boxes.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Combo,
|
||||
|
||||
/// This indicates the the window is being dragged.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Dnd,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum IMEPurpose {
|
||||
|
||||
@@ -680,11 +680,12 @@ impl<'a> Slider<'a> {
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let value = self.get_value();
|
||||
|
||||
let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect));
|
||||
let rail_rect = self.rail_rect(rect, rail_radius);
|
||||
|
||||
let visuals = ui.style().interact(response);
|
||||
let widget_visuals = &ui.visuals().widgets;
|
||||
let spacing = &ui.style().spacing;
|
||||
|
||||
let rail_radius = (spacing.slider_rail_height / 2.0).at_least(0.0);
|
||||
let rail_rect = self.rail_rect(rect, rail_radius);
|
||||
|
||||
ui.painter().rect_filled(
|
||||
rail_rect,
|
||||
@@ -800,13 +801,6 @@ impl<'a> Slider<'a> {
|
||||
limit / 2.5
|
||||
}
|
||||
|
||||
fn rail_radius_limit(&self, rect: &Rect) -> f32 {
|
||||
match self.orientation {
|
||||
SliderOrientation::Horizontal => (rect.height() / 4.0).at_least(2.0),
|
||||
SliderOrientation::Vertical => (rect.width() / 4.0).at_least(2.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn value_ui(&mut self, ui: &mut Ui, position_range: Rangef) -> Response {
|
||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||
let change = ui.input(|input| {
|
||||
|
||||
@@ -80,6 +80,8 @@ impl super::View for ContextMenus {
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.horizontal(|ui| {
|
||||
self.example_plot(ui).context_menu(|ui| {
|
||||
ui.set_min_width(220.0);
|
||||
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
@@ -96,12 +98,12 @@ impl super::View for ContextMenus {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
.prefix("Width: "),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
.prefix("Height: "),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
|
||||
@@ -11,7 +11,7 @@ impl Eq for PanZoom {}
|
||||
|
||||
impl super::Demo for PanZoom {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Pan Zoom"
|
||||
"🔍 Pan Zoom"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
|
||||
@@ -486,6 +486,10 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
if response.long_touched() {
|
||||
writeln!(new_info, "Clicked with long-press").ok();
|
||||
}
|
||||
|
||||
new_info
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ pub struct DatePickerButton<'a> {
|
||||
calendar: bool,
|
||||
calendar_week: bool,
|
||||
show_icon: bool,
|
||||
format: String,
|
||||
highlight_weekends: bool,
|
||||
}
|
||||
|
||||
impl<'a> DatePickerButton<'a> {
|
||||
@@ -28,6 +30,8 @@ impl<'a> DatePickerButton<'a> {
|
||||
calendar: true,
|
||||
calendar_week: true,
|
||||
show_icon: true,
|
||||
format: "%Y-%m-%d".to_owned(),
|
||||
highlight_weekends: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +77,21 @@ impl<'a> DatePickerButton<'a> {
|
||||
self.show_icon = show_icon;
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the format shown on the button. (Default: %Y-%m-%d)
|
||||
/// See [`chrono::format::strftime`] for valid formats.
|
||||
#[inline]
|
||||
pub fn format(mut self, format: impl Into<String>) -> Self {
|
||||
self.format = format.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Highlight weekend days. (Default: true)
|
||||
#[inline]
|
||||
pub fn highlight_weekends(mut self, highlight_weekends: bool) -> Self {
|
||||
self.highlight_weekends = highlight_weekends;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for DatePickerButton<'a> {
|
||||
@@ -83,9 +102,9 @@ impl<'a> Widget for DatePickerButton<'a> {
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut text = if self.show_icon {
|
||||
RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d")))
|
||||
RichText::new(format!("{} 📆", self.selection.format(&self.format)))
|
||||
} else {
|
||||
RichText::new(format!("{}", self.selection.format("%Y-%m-%d")))
|
||||
RichText::new(format!("{}", self.selection.format(&self.format)))
|
||||
};
|
||||
let visuals = ui.visuals().widgets.open;
|
||||
if button_state.picker_visible {
|
||||
@@ -138,6 +157,7 @@ impl<'a> Widget for DatePickerButton<'a> {
|
||||
arrows: self.arrows,
|
||||
calendar: self.calendar,
|
||||
calendar_week: self.calendar_week,
|
||||
highlight_weekends: self.highlight_weekends,
|
||||
}
|
||||
.draw(ui)
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(crate) struct DatePickerPopup<'a> {
|
||||
pub arrows: bool,
|
||||
pub calendar: bool,
|
||||
pub calendar_week: bool,
|
||||
pub highlight_weekends: bool,
|
||||
}
|
||||
|
||||
impl<'a> DatePickerPopup<'a> {
|
||||
@@ -304,8 +305,9 @@ impl<'a> DatePickerPopup<'a> {
|
||||
&& popup_state.day == day.day()
|
||||
{
|
||||
ui.visuals().selection.bg_fill
|
||||
} else if day.weekday() == Weekday::Sat
|
||||
|| day.weekday() == Weekday::Sun
|
||||
} else if (day.weekday() == Weekday::Sat
|
||||
|| day.weekday() == Weekday::Sun)
|
||||
&& self.highlight_weekends
|
||||
{
|
||||
if ui.visuals().dark_mode {
|
||||
Color32::DARK_RED
|
||||
|
||||
@@ -9,11 +9,11 @@ use crate::*;
|
||||
|
||||
use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform};
|
||||
use rect_elem::*;
|
||||
use values::{ClosestElem, PlotGeometry};
|
||||
use values::ClosestElem;
|
||||
|
||||
pub use bar::Bar;
|
||||
pub use box_elem::{BoxElem, BoxSpread};
|
||||
pub use values::{LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints};
|
||||
pub use values::{LineStyle, MarkerShape, Orientation, PlotGeometry, PlotPoint, PlotPoints};
|
||||
|
||||
mod bar;
|
||||
mod box_elem;
|
||||
|
||||
@@ -23,7 +23,8 @@ pub use crate::{
|
||||
axis::{Axis, AxisHints, HPlacement, Placement, VPlacement},
|
||||
items::{
|
||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
||||
Orientation, PlotImage, PlotItem, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
|
||||
Orientation, PlotGeometry, PlotImage, PlotItem, PlotPoint, PlotPoints, Points, Polygon,
|
||||
Text, VLine,
|
||||
},
|
||||
legend::{Corner, Legend},
|
||||
memory::PlotMemory,
|
||||
|
||||
@@ -116,6 +116,11 @@ impl PlotUi {
|
||||
self.last_plot_transform.value_from_position(position)
|
||||
}
|
||||
|
||||
/// Add an arbitrary item.
|
||||
pub fn add(&mut self, item: impl PlotItem + 'static) {
|
||||
self.items.push(Box::new(item));
|
||||
}
|
||||
|
||||
/// Add a data line.
|
||||
pub fn line(&mut self, mut line: Line) {
|
||||
if line.series.is_empty() {
|
||||
|
||||
@@ -605,6 +605,32 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Does this Rect intersect the given ray (where `d` is normalized)?
|
||||
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
|
||||
let mut tmin = -f32::INFINITY;
|
||||
let mut tmax = f32::INFINITY;
|
||||
|
||||
if d.x != 0.0 {
|
||||
let tx1 = (self.min.x - o.x) / d.x;
|
||||
let tx2 = (self.max.x - o.x) / d.x;
|
||||
|
||||
tmin = tmin.max(tx1.min(tx2));
|
||||
tmax = tmax.min(tx1.max(tx2));
|
||||
}
|
||||
|
||||
if d.y != 0.0 {
|
||||
let ty1 = (self.min.y - o.y) / d.y;
|
||||
let ty2 = (self.max.y - o.y) / d.y;
|
||||
|
||||
tmin = tmin.max(ty1.min(ty2));
|
||||
tmax = tmax.min(ty1.max(ty2));
|
||||
}
|
||||
|
||||
tmin <= tmax
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Rect {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{:?} - {:?}]", self.min, self.max)
|
||||
|
||||
@@ -47,8 +47,8 @@ pub use {
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
shadow::Shadow,
|
||||
shape::{
|
||||
CircleShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, Rounding, Shape,
|
||||
TextShape,
|
||||
CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape,
|
||||
Rounding, Shape, TextShape,
|
||||
},
|
||||
stats::PaintStats,
|
||||
stroke::Stroke,
|
||||
|
||||
@@ -30,6 +30,9 @@ pub enum Shape {
|
||||
/// Circle with optional outline and fill.
|
||||
Circle(CircleShape),
|
||||
|
||||
/// Ellipse with optional outline and fill.
|
||||
Ellipse(EllipseShape),
|
||||
|
||||
/// A line between two points.
|
||||
LineSegment { points: [Pos2; 2], stroke: Stroke },
|
||||
|
||||
@@ -236,6 +239,16 @@ impl Shape {
|
||||
Self::Circle(CircleShape::stroke(center, radius, stroke))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into<Color32>) -> Self {
|
||||
Self::Ellipse(EllipseShape::filled(center, radius, fill_color))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Ellipse(EllipseShape::stroke(center, radius, stroke))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rect_filled(
|
||||
rect: Rect,
|
||||
@@ -324,6 +337,7 @@ impl Shape {
|
||||
rect
|
||||
}
|
||||
Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(),
|
||||
Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(),
|
||||
Self::LineSegment { points, stroke } => {
|
||||
if stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
@@ -388,6 +402,11 @@ impl Shape {
|
||||
circle_shape.radius *= transform.scaling;
|
||||
circle_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::Ellipse(ellipse_shape) => {
|
||||
ellipse_shape.center = transform * ellipse_shape.center;
|
||||
ellipse_shape.radius *= transform.scaling;
|
||||
ellipse_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::LineSegment { points, stroke } => {
|
||||
for p in points {
|
||||
*p = transform * *p;
|
||||
@@ -497,6 +516,61 @@ impl From<CircleShape> for Shape {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to paint an ellipse.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct EllipseShape {
|
||||
pub center: Pos2,
|
||||
|
||||
/// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b
|
||||
pub radius: Vec2,
|
||||
pub fill: Color32,
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl EllipseShape {
|
||||
#[inline]
|
||||
pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
center,
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into<Stroke>) -> Self {
|
||||
Self {
|
||||
center,
|
||||
radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
Rect::from_center_size(
|
||||
self.center,
|
||||
self.radius * 2.0 + Vec2::splat(self.stroke.width),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EllipseShape> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: EllipseShape) -> Self {
|
||||
Self::Ellipse(shape)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A path which can be stroked and/or filled (if closed).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
|
||||
@@ -20,6 +20,12 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
|
||||
fill,
|
||||
stroke,
|
||||
})
|
||||
| Shape::Ellipse(EllipseShape {
|
||||
center: _,
|
||||
radius: _,
|
||||
fill,
|
||||
stroke,
|
||||
})
|
||||
| Shape::Path(PathShape {
|
||||
points: _,
|
||||
closed: _,
|
||||
|
||||
@@ -201,6 +201,7 @@ impl PaintStats {
|
||||
}
|
||||
Shape::Noop
|
||||
| Shape::Circle { .. }
|
||||
| Shape::Ellipse { .. }
|
||||
| Shape::LineSegment { .. }
|
||||
| Shape::Rect { .. }
|
||||
| Shape::CubicBezier(_)
|
||||
|
||||
@@ -1215,6 +1215,9 @@ impl Tessellator {
|
||||
Shape::Circle(circle) => {
|
||||
self.tessellate_circle(circle, out);
|
||||
}
|
||||
Shape::Ellipse(ellipse) => {
|
||||
self.tessellate_ellipse(ellipse, out);
|
||||
}
|
||||
Shape::Mesh(mesh) => {
|
||||
crate::profile_scope!("mesh");
|
||||
|
||||
@@ -1315,6 +1318,73 @@ impl Tessellator {
|
||||
.stroke_closed(self.feathering, stroke, out);
|
||||
}
|
||||
|
||||
/// Tessellate a single [`EllipseShape`] into a [`Mesh`].
|
||||
///
|
||||
/// * `shape`: the ellipse to tessellate.
|
||||
/// * `out`: triangles are appended to this.
|
||||
pub fn tessellate_ellipse(&mut self, shape: EllipseShape, out: &mut Mesh) {
|
||||
let EllipseShape {
|
||||
center,
|
||||
radius,
|
||||
fill,
|
||||
stroke,
|
||||
} = shape;
|
||||
|
||||
if radius.x <= 0.0 || radius.y <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.options.coarse_tessellation_culling
|
||||
&& !self
|
||||
.clip_rect
|
||||
.expand2(radius + Vec2::splat(stroke.width))
|
||||
.contains(center)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the max pixel radius
|
||||
let max_radius = (radius.max_elem() * self.pixels_per_point) as u32;
|
||||
|
||||
// Ensure there is at least 8 points in each quarter of the ellipse
|
||||
let num_points = u32::max(8, max_radius / 16);
|
||||
|
||||
// Create an ease ratio based the ellipses a and b
|
||||
let ratio = ((radius.y / radius.x) / 2.0).clamp(0.0, 1.0);
|
||||
|
||||
// Generate points between the 0 to pi/2
|
||||
let quarter: Vec<Vec2> = (1..num_points)
|
||||
.map(|i| {
|
||||
let percent = i as f32 / num_points as f32;
|
||||
|
||||
// Ease the percent value, concentrating points around tight bends
|
||||
let eased = 2.0 * (percent - percent.powf(2.0)) * ratio + percent.powf(2.0);
|
||||
|
||||
// Scale the ease to the quarter
|
||||
let t = eased * std::f32::consts::FRAC_PI_2;
|
||||
Vec2::new(radius.x * f32::cos(t), radius.y * f32::sin(t))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build the ellipse from the 4 known vertices filling arcs between
|
||||
// them by mirroring the points between 0 and pi/2
|
||||
let mut points = Vec::new();
|
||||
points.push(center + Vec2::new(radius.x, 0.0));
|
||||
points.extend(quarter.iter().map(|p| center + *p));
|
||||
points.push(center + Vec2::new(0.0, radius.y));
|
||||
points.extend(quarter.iter().rev().map(|p| center + Vec2::new(-p.x, p.y)));
|
||||
points.push(center + Vec2::new(-radius.x, 0.0));
|
||||
points.extend(quarter.iter().map(|p| center - *p));
|
||||
points.push(center + Vec2::new(0.0, -radius.y));
|
||||
points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y)));
|
||||
|
||||
self.scratchpad_path.clear();
|
||||
self.scratchpad_path.add_line_loop(&points);
|
||||
self.scratchpad_path.fill(self.feathering, fill, out);
|
||||
self.scratchpad_path
|
||||
.stroke_closed(self.feathering, stroke, out);
|
||||
}
|
||||
|
||||
/// Tessellate a single [`Mesh`] into a [`Mesh`].
|
||||
///
|
||||
/// * `mesh`: the mesh to tessellate.
|
||||
@@ -1776,7 +1846,7 @@ impl Tessellator {
|
||||
|
||||
Shape::Path(path_shape) => 32 < path_shape.points.len(),
|
||||
|
||||
Shape::QuadraticBezier(_) | Shape::CubicBezier(_) => true,
|
||||
Shape::QuadraticBezier(_) | Shape::CubicBezier(_) | Shape::Ellipse(_) => true,
|
||||
|
||||
Shape::Noop
|
||||
| Shape::Text(_)
|
||||
|
||||
@@ -174,7 +174,7 @@ impl Keypad {
|
||||
pub fn show(&self, ctx: &egui::Context) {
|
||||
let (focus, mut state) = ctx.memory(|m| {
|
||||
(
|
||||
m.focus(),
|
||||
m.focused(),
|
||||
m.data.get_temp::<State>(self.id).unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user