squash before rebase

This commit is contained in:
Matt Campbell 2022-11-29 10:24:06 -06:00
parent 0336816faf
commit 6483b45c6d
27 changed files with 870 additions and 71 deletions

View file

@ -23,6 +23,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)).
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
### Changed 🔧
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).

202
Cargo.lock generated
View file

@ -18,6 +18,68 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e"
[[package]]
name = "accesskit"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f58dda4ca012077ac19d2062ccc30187fa5588f377961be11674c3ca5f8df1"
dependencies = [
"enumset",
"kurbo",
"serde",
]
[[package]]
name = "accesskit_consumer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9f45191913a5a83cee9e9ec161fe20216d345d0eaa321ec599f69188fa5b0d"
dependencies = [
"accesskit",
"parking_lot",
]
[[package]]
name = "accesskit_macos"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe873b1f135b6538ef3c044424703dc6f381e9232ef84a3f080f4c3731d791b"
dependencies = [
"accesskit",
"accesskit_consumer",
"objc2",
"once_cell",
"parking_lot",
]
[[package]]
name = "accesskit_windows"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87a936fb4082e46649748c3c3089bfe5e41f59e1246503ab8e18e1ad81683559"
dependencies = [
"accesskit",
"accesskit_consumer",
"arrayvec 0.7.2",
"lazy-init",
"parking_lot",
"paste",
"windows 0.42.0",
]
[[package]]
name = "accesskit_winit"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f036fe9dbb998ddca93fcefb8343988bb799eeb534144b5164f2cdb1056068eb"
dependencies = [
"accesskit",
"accesskit_macos",
"accesskit_windows",
"parking_lot",
"winit",
]
[[package]]
name = "addr2line"
version = "0.17.0"
@ -369,6 +431,25 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-sys"
version = "0.1.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146"
dependencies = [
"objc-sys",
]
[[package]]
name = "block2"
version = "0.2.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42"
dependencies = [
"block-sys",
"objc2-encode",
]
[[package]]
name = "bumpalo"
version = "3.11.0"
@ -923,8 +1004,18 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.13.4",
"darling_macro 0.13.4",
]
[[package]]
name = "darling"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
dependencies = [
"darling_core 0.14.2",
"darling_macro 0.14.2",
]
[[package]]
@ -941,13 +1032,37 @@ dependencies = [
"syn",
]
[[package]]
name = "darling_core"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"darling_core 0.13.4",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
dependencies = [
"darling_core 0.14.2",
"quote",
"syn",
]
@ -1165,6 +1280,7 @@ dependencies = [
name = "egui"
version = "0.19.0"
dependencies = [
"accesskit",
"ahash 0.8.1",
"document-features",
"epaint",
@ -1193,6 +1309,7 @@ dependencies = [
name = "egui-winit"
version = "0.19.0"
dependencies = [
"accesskit_winit",
"arboard",
"document-features",
"egui",
@ -1360,6 +1477,28 @@ dependencies = [
"syn",
]
[[package]]
name = "enumset"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19be8061a06ab6f3a6cf21106c873578bf01bd42ad15e0311a9c76161cb1c753"
dependencies = [
"enumset_derive",
"serde",
]
[[package]]
name = "enumset_derive"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e7b551eba279bf0fa88b83a46330168c1560a52a94f5126f892f0b364ab3e0"
dependencies = [
"darling 0.14.2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "epaint"
version = "0.19.0"
@ -2094,8 +2233,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
dependencies = [
"arrayvec 0.7.2",
"serde",
]
[[package]]
name = "lazy-init"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f40963626ac12dcaf92afc15e4c3db624858c92fd9f8ba2125eaada3ac2706f"
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -2340,7 +2486,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
dependencies = [
"darling",
"darling 0.13.4",
"proc-macro-crate",
"proc-macro2",
"quote",
@ -2501,6 +2647,32 @@ dependencies = [
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.2.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7"
[[package]]
name = "objc2"
version = "0.3.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe31e5425d3d0b89a15982c024392815da40689aceb34bad364d58732bcfd649"
dependencies = [
"block2",
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "2.0.0-pre.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512"
dependencies = [
"objc-sys",
]
[[package]]
name = "objc_exception"
version = "0.1.2"
@ -2625,6 +2797,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "paste"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -4365,6 +4543,7 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0286ba339aa753e70765d521bb0242cc48e1194562bfa2a2ad7ac8a6de28f5d5"
dependencies = [
"windows-implement",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
@ -4374,6 +4553,17 @@ dependencies = [
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows-implement"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9539b6bd3eadbd9de66c9666b22d802b833da7e996bc06896142e09854a61767"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-sys"
version = "0.36.1"
@ -4491,9 +4681,9 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winit"
version = "0.27.2"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a8f3e9d742401efcfe833b8f84960397482ff049cb7bf59a112e14a4be97f7"
checksum = "bb796d6fbd86b2fd896c9471e6f04d39d750076ebe5680a3958f00f5ab97657c"
dependencies = [
"bitflags",
"cocoa",

View file

@ -18,6 +18,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
## 0.19.0 - 2022-08-20

View file

@ -20,7 +20,10 @@ all-features = true
[features]
default = ["default_fonts", "glow"]
default = ["accesskit", "default_fonts", "glow"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
##

View file

@ -3,6 +3,10 @@ use winit::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
use winit::platform::macos::WindowBuilderExtMacOS as _;
#[cfg(feature = "accesskit")]
use egui::accesskit;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
use crate::{epi, Theme, WindowInfo};
@ -262,6 +266,15 @@ impl EpiIntegration {
}
}
#[cfg(feature = "accesskit")]
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
) {
self.egui_winit.init_accesskit(window, event_loop_proxy);
}
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
@ -301,6 +314,11 @@ impl EpiIntegration {
self.egui_winit.on_event(&self.egui_ctx, event)
}
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_winit.on_accesskit_action_request(request);
}
pub fn update(
&mut self,
app: &mut dyn epi::App,

View file

@ -4,6 +4,8 @@
use std::time::Duration;
use std::time::Instant;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::winit;
use winit::event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
@ -15,6 +17,15 @@ use crate::epi;
#[derive(Debug)]
pub enum UserEvent {
RequestRepaint,
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
}
#[cfg(feature = "accesskit")]
impl From<accesskit_winit::ActionRequestEvent> for UserEvent {
fn from(inner: accesskit_winit::ActionRequestEvent) -> Self {
Self::AccessKitActionRequest(inner)
}
}
// ----------------------------------------------------------------------------
@ -350,7 +361,9 @@ mod glow_integration {
let window_builder = epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
.with_visible(false); // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
let gl_window = unsafe {
glutin::ContextBuilder::new()
@ -397,6 +410,10 @@ mod glow_integration {
#[cfg(feature = "wgpu")]
None,
);
#[cfg(feature = "accesskit")]
{
integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone());
}
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
@ -646,6 +663,21 @@ mod glow_integration {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, .. },
)) => {
if let Some(running) = &mut self.running {
running
.integration
.on_accesskit_action_request(request.clone());
// As a form of user input, accessibility actions should
// lead to a repaint.
EventResult::RepaintNext
} else {
EventResult::Wait
}
}
_ => EventResult::Wait,
}
}
@ -738,7 +770,9 @@ mod wgpu_integration {
let window_settings = epi_integration::load_window_settings(storage);
epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
.with_visible(false) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false)
.build(event_loop)
.unwrap()
}
@ -794,6 +828,10 @@ mod wgpu_integration {
None,
wgpu_render_state.clone(),
);
#[cfg(feature = "accesskit")]
{
integration.init_accesskit(&window, self.repaint_proxy.lock().unwrap().clone());
}
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
@ -1037,6 +1075,21 @@ mod wgpu_integration {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, .. },
)) => {
if let Some(running) = &mut self.running {
running
.integration
.on_accesskit_action_request(request.clone());
// As a form of user input, accessibility actions should
// lead to a repaint.
EventResult::RepaintNext
} else {
EventResult::Wait
}
}
_ => EventResult::Wait,
}
}

View file

@ -400,6 +400,8 @@ impl AppRunner {
events: _, // already handled
mutable_text_under_cursor,
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update: _, // not currently implemented
} = platform_output;
set_cursor_icon(cursor_icon);

View file

@ -5,6 +5,7 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971))
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971))
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).

View file

@ -20,6 +20,9 @@ all-features = true
[features]
default = ["clipboard", "links", "wayland", "winit/default"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"]
@ -57,6 +60,9 @@ winit = { version = "0.27.2", default-features = false }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
# feature accesskit
accesskit_winit = { version = "0.6.0", optional = true }
puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }

View file

@ -11,7 +11,11 @@
use std::os::raw::c_void;
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
#[cfg(feature = "accesskit")]
use egui::accesskit;
pub use winit;
pub mod clipboard;
@ -86,6 +90,9 @@ pub struct State {
/// track ime state
input_method_editor_started: bool,
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
}
impl State {
@ -114,9 +121,25 @@ impl State {
pointer_touch_id: None,
input_method_editor_started: false,
#[cfg(feature = "accesskit")]
accesskit: None,
}
}
#[cfg(feature = "accesskit")]
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
) {
self.accesskit = Some(accesskit_winit::Adapter::new(
window,
Box::new(egui::accesskit_placeholder_tree_update),
event_loop_proxy,
));
}
/// Call this once a graphics context has been created to update the maximum texture dimensions
/// that egui will use.
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
@ -374,6 +397,16 @@ impl State {
}
}
/// Call this when there is a new [`accesskit::ActionRequest`].
///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_input
.events
.push(egui::Event::AccessKitActionRequest(request));
}
fn on_mouse_button_input(
&mut self,
state: winit::event::ElementState,
@ -592,6 +625,8 @@ impl State {
events: _, // handled above
mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = platform_output;
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
@ -608,6 +643,13 @@ impl State {
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
}
#[cfg(feature = "accesskit")]
{
if let Some(accesskit) = self.accesskit.as_ref() {
accesskit.update(accesskit_update);
}
}
}
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {

View file

@ -52,11 +52,12 @@ mint = ["epaint/mint"]
persistence = ["serde", "epaint/serde", "ron"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde"]
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
[dependencies]
epaint = { version = "0.19.0", path = "../epaint", default-features = false }
accesskit = { version = "0.8.0", optional = true }
ahash = { version = "0.8.1", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",

View file

@ -105,6 +105,25 @@ impl ContextImpl {
interactable: true,
},
);
#[cfg(feature = "accesskit")]
{
let nodes = &mut self.output.accesskit_update.nodes;
assert!(nodes.is_empty());
let id = crate::accesskit_root_id();
let accesskit_id = id.accesskit_id();
let node = accesskit::Node {
role: accesskit::Role::Window,
transform: Some(
accesskit::kurbo::Affine::scale(self.input.pixels_per_point().into()).into(),
),
..Default::default()
};
nodes.push((accesskit_id, Arc::new(node)));
self.frame_state.accesskit_nodes.insert(id, nodes.len() - 1);
assert!(self.output.accesskit_update.tree.is_none());
self.output.accesskit_update.tree = Some(accesskit::Tree::new(accesskit_id));
}
}
/// Load fonts unless already loaded.
@ -132,6 +151,24 @@ impl ContextImpl {
}
}
}
#[cfg(feature = "accesskit")]
fn accesskit_node(&mut self, id: Id, parent_id: Option<Id>) -> &mut accesskit::Node {
let nodes = &mut self.output.accesskit_update.nodes;
let node_map = &mut self.frame_state.accesskit_nodes;
let index = node_map.get(&id).copied().unwrap_or_else(|| {
let accesskit_id = id.accesskit_id();
nodes.push((accesskit_id, Arc::new(Default::default())));
let index = nodes.len() - 1;
node_map.insert(id, index);
let parent_id = parent_id.unwrap_or_else(crate::accesskit_root_id);
let parent_index = node_map.get(&parent_id).unwrap();
let parent = Arc::get_mut(&mut nodes[*parent_index].1).unwrap();
parent.children.push(accesskit_id);
index
});
Arc::get_mut(&mut nodes[index].1).unwrap()
}
}
// ----------------------------------------------------------------------------
@ -436,16 +473,23 @@ impl Context {
self.check_for_id_clash(id, rect, "widget");
#[cfg(feature = "accesskit")]
{
if sense.focusable {
// Make sure anything that can receive focus has an AccessKit node.
// TODO(mwcampbell): For nodes that are filled from widget info,
// some information is written to the node twice.
let mut node = self.accesskit_node(id, None);
response.fill_accesskit_node_common(&mut node);
}
}
let clicked_elsewhere = response.clicked_elsewhere();
let ctx_impl = &mut *self.write();
let memory = &mut ctx_impl.memory;
let input = &mut ctx_impl.input;
// We only want to focus labels if the screen reader is on.
let interested_in_focus =
sense.interactive() || sense.focusable && memory.options.screen_reader;
if interested_in_focus {
if sense.focusable {
memory.interested_in_focus(id);
}
@ -457,6 +501,15 @@ impl Context {
response.clicked[PointerButton::Primary as usize] = true;
}
#[cfg(feature = "accesskit")]
{
if sense.click
&& input.has_accesskit_action_request(response.id, accesskit::Action::Default)
{
response.clicked[PointerButton::Primary as usize] = true;
}
}
if sense.click || sense.drag {
memory.interaction.click_interest |= hovered && sense.click;
memory.interaction.drag_interest |= hovered && sense.drag;
@ -983,7 +1036,20 @@ impl Context {
textures_delta = ctx_impl.tex_manager.0.write().take_delta();
};
let platform_output: PlatformOutput = std::mem::take(&mut self.output());
#[cfg_attr(not(feature = "accesskit"), allow(unused_mut))]
let mut platform_output: PlatformOutput = std::mem::take(&mut self.output());
#[cfg(feature = "accesskit")]
{
let has_focus = self.input().raw.has_focus;
platform_output.accesskit_update.focus = has_focus.then(|| {
let focus_id = self.memory().interaction.focus.id;
focus_id.map_or_else(
|| crate::accesskit_root_id().accesskit_id(),
|id| id.accesskit_id(),
)
});
}
// if repaint_requests is greater than zero. just set the duration to zero for immediate
// repaint. if there's no repaint requests, then we can use the actual repaint_after instead.
@ -1502,6 +1568,18 @@ impl Context {
}
}
/// ## Accessibility
impl Context {
#[cfg(feature = "accesskit")]
pub fn accesskit_node(
&self,
id: Id,
parent_id: Option<Id>,
) -> RwLockWriteGuard<'_, accesskit::Node> {
RwLockWriteGuard::map(self.write(), |c| c.accesskit_node(id, parent_id))
}
}
#[test]
fn context_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}

