A text field keeps the cursor where an edit left it unless it asks to be pinned to the end, which only the time picker does, and a hook for a window that loses the keyboard
A field built with `select_on_focus( true )` kept its cursor at the end of the value after every insertion and deletion. That was meant for the time picker, whose `on_change` normalises what was typed, "2" becoming "02", so that a cursor placed at the end of the typed text would land in the middle of the normalised one. But the flag was also the only way to have a field select its whole value on focus, so every field that wanted that got the pinning too, and in an address bar it is a defect: tapping inside the address to correct a word, the cursor was placed where the finger was, and the first character typed sent it to the end, so the rest of the edit went to the wrong place. The two behaviours are now separate. `select_on_focus` only selects the value when the field takes the focus. The new `TextEdit::pin_cursor_to_end( bool )`, `false` by default, keeps the cursor at the end of the displayed value through the `usize::MAX` sentinel, as before, for fields whose `on_change` rewrites the value. It reaches the runtime in a new `pin_cursor_to_end` field of `WidgetHandlers::TextEdit`, set where the snapshot is built in `Element` and copied in its `Clone`, and the four places in `insert_delete.rs` that anchored the cursor, for an insertion, a delete forward, the deletion of a selection and the move after a replacement, read it instead of `select_on_focus`. The time picker's value fields ask for it, so they behave as they did. A field that uses `select_on_focus` only to select on focus now edits with an ordinary cursor. The one other user known is a dialog in `crate`, which gets the ordinary behaviour; if its `on_change` normalises the text it has to ask for `pin_cursor_to_end( true )` as the time picker does. Documentation of the previous commit and of what building crate showed. `docs/widgets.md` says that a `pressable` loses the tap and the right click to an interactive widget inside it even when that widget has no handler, as a `list_item` without `on_press`, so its content should be built from pieces that take no input; and that the hint of `Button::tooltip` is an xdg-popup beside the button in a window, placed by the compositor and taking no grab, and an overlay spanning the output on a layer surface. `docs/theming.md` gains "Application icons": a desktop entry can point at an installed theme icon, the Figma glass effects resvg does not reproduce, a `backdrop-filter` in a `<foreignObject>` ignored and `plus-lighter` rewritten to `screen` by `decode_svg_bytes`, and the advice to flatten them; the table row of `decode_svg_bytes` mentions the rewrite and that its pixels come back premultiplied. `docs/cookbook.md` says that requesting the focus for the text field that already has it puts the cursor at the end of its value without a second `on_focus`, which is how an application moves the cursor after replacing the value, and that Tab and the vertical arrows reach `App::on_key_with_modifiers` before the field, while Escape reaches it after ltk has closed any transient UI and taken the focus off the field. Losing the keyboard. An application that repeats keys on its own timer, as prompt does for its terminal, never heard that the window lost the keyboard, so a key held while the focus went elsewhere kept repeating until the next key, since its release went to the other window. The new `App::on_keyboard_leave()`, a no-op by default, is called from the keyboard's `leave`, right after ltk stops its own repeat, for the application to stop its repeat and forget the modifiers it kept. Documentation of the previous commit's hooks and widgets. `docs/architecture.md` lists `on_mouse_button` with `MouseButtonEvent` and `MouseButton`, `on_modifiers` and `on_keyboard_leave` among the input hooks. `docs/widgets.md` describes `ListItem::ink( label, secondary )`, which draws a row's label, subtitle, trailing text and hover and press tints in colours of its own for a surface the theme does not know about, and says that `icon_button` keeps a bitmap's proportions, fitting one that is not square into its size and centring it rather than stretching it. Not done. There is no test for the new flag, as `select_on_focus` has one in `text_edit/tests.rs` for its builder and its propagation to the handler, and the documentation of the widget does not mention it.
This commit is contained in:
@@ -99,6 +99,9 @@ In practice, that model is easiest to adopt in three steps:
|
|||||||
- `on_tap` — taps that miss every widget.
|
- `on_tap` — taps that miss every widget.
|
||||||
- `on_key` / `on_key_with_modifiers` — global hotkeys. Characters, Backspace and Delete stay in a focused text input and reach the app otherwise, a focused button or pressable included; Return and Space go to the focused widget's submit or press message when it has one; arrows are offered to the app first and fall back to the text cursor or list navigation.
|
- `on_key` / `on_key_with_modifiers` — global hotkeys. Characters, Backspace and Delete stay in a focused text input and reach the app otherwise, a focused button or pressable included; Return and Space go to the focused widget's submit or press message when it has one; arrows are offered to the app first and fall back to the text cursor or list navigation.
|
||||||
- `swipe_threshold`, `swipe_down_threshold` — gesture sensitivity.
|
- `swipe_threshold`, `swipe_down_threshold` — gesture sensitivity.
|
||||||
|
- `on_mouse_button( &MouseButtonEvent )` — every press and release of the left, middle and right buttons (`MouseButton`), with the position in physical pixels and Ctrl, Shift and Alt, before ltk handles it: for an app that hands the mouse on, as a terminal does to a program that asks for it.
|
||||||
|
- `on_modifiers( ctrl, shift, alt )` — the modifiers held changed, for an app that needs them with the wheel or a pointer motion, which do not carry them.
|
||||||
|
- `on_keyboard_leave()` — the window lost the keyboard, so no release will come for the keys still held: an app that repeats keys on its own timer stops there, and forgets the modifiers it kept.
|
||||||
|
|
||||||
**Implement when the app has more than one place to be**
|
**Implement when the app has more than one place to be**
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -726,8 +726,10 @@ be added to the view by the same `update` that requests it, and may sit
|
|||||||
inside a `scroll` below the fold: the runtime waits a few frames for it
|
inside a `scroll` below the fold: the runtime waits a few frames for it
|
||||||
and moves the viewport so the widget is visible when it takes focus.
|
and moves the viewport so the widget is visible when it takes focus.
|
||||||
|
|
||||||
|
Requesting focus for the text field that already has it puts its cursor at the end of the value, without firing its `on_focus` again. That is how an application moves the cursor after replacing the value itself, for instance after completing a path on Tab: set the new value in `update` and return the field's id from `take_focus_request`. A field built with `select_on_focus( true )` selects its whole value instead.
|
||||||
|
|
||||||
Tab and Shift+Tab traverse focusable widgets in declaration order — no
|
Tab and Shift+Tab traverse focusable widgets in declaration order — no
|
||||||
opt-in needed beyond the widget being interactive.
|
opt-in needed beyond the widget being interactive. While a text field has the focus, Tab and the vertical arrows still reach `App::on_key_with_modifiers` first, so an application can claim them, for completion say: return a message for the key and ltk neither moves the focus nor the cursor. Escape reaches it too, after ltk has closed any transient UI and taken the focus off the field.
|
||||||
|
|
||||||
**See also**: [`WidgetId`](../src/types.rs),
|
**See also**: [`WidgetId`](../src/types.rs),
|
||||||
[`tests/tab_navigation.rs`](../tests/tab_navigation.rs).
|
[`tests/tab_navigation.rs`](../tests/tab_navigation.rs).
|
||||||
|
|||||||
+12
-1
@@ -518,7 +518,7 @@ fn view( &self ) -> Element<Msg>
|
|||||||
| `theme_icon_path( "category/name" )` | `Option<PathBuf>` | Catalogue icon path (filled-then-line lookup). |
|
| `theme_icon_path( "category/name" )` | `Option<PathBuf>` | Catalogue icon path (filled-then-line lookup). |
|
||||||
| `theme_icon_rgba( "category/name", size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterised + cached RGBA. Pair with `theme::tint_symbolic` for chrome glyphs. |
|
| `theme_icon_rgba( "category/name", size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterised + cached RGBA. Pair with `theme::tint_symbolic` for chrome glyphs. |
|
||||||
| `theme_icon_tinted( name, size, tint )` | `Option<ImageData>` | One-call rasterise + tint (`theme_icon_rgba` + `tint_symbolic`). |
|
| `theme_icon_tinted( name, size, tint )` | `Option<ImageData>` | One-call rasterise + tint (`theme_icon_rgba` + `tint_symbolic`). |
|
||||||
| `decode_svg_bytes( bytes, size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterise arbitrary SVG bytes (uncached, no path resolution); the primitive under `theme_icon_rgba`. |
|
| `decode_svg_bytes( bytes, size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterise arbitrary SVG bytes (uncached, no path resolution); the primitive under `theme_icon_rgba`. Rewrites the CSS-only `plus-lighter` blend to `screen` first (see [Application icons](#application-icons)); the pixels come back premultiplied. |
|
||||||
| `active_document()` | `Arc<ThemeDocument>` | The whole loaded document, for slot-typed lookups the per-slot helpers do not cover (e.g. iterating slots). |
|
| `active_document()` | `Arc<ThemeDocument>` | The whole loaded document, for slot-typed lookups the per-slot helpers do not cover (e.g. iterating slots). |
|
||||||
| `active_theme_id()` | `String` | Id of the active theme. |
|
| `active_theme_id()` | `String` | Id of the active theme. |
|
||||||
| `is_fallback_active()` | `bool` | `true` when the embedded B/W fallback theme is in force (no theme on disk). Useful to disable a theme-picker UI or warn the user. |
|
| `is_fallback_active()` | `bool` | `true` when the embedded B/W fallback theme is in force (no theme on disk). Useful to disable a theme-picker UI or warn the user. |
|
||||||
@@ -728,6 +728,17 @@ Categories under `catalogue/{filled,line}/`: `accessibility`, `actions`,
|
|||||||
`features`, `feedback`, `general`, `hardware`, `keyboard`, `multimedia`,
|
`features`, `feedback`, `general`, `hardware`, `keyboard`, `multimedia`,
|
||||||
`navigation`, `safety`, `session`, `system`, `window`.
|
`navigation`, `safety`, `session`, `system`, `window`.
|
||||||
|
|
||||||
|
### Application icons
|
||||||
|
|
||||||
|
An application's desktop entry can point at a theme icon by its installed path, `Icon=/usr/share/ltk/themes/default/icons/apps/files.svg`, which `ltk-theme-default` provides, instead of shipping an icon of its own.
|
||||||
|
|
||||||
|
The shipped app icons are exported from Figma with glass effects, and resvg, which rasterises every SVG in ltk, reproduces only part of them:
|
||||||
|
|
||||||
|
- A `<foreignObject>` carrying `backdrop-filter: blur(…)` is HTML, not SVG, and is ignored: the frosted panel comes out as a flat translucent one.
|
||||||
|
- `feBlend mode="plus-lighter"` is a CSS blend mode resvg does not know, and it would fall back to `normal`, painting the highlights as grey edges that cover whole pixels at dock size. `decode_svg_bytes` rewrites it to `screen`, the closest mode resvg supports, before parsing, so every path that rasterises an SVG through ltk gets it.
|
||||||
|
|
||||||
|
An icon meant to look the same everywhere is best drawn without these effects, or with them flattened into its geometry.
|
||||||
|
|
||||||
### Adding an icon
|
### Adding an icon
|
||||||
|
|
||||||
1. Drop a monochrome SVG into
|
1. Drop a monochrome SVG into
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ icon_button( rgba_bytes, w, h ).on_press( Msg::OpenSearch )
|
|||||||
count; pass `Length::widget( n )` instead to follow the widget-scaling
|
count; pass `Length::widget( n )` instead to follow the widget-scaling
|
||||||
mode, which is what stock icons (the [`list_item`](#list_item) disclosure
|
mode, which is what stock icons (the [`list_item`](#list_item) disclosure
|
||||||
arrow) already do — use it when the two sit side by side and should match.
|
arrow) already do — use it when the two sit side by side and should match.
|
||||||
|
The bitmap keeps its proportions: one that is not square is scaled to fit that size and centred in it, never stretched.
|
||||||
|
|
||||||
Give it a `width( … )` or `height( … )` and it becomes a framed key: it
|
Give it a `width( … )` or `height( … )` and it becomes a framed key: it
|
||||||
claims that size, paints the background and border of its `variant` like
|
claims that size, paints the background and border of its `variant` like
|
||||||
@@ -132,6 +133,8 @@ pressable(
|
|||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A tap and a long press go to the deepest interactive widget under them, and a widget is interactive even without a handler: a `list_item` with no `on_press` inside a `pressable` takes the tap and the right click and does nothing with them, so the pressable never fires. Build the content from pieces that take no input (`container`, `row`, `column`, `text`, `img_widget`); keep an inner `button` only when it should win over the pressable, as a row's own trailing action does.
|
||||||
|
|
||||||
**See also**: [`button`](#button) when a plain text button is enough,
|
**See also**: [`button`](#button) when a plain text button is enough,
|
||||||
[`list_item`](#list_item) for the standard "label + subtitle + trailing"
|
[`list_item`](#list_item) for the standard "label + subtitle + trailing"
|
||||||
pattern.
|
pattern.
|
||||||
@@ -198,6 +201,7 @@ When both are set the text sits to the left of the icon.
|
|||||||
`pad_h( px )` overrides the horizontal inset between the row edge and
|
`pad_h( px )` overrides the horizontal inset between the row edge and
|
||||||
its content (theme default 16 px) — lower it when the enclosing view's
|
its content (theme default 16 px) — lower it when the enclosing view's
|
||||||
own padding already provides the margin.
|
own padding already provides the margin.
|
||||||
|
`ink( label, secondary )` draws the label in `label`, the subtitle and trailing text in `secondary`, and tints hover and press with `label`, instead of the theme's colours, for a row laid on a surface the theme does not know about, such as a menu drawn in an app's own colours.
|
||||||
|
|
||||||
`height( len )` overrides the theme row height (56 design px, 68 with a subtitle) and `font_size( len )` the primary-label size (16 design px; subtitle and trailing keep their theme sizes), both `impl Into<Length>`. Use them together where a dense list — a context menu, a compact picker — should trade the touch-target generosity of a settings list for row density; the resolved height is floored at the label's rendered height so the text never clips however aggressive the override.
|
`height( len )` overrides the theme row height (56 design px, 68 with a subtitle) and `font_size( len )` the primary-label size (16 design px; subtitle and trailing keep their theme sizes), both `impl Into<Length>`. Use them together where a dense list — a context menu, a compact picker — should trade the touch-target generosity of a settings list for row density; the resolved height is floored at the label's rendered height so the text never clips however aggressive the override.
|
||||||
|
|
||||||
@@ -797,6 +801,8 @@ The overlay anchors below the widget built with the matching
|
|||||||
`.id( WidgetId )`. See [`docs/cookbook.md`](./cookbook.md) for the full
|
`.id( WidgetId )`. See [`docs/cookbook.md`](./cookbook.md) for the full
|
||||||
overlay wiring.
|
overlay wiring.
|
||||||
|
|
||||||
|
The hint `Button::tooltip` shows by itself is placed by the runtime. When the main surface is an xdg-shell window it is an xdg-popup child of the window, anchored to the button's rect: the compositor puts it above the button, 6 logical pixels away, flips it below or slides it sideways when it does not fit, and it takes no grab and no input, so the pointer stays with the button. A window does not know where it sits on screen, so this is the only way the hint lands beside the button. On a layer-shell surface (a shell, a panel) the hint is an overlay spanning the output, placed above the button or below it near the top edge.
|
||||||
|
|
||||||
**See also**: [`toast`](#toast) for transient self-dismissing
|
**See also**: [`toast`](#toast) for transient self-dismissing
|
||||||
notifications.
|
notifications.
|
||||||
|
|
||||||
|
|||||||
@@ -1048,6 +1048,9 @@ pub trait App: 'static
|
|||||||
/// The modifiers held changed, for an app that needs them with the wheel or a motion, which do not carry them. Default: no-op.
|
/// The modifiers held changed, for an app that needs them with the wheel or a motion, which do not carry them. Default: no-op.
|
||||||
fn on_modifiers( &mut self, _ctrl: bool, _shift: bool, _alt: bool ) {}
|
fn on_modifiers( &mut self, _ctrl: bool, _shift: bool, _alt: bool ) {}
|
||||||
|
|
||||||
|
/// The window lost the keyboard, so no release will come for the keys held: for an app that repeats keys itself. Default: no-op.
|
||||||
|
fn on_keyboard_leave( &mut self ) {}
|
||||||
|
|
||||||
/// Called on every keyboard event (press *and* release) before the
|
/// Called on every keyboard event (press *and* release) before the
|
||||||
/// regular focus-aware dispatch ([`Self::on_key`] /
|
/// regular focus-aware dispatch ([`Self::on_key`] /
|
||||||
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl<A: App> AppData<A>
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// `select_on_focus` fields keep the cursor pinned to the end
|
// `pin_cursor_to_end` fields keep the cursor pinned to the end
|
||||||
// of the *displayed* value. The pending value the user just
|
// of the *displayed* value. The pending value the user just
|
||||||
// typed (e.g. "2") may render shorter than the post-update
|
// typed (e.g. "2") may render shorter than the post-update
|
||||||
// display value (e.g. "02" after a `format!("{:02}")`
|
// display value (e.g. "02" after a `format!("{:02}")`
|
||||||
@@ -40,9 +40,9 @@ impl<A: App> AppData<A>
|
|||||||
// whatever value we eventually render". Without this, typing
|
// whatever value we eventually render". Without this, typing
|
||||||
// a second digit would insert into the middle of the
|
// a second digit would insert into the middle of the
|
||||||
// normalised string ("0|2" + "3" → "032" → 32 instead of 23).
|
// normalised string ("0|2" + "3" → "032" → 32 instead of 23).
|
||||||
let select_on_focus = matches!(
|
let pin_to_end = matches!(
|
||||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
||||||
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
|
Some( WidgetHandlers::TextEdit { pin_cursor_to_end: true, .. } ),
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_value;
|
let new_value;
|
||||||
@@ -52,7 +52,7 @@ impl<A: App> AppData<A>
|
|||||||
let safe_cursor = (*cursor).min( current_value.len() );
|
let safe_cursor = (*cursor).min( current_value.len() );
|
||||||
let mut v = current_value.clone();
|
let mut v = current_value.clone();
|
||||||
v.insert_str( safe_cursor, text );
|
v.insert_str( safe_cursor, text );
|
||||||
let next_cursor = if select_on_focus { usize::MAX } else { safe_cursor + text.len() };
|
let next_cursor = if pin_to_end { usize::MAX } else { safe_cursor + text.len() };
|
||||||
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
|
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
|
||||||
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
|
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
|
||||||
ss.pending_text_values.insert( idx, v.clone() );
|
ss.pending_text_values.insert( idx, v.clone() );
|
||||||
@@ -104,14 +104,14 @@ impl<A: App> AppData<A>
|
|||||||
let step = match next_char { Some( c ) => c.len_utf8(), None => return };
|
let step = match next_char { Some( c ) => c.len_utf8(), None => return };
|
||||||
let mut new_value = current_value.clone();
|
let mut new_value = current_value.clone();
|
||||||
new_value.replace_range( safe_cursor_pre..safe_cursor_pre + step, "" );
|
new_value.replace_range( safe_cursor_pre..safe_cursor_pre + step, "" );
|
||||||
// `select_on_focus` fields keep the cursor at the end of the
|
// `pin_cursor_to_end` fields keep the cursor at the end of the
|
||||||
// post-update displayed value via the `usize::MAX` sentinel —
|
// post-update displayed value via the `usize::MAX` sentinel —
|
||||||
// see `handle_text_insert` for the reasoning.
|
// see `handle_text_insert` for the reasoning.
|
||||||
let select_on_focus = matches!(
|
let pin_to_end = matches!(
|
||||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
||||||
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
|
Some( WidgetHandlers::TextEdit { pin_cursor_to_end: true, .. } ),
|
||||||
);
|
);
|
||||||
let next_cursor = if select_on_focus { usize::MAX } else { safe_cursor_pre };
|
let next_cursor = if pin_to_end { usize::MAX } else { safe_cursor_pre };
|
||||||
{
|
{
|
||||||
let ss = self.surface_mut( focus );
|
let ss = self.surface_mut( focus );
|
||||||
// Cursor stays put — the char to its right is gone, so the
|
// Cursor stays put — the char to its right is gone, so the
|
||||||
@@ -189,11 +189,11 @@ impl<A: App> AppData<A>
|
|||||||
let mut new_value = current_value.clone();
|
let mut new_value = current_value.clone();
|
||||||
new_value.replace_range( start_byte..end_byte, "" );
|
new_value.replace_range( start_byte..end_byte, "" );
|
||||||
|
|
||||||
let select_on_focus = matches!(
|
let pin_to_end = matches!(
|
||||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
||||||
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
|
Some( WidgetHandlers::TextEdit { pin_cursor_to_end: true, .. } ),
|
||||||
);
|
);
|
||||||
let next_cursor = if select_on_focus { usize::MAX } else { start_byte };
|
let next_cursor = if pin_to_end { usize::MAX } else { start_byte };
|
||||||
|
|
||||||
{
|
{
|
||||||
let ss = self.surface_mut( focus );
|
let ss = self.surface_mut( focus );
|
||||||
@@ -249,14 +249,14 @@ impl<A: App> AppData<A>
|
|||||||
let new_cursor = safe_cursor - removed_char.len_utf8();
|
let new_cursor = safe_cursor - removed_char.len_utf8();
|
||||||
let mut new_value = current_value.clone();
|
let mut new_value = current_value.clone();
|
||||||
new_value.remove( new_cursor );
|
new_value.remove( new_cursor );
|
||||||
// `select_on_focus` fields keep the cursor at the end of the
|
// `pin_cursor_to_end` fields keep the cursor at the end of the
|
||||||
// post-update displayed value via the `usize::MAX` sentinel —
|
// post-update displayed value via the `usize::MAX` sentinel —
|
||||||
// see `handle_text_insert` for the reasoning.
|
// see `handle_text_insert` for the reasoning.
|
||||||
let select_on_focus = matches!(
|
let pin_to_end = matches!(
|
||||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
|
||||||
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
|
Some( WidgetHandlers::TextEdit { pin_cursor_to_end: true, .. } ),
|
||||||
);
|
);
|
||||||
let next_cursor = if select_on_focus { usize::MAX } else { new_cursor };
|
let next_cursor = if pin_to_end { usize::MAX } else { new_cursor };
|
||||||
{
|
{
|
||||||
let ss = self.surface_mut( focus );
|
let ss = self.surface_mut( focus );
|
||||||
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
|
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ impl<A: App> KeyboardHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
self.stop_key_repeat();
|
self.stop_key_repeat();
|
||||||
self.keyboard_focus = SurfaceFocus::Main;
|
self.keyboard_focus = SurfaceFocus::Main;
|
||||||
|
self.app.on_keyboard_leave();
|
||||||
if let Some( ref mut a ) = self.a11y { a.set_window_focus( false ); }
|
if let Some( ref mut a ) = self.a11y { a.set_window_focus( false ); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ impl<Msg: Clone> Element<Msg>
|
|||||||
font_size: t.font_size,
|
font_size: t.font_size,
|
||||||
pad_h: t.pad_h,
|
pad_h: t.pad_h,
|
||||||
select_on_focus: t.select_on_focus,
|
select_on_focus: t.select_on_focus,
|
||||||
|
pin_cursor_to_end: t.pin_cursor_to_end,
|
||||||
password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ),
|
password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ pub enum WidgetHandlers<Msg: Clone>
|
|||||||
/// whether the new selection should anchor at `0` (replace
|
/// whether the new selection should anchor at `0` (replace
|
||||||
/// on next keystroke) or at the cursor (insert).
|
/// on next keystroke) or at the cursor (insert).
|
||||||
select_on_focus: bool,
|
select_on_focus: bool,
|
||||||
|
/// `true` when edits keep the cursor at the end of the displayed value.
|
||||||
|
pin_cursor_to_end: bool,
|
||||||
/// Snapshot of the
|
/// Snapshot of the
|
||||||
/// [`crate::widget::text_edit::TextEdit::password_toggle`]
|
/// [`crate::widget::text_edit::TextEdit::password_toggle`]
|
||||||
/// callback. When `Some`, pointer / touch dispatch checks
|
/// callback. When `Some`, pointer / touch dispatch checks
|
||||||
@@ -144,7 +146,7 @@ impl<Msg: Clone> Clone for WidgetHandlers<Msg>
|
|||||||
WidgetHandlers::Radio { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected },
|
WidgetHandlers::Radio { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected },
|
||||||
WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() },
|
WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() },
|
||||||
WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() },
|
WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() },
|
||||||
WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, pad_h, select_on_focus, password_toggle_msg } =>
|
WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, pad_h, select_on_focus, pin_cursor_to_end, password_toggle_msg } =>
|
||||||
{
|
{
|
||||||
WidgetHandlers::TextEdit
|
WidgetHandlers::TextEdit
|
||||||
{
|
{
|
||||||
@@ -159,6 +161,7 @@ impl<Msg: Clone> Clone for WidgetHandlers<Msg>
|
|||||||
font_size: *font_size,
|
font_size: *font_size,
|
||||||
pad_h: *pad_h,
|
pad_h: *pad_h,
|
||||||
select_on_focus: *select_on_focus,
|
select_on_focus: *select_on_focus,
|
||||||
|
pin_cursor_to_end: *pin_cursor_to_end,
|
||||||
password_toggle_msg: password_toggle_msg.clone(),
|
password_toggle_msg: password_toggle_msg.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ pub struct TextEdit<Msg: Clone>
|
|||||||
/// retype rather than edit. Default `false` — long-form fields
|
/// retype rather than edit. Default `false` — long-form fields
|
||||||
/// keep the cursor at the end on focus.
|
/// keep the cursor at the end on focus.
|
||||||
pub( crate ) select_on_focus: bool,
|
pub( crate ) select_on_focus: bool,
|
||||||
|
/// When `true`, editing keeps the cursor at the end of the displayed
|
||||||
|
/// value, whatever the app rewrites it to in `on_change`. Default
|
||||||
|
/// `false` — the cursor stays where the edit left it.
|
||||||
|
pub( crate ) pin_cursor_to_end: bool,
|
||||||
/// Self-managed "show / hide password" eye affordance — when
|
/// Self-managed "show / hide password" eye affordance — when
|
||||||
/// `Some( ( visible, on_toggle ) )` the field renders an
|
/// `Some( ( visible, on_toggle ) )` the field renders an
|
||||||
/// `actions/visible` ↔ `actions/invisible` icon at its right
|
/// `actions/visible` ↔ `actions/invisible` icon at its right
|
||||||
@@ -220,6 +224,7 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
font_size: None,
|
font_size: None,
|
||||||
height: None,
|
height: None,
|
||||||
select_on_focus: false,
|
select_on_focus: false,
|
||||||
|
pin_cursor_to_end: false,
|
||||||
password_toggle: None,
|
password_toggle: None,
|
||||||
read_only: false,
|
read_only: false,
|
||||||
}
|
}
|
||||||
@@ -296,6 +301,15 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep the cursor at the end of the displayed value while editing,
|
||||||
|
/// for fields whose `on_change` normalises what was typed ("2" → "02").
|
||||||
|
/// Default `false`.
|
||||||
|
pub fn pin_cursor_to_end( mut self, on: bool ) -> Self
|
||||||
|
{
|
||||||
|
self.pin_cursor_to_end = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the horizontal alignment of the displayed text. Default
|
/// Set the horizontal alignment of the displayed text. Default
|
||||||
/// [`TextAlign::Left`](super::text::TextAlign::Left).
|
/// [`TextAlign::Left`](super::text::TextAlign::Left).
|
||||||
pub fn align( mut self, a: super::text::TextAlign ) -> Self
|
pub fn align( mut self, a: super::text::TextAlign ) -> Self
|
||||||
@@ -608,6 +622,7 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
select_on_focus: self.select_on_focus,
|
select_on_focus: self.select_on_focus,
|
||||||
|
pin_cursor_to_end: self.pin_cursor_to_end,
|
||||||
password_toggle: self.password_toggle.clone().map( |( v, m )| ( v, ( *f )( m ) ) ),
|
password_toggle: self.password_toggle.clone().map( |( v, m )| ( v, ( *f )( m ) ) ),
|
||||||
read_only: self.read_only,
|
read_only: self.read_only,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
|
|||||||
.font_size_fluid( theme::VAL_FS )
|
.font_size_fluid( theme::VAL_FS )
|
||||||
.align( TextAlign::Center )
|
.align( TextAlign::Center )
|
||||||
.select_on_focus( true )
|
.select_on_focus( true )
|
||||||
|
.pin_cursor_to_end( true )
|
||||||
.on_change( move |s: String|
|
.on_change( move |s: String|
|
||||||
{
|
{
|
||||||
let new = parser( &s ).unwrap_or( snapshot );
|
let new = parser( &s ).unwrap_or( snapshot );
|
||||||
|
|||||||
Reference in New Issue
Block a user