Skip to content

Commit

Permalink
Improve widget info output for potential screen readers
Browse files Browse the repository at this point in the history
Part of #167
  • Loading branch information
emilk committed Mar 8, 2021
1 parent 1c06622 commit ea248d6
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 83 deletions.
9 changes: 4 additions & 5 deletions egui/src/containers/collapsing_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,7 @@ impl CollapsingHeader {
desired_size = desired_size.at_least(ui.spacing().interact_size);
let (_, rect) = ui.allocate_space(desired_size);

let header_response = ui.interact(rect, id, Sense::click());
if header_response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::CollapsingHeader, &galley.text);
}
let mut header_response = ui.interact(rect, id, Sense::click());
let text_pos = pos2(
text_pos.x,
header_response.rect.center().y - galley.size.y / 2.0,
Expand All @@ -219,7 +215,10 @@ impl CollapsingHeader {
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open);
if header_response.clicked() {
state.toggle(ui);
header_response.mark_changed();
}
header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, &galley.text));

let visuals = ui.style().interact(&header_response);
let text_color = visuals.text_color();
Expand Down
5 changes: 1 addition & 4 deletions egui/src/containers/combo_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ pub fn combo_box_with_label(

ui.horizontal(|ui| {
let mut response = combo_box(ui, button_id, selected, menu_contents);
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::ComboBox, label.text());
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
response |= ui.add(label);
response
})
Expand Down
150 changes: 129 additions & 21 deletions egui/src/data/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ pub struct Output {
pub events: Vec<OutputEvent>,
}

impl Output {
/// Open the given url in a web browser.
/// If egui is running in a browser, the same tab will be reused.
pub fn open_url(&mut self, url: impl Into<String>) {
self.open_url = Some(OpenUrl::new_tab(url))
}
}

#[derive(Clone, PartialEq)]
pub struct OpenUrl {
pub url: String,
Expand Down Expand Up @@ -77,20 +85,129 @@ impl Default for CursorIcon {
/// Things that happened during this frame that the integration may be interested in.
///
/// In particular, these events may be useful for accessability, i.e. for screen readers.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, PartialEq)]
pub enum OutputEvent {
/// A widget gained keyboard focus (by tab key).
///
/// An integration can for instance read the newly selected widget out loud for the visually impaired.
//
// TODO: we should output state too, e.g. if a checkbox is selected, or current slider value.
Focused(WidgetType, String),
WidgetEvent(WidgetEvent, WidgetInfo),
}

impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WidgetEvent(we, wi) => write!(f, "{:?}: {:?}", we, wi),
}
}
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WidgetEvent {
/// Keyboard focused moved onto the widget.
Focus,
// /// Started hovering a new widget.
// Hover, // TODO: cursor hovered events
}

/// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`].
#[derive(Clone, PartialEq)]
pub struct WidgetInfo {
/// The type of widget this is.
pub typ: WidgetType,
/// The text on labels, buttons, checkboxes etc.
pub label: Option<String>,
/// The contents of some editable text (for `TextEdit` fields).
pub edit_text: Option<String>,
/// The current value of checkboxes and radio buttons.
pub selected: Option<bool>,
/// The current value of sliders etc.
pub value: Option<f64>,
}

impl std::fmt::Debug for WidgetInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
typ,
label,
edit_text,
selected,
value,
} = self;

let mut s = f.debug_struct("WidgetInfo");

s.field("typ", typ);

if let Some(label) = label {
s.field("label", label);
}
if let Some(edit_text) = edit_text {
s.field("edit_text", edit_text);
}
if let Some(selected) = selected {
s.field("selected", selected);
}
if let Some(value) = value {
s.field("value", value);
}

s.finish()
}
}

impl WidgetInfo {
pub fn new(typ: WidgetType) -> Self {
Self {
typ,
label: None,
edit_text: None,
selected: None,
value: None,
}
}

pub fn labeled(typ: WidgetType, label: impl Into<String>) -> Self {
Self {
label: Some(label.into()),
..Self::new(typ)
}
}

/// checkboxes, radio-buttons etc
pub fn selected(typ: WidgetType, selected: bool, label: impl Into<String>) -> Self {
Self {
label: Some(label.into()),
selected: Some(selected),
..Self::new(typ)
}
}

pub fn drag_value(value: f64) -> Self {
Self {
value: Some(value),
..Self::new(WidgetType::DragValue)
}
}

pub fn slider(value: f64, label: impl Into<String>) -> Self {
let label = label.into();
Self {
label: if label.is_empty() { None } else { Some(label) },
value: Some(value),
..Self::new(WidgetType::Slider)
}
}

pub fn text_edit(edit_text: impl Into<String>) -> Self {
Self {
edit_text: Some(edit_text.into()),
..Self::new(WidgetType::TextEdit)
}
}
}

/// The different types of built-in widgets in egui
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WidgetType {
Label,
Label, // TODO: emit Label events
Hyperlink,
TextEdit,
Button,
Expand All @@ -103,18 +220,9 @@ pub enum WidgetType {
ColorButton,
ImageButton,
CollapsingHeader,
}

impl Output {
/// Open the given url in a web browser.
/// If egui is running in a browser, the same tab will be reused.
pub fn open_url(&mut self, url: impl Into<String>) {
self.open_url = Some(OpenUrl::new_tab(url))
}

/// Inform the backend integration that a widget gained focus
pub fn push_gained_focus_event(&mut self, widget_type: WidgetType, text: impl Into<String>) {
self.events
.push(OutputEvent::Focused(widget_type, text.into()));
}
/// If you cannot fit any of the above slots.
///
/// If this is something you think should be added, file an issue.
Other,
}
2 changes: 1 addition & 1 deletion egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ pub use {
context::{Context, CtxRef},
data::{
input::*,
output::{self, CursorIcon, Output, WidgetType},
output::{self, CursorIcon, Output, WidgetInfo, WidgetType},
},
grid::Grid,
id::Id,
Expand Down
12 changes: 12 additions & 0 deletions egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ impl Response {
let scroll_target = lerp(self.rect.y_range(), align.to_factor());
self.ctx.frame_state().scroll_target = Some((scroll_target, align));
}

/// For accessibility.
///
/// Call after interacting and potential calls to [`Self::mark_changed`].
pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
if self.gained_kb_focus() {
use crate::output::{OutputEvent, WidgetEvent};
let widget_info = make_info();
let event = OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info);
self.ctx.output().events.push(event);
}
}
}

impl Response {
Expand Down
21 changes: 5 additions & 16 deletions egui/src/widgets/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,7 @@ impl Button {
}

let (rect, response) = ui.allocate_at_least(desired_size, sense);
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::TextEdit, &galley.text);
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, &galley.text));

if ui.clip_rect().intersects(rect) {
let visuals = ui.style().interact(&response);
Expand Down Expand Up @@ -232,15 +229,12 @@ impl<'a> Widget for Checkbox<'a> {
desired_size = desired_size.at_least(spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::Checkbox, &galley.text);
}

if response.clicked() {
*checked = !*checked;
response.mark_changed();
}
response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text));

// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response);
Expand Down Expand Up @@ -346,10 +340,8 @@ impl Widget for RadioButton {
desired_size = desired_size.at_least(ui.spacing().interact_size);
desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::RadioButton, &galley.text);
}
response
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, &galley.text));

let text_cursor = pos2(
rect.min.x + button_padding.x + icon_width + icon_spacing,
Expand Down Expand Up @@ -454,10 +446,7 @@ impl Widget for ImageButton {
let button_padding = ui.spacing().button_padding;
let size = image.size() + 2.0 * button_padding;
let (rect, response) = ui.allocate_exact_size(size, sense);
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::ImageButton, "");
}
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));

if ui.clip_rect().intersects(rect) {
let visuals = ui.style().interact(&response);
Expand Down
5 changes: 1 addition & 4 deletions egui/src/widgets/color_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ fn show_hsva(ui: &mut Ui, color: Hsva, desired_size: Vec2) -> Response {
fn color_button(ui: &mut Ui, color: Color32) -> Response {
let size = ui.spacing().interact_size;
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::ColorButton, "");
}
response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));
let visuals = ui.style().interact(&response);
let rect = rect.expand(visuals.expansion);

Expand Down
6 changes: 1 addition & 5 deletions egui/src/widgets/drag_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,12 @@ impl<'a> Widget for DragValue<'a> {
response
};

if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::DragValue, "");
}

#[allow(clippy::float_cmp)]
{
response.changed = get(&mut get_set_value) != value;
}

response.widget_info(|| WidgetInfo::drag_value(value));
response
}
}
5 changes: 1 addition & 4 deletions egui/src/widgets/hyperlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ impl Widget for Hyperlink {
let Hyperlink { url, label } = self;
let galley = label.layout(ui);
let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click());
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::Hyperlink, &galley.text);
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, &galley.text));

if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
Expand Down
7 changes: 3 additions & 4 deletions egui/src/widgets/selected_label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ impl Widget for SelectableLabel {
let mut desired_size = total_extra + galley.size;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::SelectableLabel, &galley.text);
}
response.widget_info(|| {
WidgetInfo::selected(WidgetType::SelectableLabel, selected, &galley.text)
});

let text_cursor = ui
.layout()
Expand Down
6 changes: 2 additions & 4 deletions egui/src/widgets/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,8 @@ impl<'a> Slider<'a> {
self.set_value(new_value);
}

if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::Slider, &self.text);
}
let value = self.get_value();
response.widget_info(|| WidgetInfo::slider(value, &self.text));

if response.has_kb_focus() {
let kb_step = ui.input().num_presses(Key::ArrowRight) as f32
Expand Down
5 changes: 1 addition & 4 deletions egui/src/widgets/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,6 @@ impl<'t> TextEdit<'t> {
Sense::hover()
};
let mut response = ui.interact(rect, id, sense);
if response.gained_kb_focus() {
ui.output()
.push_gained_focus_event(WidgetType::TextEdit, &*text);
}

if enabled {
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
Expand Down Expand Up @@ -523,6 +519,7 @@ impl<'t> TextEdit<'t> {

ui.memory().text_edit.insert(id, state);

response.widget_info(|| WidgetInfo::text_edit(&*text));
response
}
}
Expand Down
Loading

0 comments on commit ea248d6

Please sign in to comment.