View file

@ -268,6 +268,10 @@ pub enum Event {
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
force: f32,
},
/// An assistive technology (e.g. screen reader) requested an action.
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit::ActionRequest),
}
/// Mouse button (or similar for touch input)

View file

@ -85,6 +85,9 @@ pub struct PlatformOutput {
/// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>,
#[cfg(feature = "accesskit")]
pub accesskit_update: accesskit::TreeUpdate,
}
impl PlatformOutput {
@ -121,6 +124,8 @@ impl PlatformOutput {
mut events,
mutable_text_under_cursor,
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = newer;
self.cursor_icon = cursor_icon;
@ -133,6 +138,13 @@ impl PlatformOutput {
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
#[cfg(feature = "accesskit")]
{
// egui produces a complete AccessKit tree for each frame,
// so overwrite rather than appending.
self.accesskit_update = accesskit_update;
}
}
/// Take everything ephemeral (everything except `cursor_icon` currently)
@ -372,6 +384,19 @@ pub enum OutputEvent {
ValueChanged(WidgetInfo),
}
impl OutputEvent {
pub fn widget_info(&self) -> &WidgetInfo {
match self {
OutputEvent::Clicked(info)
| OutputEvent::DoubleClicked(info)
| OutputEvent::TripleClicked(info)
| OutputEvent::FocusGained(info)
| OutputEvent::TextSelectionChanged(info)
| OutputEvent::ValueChanged(info) => info,
}
}
}
impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -41,6 +41,9 @@ pub(crate) struct FrameState {
/// horizontal, vertical
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
#[cfg(feature = "accesskit")]
pub(crate) accesskit_nodes: IdMap<usize>,
}
impl Default for FrameState {
@ -53,6 +56,8 @@ impl Default for FrameState {
tooltip_state: None,
scroll_delta: Vec2::ZERO,
scroll_target: [None, None],
#[cfg(feature = "accesskit")]
accesskit_nodes: Default::default(),
}
}
}
@ -67,6 +72,8 @@ impl FrameState {
tooltip_state,
scroll_delta,
scroll_target,
#[cfg(feature = "accesskit")]
accesskit_nodes,
} = self;
used_ids.clear();
@ -76,6 +83,10 @@ impl FrameState {
*tooltip_state = None;
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];
#[cfg(feature = "accesskit")]
{
accesskit_nodes.clear();
}
}
/// How much space is still available after panels has been added.

