Compare commits
16 commits
master
...
ndarilek/m
Author | SHA1 | Date | |
---|---|---|---|
![]() |
77b3a154b4 | ||
![]() |
265190a892 | ||
![]() |
9fe465cc4a | ||
![]() |
4fa1e82129 | ||
![]() |
68499cebf6 | ||
![]() |
875d3a650d | ||
![]() |
ff9059fe68 | ||
![]() |
131525536b | ||
![]() |
fa18727933 | ||
![]() |
73271a6265 | ||
![]() |
2433506c92 | ||
![]() |
97aa56f465 | ||
![]() |
8269cca95d | ||
![]() |
b3ced6106b | ||
![]() |
50c8310de5 | ||
![]() |
0fd88e52da |
4 changed files with 135 additions and 36 deletions
|
@ -24,7 +24,7 @@ pub struct Output {
|
||||||
/// Events that may be useful to e.g. a screen reader.
|
/// Events that may be useful to e.g. a screen reader.
|
||||||
pub events: Vec<OutputEvent>,
|
pub events: Vec<OutputEvent>,
|
||||||
|
|
||||||
/// Position of text widgts' cursor
|
/// Position of text widget's cursor
|
||||||
pub text_cursor: Option<crate::Pos2>,
|
pub text_cursor: Option<crate::Pos2>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,11 @@ impl Output {
|
||||||
// only describe last event:
|
// only describe last event:
|
||||||
if let Some(event) = self.events.iter().rev().next() {
|
if let Some(event) = self.events.iter().rev().next() {
|
||||||
match event {
|
match event {
|
||||||
OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info) => {
|
OutputEvent::Clicked(widget_info)
|
||||||
|
| OutputEvent::DoubleClicked(widget_info)
|
||||||
|
| OutputEvent::FocusGained(widget_info)
|
||||||
|
| OutputEvent::TextSelectionChanged(widget_info)
|
||||||
|
| OutputEvent::ValueChanged(widget_info) => {
|
||||||
return widget_info.description();
|
return widget_info.description();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,60 +208,77 @@ impl Default for CursorIcon {
|
||||||
/// In particular, these events may be useful for accessability, i.e. for screen readers.
|
/// In particular, these events may be useful for accessability, i.e. for screen readers.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum OutputEvent {
|
pub enum OutputEvent {
|
||||||
|
// A widget was clicked.
|
||||||
|
Clicked(WidgetInfo),
|
||||||
|
// A widget was double-clicked.
|
||||||
|
DoubleClicked(WidgetInfo),
|
||||||
/// A widget gained keyboard focus (by tab key).
|
/// A widget gained keyboard focus (by tab key).
|
||||||
WidgetEvent(WidgetEvent, WidgetInfo),
|
FocusGained(WidgetInfo),
|
||||||
|
// Text selection was updated.
|
||||||
|
TextSelectionChanged(WidgetInfo),
|
||||||
|
// A widget's value changed.
|
||||||
|
ValueChanged(WidgetInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for OutputEvent {
|
impl std::fmt::Debug for OutputEvent {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::WidgetEvent(we, wi) => write!(f, "{:?}: {:?}", we, wi),
|
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
|
||||||
|
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
|
||||||
|
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
|
||||||
|
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
|
||||||
|
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", 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`].
|
/// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`].
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct WidgetInfo {
|
pub struct WidgetInfo {
|
||||||
/// The type of widget this is.
|
/// The type of widget this is.
|
||||||
pub typ: WidgetType,
|
pub typ: WidgetType,
|
||||||
|
// Whether the widget is enabled.
|
||||||
|
pub enabled: bool,
|
||||||
/// The text on labels, buttons, checkboxes etc.
|
/// The text on labels, buttons, checkboxes etc.
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
/// The contents of some editable text (for `TextEdit` fields).
|
/// The contents of some editable text (for `TextEdit` fields).
|
||||||
pub edit_text: Option<String>,
|
pub current_text_value: Option<String>,
|
||||||
|
// The previous text value.
|
||||||
|
pub prev_text_value: Option<String>,
|
||||||
/// The current value of checkboxes and radio buttons.
|
/// The current value of checkboxes and radio buttons.
|
||||||
pub selected: Option<bool>,
|
pub selected: Option<bool>,
|
||||||
/// The current value of sliders etc.
|
/// The current value of sliders etc.
|
||||||
pub value: Option<f64>,
|
pub value: Option<f64>,
|
||||||
|
// Selected range of characters in [`Self::current_text_value`].
|
||||||
|
pub text_selection: Option<std::ops::RangeInclusive<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WidgetInfo {
|
impl std::fmt::Debug for WidgetInfo {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let Self {
|
let Self {
|
||||||
typ,
|
typ,
|
||||||
|
enabled,
|
||||||
label,
|
label,
|
||||||
edit_text,
|
current_text_value: text_value,
|
||||||
|
prev_text_value,
|
||||||
selected,
|
selected,
|
||||||
value,
|
value,
|
||||||
|
text_selection,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mut s = f.debug_struct("WidgetInfo");
|
let mut s = f.debug_struct("WidgetInfo");
|
||||||
|
|
||||||
s.field("typ", typ);
|
s.field("typ", typ);
|
||||||
|
s.field("enabled", enabled);
|
||||||
|
|
||||||
if let Some(label) = label {
|
if let Some(label) = label {
|
||||||
s.field("label", label);
|
s.field("label", label);
|
||||||
}
|
}
|
||||||
if let Some(edit_text) = edit_text {
|
if let Some(text_value) = text_value {
|
||||||
s.field("edit_text", edit_text);
|
s.field("text_value", text_value);
|
||||||
|
}
|
||||||
|
if let Some(prev_text_value) = prev_text_value {
|
||||||
|
s.field("prev_text_value", prev_text_value);
|
||||||
}
|
}
|
||||||
if let Some(selected) = selected {
|
if let Some(selected) = selected {
|
||||||
s.field("selected", selected);
|
s.field("selected", selected);
|
||||||
|
@ -265,6 +286,9 @@ impl std::fmt::Debug for WidgetInfo {
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
s.field("value", value);
|
s.field("value", value);
|
||||||
}
|
}
|
||||||
|
if let Some(text_selection) = text_selection {
|
||||||
|
s.field("text_selection", text_selection);
|
||||||
|
}
|
||||||
|
|
||||||
s.finish()
|
s.finish()
|
||||||
}
|
}
|
||||||
|
@ -274,10 +298,13 @@ impl WidgetInfo {
|
||||||
pub fn new(typ: WidgetType) -> Self {
|
pub fn new(typ: WidgetType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
typ,
|
typ,
|
||||||
|
enabled: true,
|
||||||
label: None,
|
label: None,
|
||||||
edit_text: None,
|
current_text_value: None,
|
||||||
|
prev_text_value: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
value: None,
|
value: None,
|
||||||
|
text_selection: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,9 +344,29 @@ impl WidgetInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub fn text_edit(edit_text: impl ToString) -> Self {
|
pub fn text_edit(prev_text_value: impl ToString, text_value: impl ToString) -> Self {
|
||||||
|
let text_value = text_value.to_string();
|
||||||
|
let prev_text_value = prev_text_value.to_string();
|
||||||
|
let prev_text_value = if text_value == prev_text_value {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(prev_text_value)
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
edit_text: Some(edit_text.to_string()),
|
current_text_value: Some(text_value.to_string()),
|
||||||
|
prev_text_value,
|
||||||
|
..Self::new(WidgetType::TextEdit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn text_selection_changed(
|
||||||
|
text_selection: std::ops::RangeInclusive<usize>,
|
||||||
|
current_text_value: impl ToString,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
text_selection: Some(text_selection),
|
||||||
|
current_text_value: Some(current_text_value.to_string()),
|
||||||
..Self::new(WidgetType::TextEdit)
|
..Self::new(WidgetType::TextEdit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,14 +375,17 @@ impl WidgetInfo {
|
||||||
pub fn description(&self) -> String {
|
pub fn description(&self) -> String {
|
||||||
let Self {
|
let Self {
|
||||||
typ,
|
typ,
|
||||||
|
enabled,
|
||||||
label,
|
label,
|
||||||
edit_text,
|
current_text_value: text_value,
|
||||||
|
prev_text_value: _,
|
||||||
selected,
|
selected,
|
||||||
value,
|
value,
|
||||||
|
text_selection: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// TODO: localization
|
// TODO: localization
|
||||||
let widget_name = match typ {
|
let widget_type = match typ {
|
||||||
WidgetType::Hyperlink => "link",
|
WidgetType::Hyperlink => "link",
|
||||||
WidgetType::TextEdit => "text edit",
|
WidgetType::TextEdit => "text edit",
|
||||||
WidgetType::Button => "button",
|
WidgetType::Button => "button",
|
||||||
|
@ -351,25 +401,33 @@ impl WidgetInfo {
|
||||||
WidgetType::Label | WidgetType::Other => "",
|
WidgetType::Label | WidgetType::Other => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut description = widget_name.to_owned();
|
let mut description = widget_type.to_owned();
|
||||||
|
|
||||||
if let Some(selected) = selected {
|
if let Some(selected) = selected {
|
||||||
if *typ == WidgetType::Checkbox {
|
if *typ == WidgetType::Checkbox {
|
||||||
description += " ";
|
let state = if *selected { "checked" } else { "unchecked" };
|
||||||
description += if *selected { "checked" } else { "unchecked" };
|
description = format!("{} {}", state, description);
|
||||||
} else {
|
} else {
|
||||||
description += if *selected { "selected" } else { "" };
|
description += if *selected { "selected" } else { "" };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(label) = label {
|
if let Some(label) = label {
|
||||||
description += " ";
|
description = format!("{}: {}", label, description);
|
||||||
description += label;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(edit_text) = edit_text {
|
if typ == &WidgetType::TextEdit {
|
||||||
description += " ";
|
let text;
|
||||||
description += edit_text;
|
if let Some(text_value) = text_value {
|
||||||
|
if text_value.is_empty() {
|
||||||
|
text = "blank".into();
|
||||||
|
} else {
|
||||||
|
text = text_value.to_string();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "blank".into();
|
||||||
|
}
|
||||||
|
description = format!("{}: {}", text, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
|
@ -377,6 +435,9 @@ impl WidgetInfo {
|
||||||
description += &value.to_string();
|
description += &value.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
description += ": disabled";
|
||||||
|
}
|
||||||
description.trim().to_owned()
|
description.trim().to_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -314,6 +314,11 @@ impl Memory {
|
||||||
self.interaction.focus.id == Some(id)
|
self.interaction.focus.id == Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which widget has keyboard focus?
|
||||||
|
pub fn focus(&self) -> Option<Id> {
|
||||||
|
self.interaction.focus.id
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn lock_focus(&mut self, id: Id, lock_focus: bool) {
|
pub(crate) fn lock_focus(&mut self, id: Id, lock_focus: bool) {
|
||||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||||
self.interaction.focus.is_focus_locked = lock_focus;
|
self.interaction.focus.is_focus_locked = lock_focus;
|
||||||
|
|
|
@ -428,10 +428,19 @@ impl Response {
|
||||||
///
|
///
|
||||||
/// Call after interacting and potential calls to [`Self::mark_changed`].
|
/// Call after interacting and potential calls to [`Self::mark_changed`].
|
||||||
pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
|
pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
|
||||||
if self.gained_focus() {
|
use crate::output::OutputEvent;
|
||||||
use crate::output::{OutputEvent, WidgetEvent};
|
let event = if self.clicked() {
|
||||||
let widget_info = make_info();
|
Some(OutputEvent::Clicked(make_info()))
|
||||||
let event = OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info);
|
} else if self.double_clicked() {
|
||||||
|
Some(OutputEvent::DoubleClicked(make_info()))
|
||||||
|
} else if self.gained_focus() {
|
||||||
|
Some(OutputEvent::FocusGained(make_info()))
|
||||||
|
} else if self.changed {
|
||||||
|
Some(OutputEvent::ValueChanged(make_info()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(event) = event {
|
||||||
self.ctx.output().events.push(event);
|
self.ctx.output().events.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{util::undoer::Undoer, *};
|
use crate::{output::OutputEvent, util::undoer::Undoer, *};
|
||||||
use epaint::{text::cursor::*, *};
|
use epaint::{text::cursor::*, *};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
@ -346,6 +346,7 @@ impl<'t> TextEdit<'t> {
|
||||||
lock_focus,
|
lock_focus,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
let prev_text = text.clone();
|
||||||
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
||||||
let line_spacing = ui.fonts().row_height(text_style);
|
let line_spacing = ui.fonts().row_height(text_style);
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
|
@ -448,6 +449,7 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text_cursor = None;
|
let mut text_cursor = None;
|
||||||
|
let prev_text_cursor = state.cursorp;
|
||||||
if ui.memory().has_focus(id) && enabled {
|
if ui.memory().has_focus(id) && enabled {
|
||||||
ui.memory().lock_focus(id, lock_focus);
|
ui.memory().lock_focus(id, lock_focus);
|
||||||
|
|
||||||
|
@ -500,7 +502,6 @@ impl<'t> TextEdit<'t> {
|
||||||
&& text_to_insert != "\r"
|
&& text_to_insert != "\r"
|
||||||
{
|
{
|
||||||
let mut ccursor = delete_selected(text, &cursorp);
|
let mut ccursor = delete_selected(text, &cursorp);
|
||||||
|
|
||||||
insert_text(&mut ccursor, text, text_to_insert);
|
insert_text(&mut ccursor, text, text_to_insert);
|
||||||
Some(CCursorPair::one(ccursor))
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
|
@ -656,7 +657,30 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
ui.memory().id_data.insert(id, state);
|
ui.memory().id_data.insert(id, state);
|
||||||
|
|
||||||
response.widget_info(|| WidgetInfo::text_edit(&*text));
|
let selection_changed = if let (Some(text_cursor), Some(prev_text_cursor)) =
|
||||||
|
(text_cursor, prev_text_cursor)
|
||||||
|
{
|
||||||
|
text_cursor.primary.ccursor.index != prev_text_cursor.primary.ccursor.index
|
||||||
|
|| text_cursor.secondary.ccursor.index != prev_text_cursor.secondary.ccursor.index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.changed {
|
||||||
|
response.widget_info(|| WidgetInfo::text_edit(&*prev_text, &*text));
|
||||||
|
} else if selection_changed {
|
||||||
|
let text_cursor = text_cursor.unwrap();
|
||||||
|
let char_range =
|
||||||
|
text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index;
|
||||||
|
let info = WidgetInfo::text_selection_changed(char_range, &*text);
|
||||||
|
response
|
||||||
|
.ctx
|
||||||
|
.output()
|
||||||
|
.events
|
||||||
|
.push(OutputEvent::TextSelectionChanged(info));
|
||||||
|
} else {
|
||||||
|
response.widget_info(|| WidgetInfo::text_edit(&*prev_text, &*text));
|
||||||
|
}
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue