1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-28 07:23:13 -04:00
This commit is contained in:
obellish
2024-03-21 13:18:53 -04:00
46 changed files with 797 additions and 337 deletions

View File

@@ -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)!)

View File

@@ -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`
///

View File

@@ -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()),

View File

@@ -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()),

View File

@@ -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;
}
}

View File

@@ -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&param2=bar"),
("mushroom", "snake")
])
);
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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(())
}
}
// ----------------------------------------------------------------------------

View File

@@ -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 _;

View File

@@ -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,

View File

@@ -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();

View File

@@ -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 =

View File

@@ -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()

View File

@@ -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];

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -194,7 +194,6 @@ impl Widget for &memory::InteractionState {
let memory::InteractionState {
potential_click_id,
potential_drag_id,
focus: _,
} = self;
ui.vertical(|ui| {

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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");

View File

@@ -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())
}

View File

@@ -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 {

View File

@@ -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| {

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
})

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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))]

View File

@@ -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: _,

View File

@@ -201,6 +201,7 @@ impl PaintStats {
}
Shape::Noop
| Shape::Circle { .. }
| Shape::Ellipse { .. }
| Shape::LineSegment { .. }
| Shape::Rect { .. }
| Shape::CubicBezier(_)

View File

@@ -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(_)

View File

@@ -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(),
)
});