View file

@ -69,6 +69,11 @@ impl Id {
pub(crate) fn value(&self) -> u64 {
self.0
}
#[cfg(feature = "accesskit")]
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
std::num::NonZeroU64::new(self.0).unwrap().into()
}
}
impl std::fmt::Debug for Id {

View file

@ -393,6 +393,50 @@ impl InputState {
}
}
}
#[cfg(feature = "accesskit")]
pub fn accesskit_action_request(
&self,
id: crate::Id,
action: accesskit::Action,
) -> Option<&accesskit::ActionRequest> {
let accesskit_id = id.accesskit_id();
for event in &self.events {
if let Event::AccessKitActionRequest(request) = event {
if request.target == accesskit_id && request.action == action {
return Some(request);
}
}
}
None
}
#[cfg(feature = "accesskit")]
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
self.accesskit_action_request(id, action).is_some()
}
#[cfg(feature = "accesskit")]
pub fn num_accesskit_action_requests(
&self,
id: crate::Id,
desired_action: accesskit::Action,
) -> usize {
let accesskit_id = id.accesskit_id();
self.events
.iter()
.filter(|event| {
matches!(
event,
Event::AccessKitActionRequest(accesskit::ActionRequest {
target,
action,
..
}) if *target == accesskit_id && *action == desired_action
)
})
.count()
}
}
// ----------------------------------------------------------------------------

View file

@ -324,6 +324,9 @@ pub mod util;
pub mod widget_text;
pub mod widgets;
#[cfg(feature = "accesskit")]
pub use accesskit;
pub use epaint;
pub use epaint::emath;
@ -549,3 +552,27 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
});
});
}
#[cfg(feature = "accesskit")]
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}
#[cfg(feature = "accesskit")]
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
use accesskit::{Node, Role, Tree, TreeUpdate};
use std::sync::Arc;
let root_id = accesskit_root_id().accesskit_id();
TreeUpdate {
nodes: vec![(
root_id,
Arc::new(Node {
role: Role::Window,
..Default::default()
}),
)],
tree: Some(Tree::new(root_id)),
focus: None,
}
}

View file

@ -166,7 +166,7 @@ pub(crate) struct Interaction {
#[derive(Clone, Debug, Default)]
pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field).
id: Option<Id>,
pub(crate) id: Option<Id>,
/// What had keyboard focus previous frame?
id_previous_frame: Option<Id>,
@ -174,6 +174,9 @@ pub(crate) struct Focus {
/// Give focus to this widget next frame
id_next_frame: Option<Id>,
#[cfg(feature = "accesskit")]
id_requested_by_accesskit: Option<accesskit::NodeId>,
/// If set, the next widget that is interested in focus will automatically get it.
/// Probably because the user pressed Tab.
give_to_next: bool,
@ -231,6 +234,11 @@ impl Focus {
self.id = Some(id);
}
#[cfg(feature = "accesskit")]
{
self.id_requested_by_accesskit = None;
}
self.pressed_tab = false;
self.pressed_shift_tab = false;
for event in &new_input.events {
@ -261,6 +269,18 @@ impl Focus {
}
}
}
#[cfg(feature = "accesskit")]
{
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus,
target,
data: None,
}) = event
{
self.id_requested_by_accesskit = Some(*target);
}
}
}
}
@ -281,6 +301,17 @@ impl Focus {
}
fn interested_in_focus(&mut self, id: Id) {
#[cfg(feature = "accesskit")]
{
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
self.id = Some(id);
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.pressed_tab = false;
self.pressed_shift_tab = false;
}
}
if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id);
self.give_to_next = false;

View file

@ -526,8 +526,91 @@ impl Response {
None
};
if let Some(event) = event {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
{
self.fill_accesskit_node_from_widget_info(make_info());
}
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
self.fill_accesskit_node_from_widget_info(event.widget_info().clone());
self.ctx.output().events.push(event);
}
#[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
node.bounds = Some(accesskit::kurbo::Rect {
x0: self.rect.min.x.into(),
y0: self.rect.min.y.into(),
x1: self.rect.max.x.into(),
y1: self.rect.max.y.into(),
});
if self.sense.focusable {
node.focusable = true;
}
if self.sense.click {
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
}
}
#[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info(&self, info: crate::WidgetInfo) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
let mut node = self.ctx.accesskit_node(self.id, None);
self.fill_accesskit_node_common(&mut node);
node.role = match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField,
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
Role::Button
}
WidgetType::Checkbox => Role::CheckBox,
WidgetType::RadioButton => Role::RadioButton,
WidgetType::SelectableLabel => Role::ToggleButton,
WidgetType::ComboBox => Role::PopupButton,
WidgetType::Slider => Role::Slider,
WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown,
};
if let Some(label) = info.label {
node.name = Some(label.into());
}
if let Some(value) = info.current_text_value {
node.value = Some(value.into());
}
if let Some(value) = info.value {
node.numeric_value = Some(value);
}
if let Some(selected) = info.selected {
node.checked_state = Some(if selected {
CheckedState::True
} else {
CheckedState::False
});
}
}
/// Associate a label with a control for accessibility.
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
{
let mut node = self.ctx.accesskit_node(self.id, None);
node.labelled_by.push(id.accesskit_id());
}
#[cfg(not(feature = "accesskit"))]
{
let _ = id;
}
self
}
/// Response to secondary clicks (right-clicks) by showing the given menu.

View file

@ -425,6 +425,7 @@ impl<'a> Widget for DragValue<'a> {
}
};
#[allow(clippy::redundant_clone)] // some clones below are dundant if AccessKit is disabled
let mut response = if is_kb_editing {
let button_width = ui.spacing().interact_size.x;
let mut value_text = ui
@ -444,7 +445,7 @@ impl<'a> Widget for DragValue<'a> {
None => value_text.parse().ok(),
};
if let Some(parsed_value) = parsed_value {
let parsed_value = clamp_to_range(parsed_value, clamp_range);
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
set(&mut get_set_value, parsed_value);
}
ui.memory().drag_value.edit_string = Some(value_text);
@ -499,7 +500,7 @@ impl<'a> Widget for DragValue<'a> {
);
let rounded_new_value =
emath::round_to_decimals(rounded_new_value, auto_decimals);
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range);
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range.clone());
set(&mut get_set_value, rounded_new_value);
drag_state.last_dragged_id = Some(response.id);
@ -514,6 +515,24 @@ impl<'a> Widget for DragValue<'a> {
response.changed = get(&mut get_set_value) != old_value;
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
{
let mut node = ui.ctx().accesskit_node(response.id, None);
// If either end of the range is unbounded, it's better
// to leave the corresponding AccessKit field set to None,
// to allow for platform-specific default behavior.
if clamp_range.start().is_finite() {
node.min_numeric_value = Some(*clamp_range.start());
}
if clamp_range.end().is_finite() {
node.max_numeric_value = Some(*clamp_range.end());
}
// The name field is set to the current value by the button,
// but we don't want it set that way on this widget type.
node.name = None;
}
response
}
}

View file

@ -16,7 +16,7 @@ use crate::{widget_text::WidgetTextGalley, *};
pub struct Label {
text: WidgetText,
wrap: Option<bool>,
sense: Sense,
sense: Option<Sense>,
}
impl Label {
@ -24,7 +24,7 @@ impl Label {
Self {
text: text.into(),
wrap: None,
sense: Sense::focusable_noninteractive(),
sense: None,
}
}
@ -60,7 +60,7 @@ impl Label {
/// # });
/// ```
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self.sense = Some(sense);
self
}
}
@ -68,9 +68,17 @@ impl Label {
impl Label {
/// Do layout and position the galley in the ui, without painting it or adding widget info.
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let sense = self.sense.unwrap_or_else(|| {
// We only want to focus labels if the screen reader is on.
if ui.memory().options.screen_reader {
Sense::focusable_noninteractive()
} else {
Sense::hover()
}
});
if let WidgetText::Galley(galley) = self.text {
// If the user said "use this specific galley", then just use it:
let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
let pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),
@ -121,10 +129,10 @@ impl Label {
let rect = text_galley.galley.rows[0]
.rect
.translate(vec2(pos.x, pos.y));
let mut response = ui.allocate_rect(rect, self.sense);
let mut response = ui.allocate_rect(rect, sense);
for row in text_galley.galley.rows.iter().skip(1) {
let rect = row.rect.translate(vec2(pos.x, pos.y));
response |= ui.allocate_rect(rect, self.sense);
response |= ui.allocate_rect(rect, sense);
}
(pos, text_galley, response)
} else {
@ -144,7 +152,7 @@ impl Label {
};
let text_galley = text_job.into_galley(&ui.fonts());
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.sense);
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),

View file

@ -510,7 +510,7 @@ impl<'a> Slider<'a> {
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
};
ui.allocate_response(desired_size, Sense::click_and_drag())
ui.allocate_response(desired_size, Sense::drag())
}
/// Just the slider, no text
@ -532,6 +532,9 @@ impl<'a> Slider<'a> {
self.set_value(new_value);
}
let mut decrement = 0usize;
let mut increment = 0usize;
if response.has_focus() {
let (dec_key, inc_key) = match self.orientation {
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
@ -540,8 +543,21 @@ impl<'a> Slider<'a> {
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
};
let decrement = ui.input().num_presses(dec_key);
let increment = ui.input().num_presses(inc_key);
decrement += ui.input().num_presses(dec_key);
increment += ui.input().num_presses(inc_key);
}
#[cfg(feature = "accesskit")]
{
use accesskit::Action;
decrement += ui
.input()
.num_accesskit_action_requests(response.id, Action::Decrement);
increment += ui
.input()
.num_accesskit_action_requests(response.id, Action::Increment);
}
let kb_step = increment as f32 - decrement as f32;
if kb_step != 0.0 {
@ -553,21 +569,14 @@ impl<'a> Slider<'a> {
None if self.smart_aim => {
let aim_radius = ui.input().aim_radius();
emath::smart_aim::best_in_range_f64(
self.value_from_position(
new_position - aim_radius,
position_range.clone(),
),
self.value_from_position(
new_position + aim_radius,
position_range.clone(),
),
self.value_from_position(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()),
)
}
_ => self.value_from_position(new_position, position_range.clone()),
};
self.set_value(new_value);
}
}
// Paint it:
if ui.is_rect_visible(response.rect) {
@ -705,12 +714,33 @@ impl<'a> Slider<'a> {
}
fn add_contents(&mut self, ui: &mut Ui) -> Response {
let old_value = self.get_value();
let thickness = ui
.text_style_height(&TextStyle::Body)
.at_least(ui.spacing().interact_size.y);
let mut response = self.allocate_slider_space(ui, thickness);
self.slider_ui(ui, &response);
let value = self.get_value();
response.changed = value != old_value;
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")]
{
use accesskit::Action;
let mut node = ui.ctx().accesskit_node(response.id, None);
node.min_numeric_value = Some(*self.range.start());
node.max_numeric_value = Some(*self.range.end());
let clamp_range = self.clamp_range();
if value < *clamp_range.end() {
node.actions |= Action::Increment;
}
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
}
}
if self.show_value {
let position_range = self.position_range(&response.rect);
let value_response = self.value_ui(ui, position_range);
@ -737,18 +767,12 @@ impl<'a> Slider<'a> {
impl<'a> Widget for Slider<'a> {
fn ui(mut self, ui: &mut Ui) -> Response {
let old_value = self.get_value();
let inner_response = match self.orientation {
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
};
let mut response = inner_response.inner | inner_response.response;
let value = self.get_value();
response.changed = value != old_value;
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
response
inner_response.inner | inner_response.response
}
}

View file

@ -648,11 +648,7 @@ impl<'t> TextEdit<'t> {
char_range,
mask_if_password(password, text.as_str()),
);
response
.ctx
.output()
.events
.push(OutputEvent::TextSelectionChanged(info));
response.output_event(OutputEvent::TextSelectionChanged(info));
} else {
response.widget_info(|| {
WidgetInfo::text_edit(
@ -662,6 +658,84 @@ impl<'t> TextEdit<'t> {
});
}
#[cfg(feature = "accesskit")]
{
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
let parent_id = response.id;
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
let mut node = ui.ctx().accesskit_node(id, Some(parent_id));
node.role = Role::InlineTextBox;
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
node.text_direction = Some(TextDirection::LeftToRight);
// TODO: more info for the whole row
let glyph_count = row.glyphs.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::new();
character_lengths.reserve(glyph_count);
let mut character_positions = Vec::<f32>::new();
character_positions.reserve(glyph_count);
let mut character_widths = Vec::<f32>::new();
character_widths.reserve(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.rect.min.x);
character_widths.push(glyph.size.x);
}
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.rect.max.x - row.rect.min.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
node.value = Some(value.into());
node.character_lengths = character_lengths.into();
node.character_positions = Some(character_positions.into());
node.character_widths = Some(character_widths.into());
node.word_lengths = word_lengths.into();
}
if let Some(cursor_range) = &cursor_range {
let mut node = ui.ctx().accesskit_node(parent_id, None);
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
node.text_selection = Some(TextSelection {
anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
},
focus: TextPosition {
node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column,
},
});
}
}
TextEditOutput {
response,
galley,
@ -689,6 +763,28 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
// ----------------------------------------------------------------------------
#[cfg(feature = "accesskit")]
fn ccursor_from_accesskit_text_position(
id: Id,
galley: &Galley,
position: &accesskit::TextPosition,
) -> Option<CCursor> {
let mut total_length = 0usize;
for (i, row) in galley.rows.iter().enumerate() {
let row_id = id.with(i);
if row_id.accesskit_id() == position.node {
return Some(CCursor {
index: total_length + position.character_index,
prefer_next_row: !(position.character_index == row.glyphs.len()
&& !row.ends_with_newline
&& (i + 1) < galley.rows.len()),
});
}
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
}
None
}
/// Check for (keyboard) events to edit the cursor and/or text.
#[allow(clippy::too_many_arguments)]
fn events(
@ -849,6 +945,27 @@ fn events(
}
}
#[cfg(feature = "accesskit")]
Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::SetTextSelection,
target,
data: Some(accesskit::ActionData::SetTextSelection(selection)),
}) => {
if id.accesskit_id() == *target {
let primary =
ccursor_from_accesskit_text_position(id, galley, &selection.focus);
let secondary =
ccursor_from_accesskit_text_position(id, galley, &selection.anchor);
if let (Some(primary), Some(secondary)) = (primary, secondary) {
Some(CCursorRange { primary, secondary })
} else {
None
}
} else {
None
}
}
_ => None,
};

View file

@ -1,7 +1,7 @@
use epaint::text::cursor::*;
/// A selected text range (could be a range of length zero).
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CursorRange {
/// When selecting with a mouse, this is where the mouse was released.

View file

@ -113,7 +113,7 @@ impl PartialEq for PCursor {
/// They all point to the same place, but in their own different ways.
/// pcursor/rcursor can also point to after the end of the paragraph/row.
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Cursor {
pub ccursor: CCursor,

View file

@ -33,10 +33,15 @@ impl eframe::App for MyApp {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name);
let name_label = ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name)
.labelled_by(name_label.id);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
ui.add(
egui::Slider::new(&mut self.age, 0..=120)
.step_by(1.0)
.text("age"),
);
if ui.button("Click each year").clicked() {
self.age += 1;
}