Files
ltk/src/widget/time_picker/mod.rs
T
admin cacf871ddd
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
An Android backend, a platform layer that keeps Wayland out of the runtime, the built-in keyboard for an app's own text input, and APKs packaged with Debian's own tools
ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there.
The platform layer. Input reaches the runtime as platform-neutral events: `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). The runtime is generic over the platform, `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time. The backend-neutral half of a loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type.
Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering.
The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it.
Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero.
Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora.
APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry.
The built-in keyboard for an app's own text input. While `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose`, so a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows, comes back when the serial changes and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes.
`README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend.
Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings.
2026-10-16 23:46:50 +02:00

470 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! TimePicker — HH:MM (and optionally :SS / AM-PM) stepper widget.
//!
//! Stateless — the application owns a [`Time`] and updates it from
//! the [`TimePicker::on_change`] callback. Each unit (hour, minute,
//! second) is a small stepper: an up arrow above the digit cell,
//! a down arrow below. Each digit cell is itself an editable
//! [`crate::widget::text_edit::TextEdit`] with `select_on_focus` —
//! click (or Tab) into it and type to set the value with the
//! keyboard, parser-clamped to the unit's valid range.
//!
//! Stepper buttons opt into press-and-hold repeat
//! ([`crate::widget::button::Button::repeating`]) so a held arrow
//! ramps through values at ~8 Hz. The minute / second steppers
//! honour [`TimePicker::minute_step`] (default 1, common values 5 /
//! 15) and **snap to the next or previous multiple of the step**
//! rather than adding the step verbatim — so `:32` with
//! `minute_step( 5 )` jumps to `:35` (next multiple) rather than
//! `:37`. Typed input bypasses the snap so the user can still type
//! any minute they want.
//!
//! ```rust,no_run
//! # use ltk::{ time_picker, Time, TimePicker };
//! # #[ derive( Clone ) ] enum Msg { TimeChanged( Time ) }
//! # struct App { time: Time }
//! # impl App { fn _ex( &self ) -> TimePicker<Msg> {
//! time_picker( self.time )
//! .minute_step( 5 )
//! .twelve_hour( true )
//! .on_change( Msg::TimeChanged )
//! # }}
//! ```
use std::sync::Arc;
use crate::layout::column::column;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
use crate::types::Length;
use super::Element;
mod theme;
#[ cfg( test ) ]
mod tests;
/// A wall-clock time, no date / no timezone. `hour` is 0–23 (24-hour
/// representation regardless of [`TimePicker::twelve_hour`] display
/// mode), `minute` and `second` are 0–59.
#[ derive( Clone, Copy, Debug, PartialEq, Eq, Hash, Default ) ]
pub struct Time
{
pub hour: u8,
pub minute: u8,
pub second: u8,
}
impl Time
{
pub const fn new( hour: u8, minute: u8 ) -> Self
{
Self { hour, minute, second: 0 }
}
pub const fn with_seconds( hour: u8, minute: u8, second: u8 ) -> Self
{
Self { hour, minute, second }
}
/// Total seconds from 00:00:00. Used internally for arithmetic
/// that needs to wrap correctly across midnight.
pub fn as_seconds( self ) -> i32
{
self.hour as i32 * 3600 + self.minute as i32 * 60 + self.second as i32
}
/// Reconstruct a [`Time`] from a (signed) seconds-of-day count,
/// wrapping on the 24-hour boundary in either direction.
pub fn from_seconds( s: i32 ) -> Self
{
let total = s.rem_euclid( 24 * 3600 );
let hour = ( total / 3600 ) as u8;
let minute = ( ( total % 3600 ) / 60 ) as u8;
let second = ( total % 60 ) as u8;
Self { hour, minute, second }
}
/// `true` when the values are in the canonical ranges (h: 0..24,
/// m / s: 0..60).
pub fn is_valid( self ) -> bool
{
self.hour < 24 && self.minute < 60 && self.second < 60
}
}
/// Filter `s` down to its ASCII digit characters, parse them as a
/// decimal integer and clamp the result to `0..=max`. Returns `None`
/// only when the filtered string is empty so the caller can detect
/// "the user erased the field" and skip the on-change message rather
/// than committing a spurious zero. Anything else — leading zeros,
/// punctuation, alpha characters, values past `max` — is forgiven and
/// folded into a valid `u8`.
fn parse_clamped_digits( s: &str, max: u8 ) -> Option<u8>
{
let digits: String = s.chars().filter( |c| c.is_ascii_digit() ).collect();
if digits.is_empty() { return None; }
// `u32` instead of `u8::from_str` so a typed "099" or "100" does
// not overflow the parse and bail out — we want to clamp, not
// reject.
let n: u32 = digits.parse().ok()?;
Some( n.min( max as u32 ) as u8 )
}
/// Compute the signed delta needed to step `value` to the next
/// (`dir = 1`) or previous (`dir = -1`) multiple of `step`. The
/// returned delta carries the right sign already, so the caller just
/// adds it to its working seconds-of-day count.
///
/// Designed for the minute / second steppers in
/// [`TimePicker`](crate::widget::time_picker::TimePicker) — it ignores
/// the wraparound at 60 (the picker always feeds the result through
/// [`Time::from_seconds`], which handles the `:00` → next-hour
/// rollover via `rem_euclid`).
///
/// Examples (`step = 5`):
/// * `value = 30, dir = 1` → `+5` → next aligned at `35`
/// * `value = 32, dir = 1` → `+3` → next aligned at `35`
/// * `value = 32, dir = -1` → `-2` → previous aligned at `30`
/// * `value = 30, dir = -1` → `-5` → previous aligned at `25`
fn snap_step_delta( value: i32, step: i32, dir: i32 ) -> i32
{
let step = step.max( 1 );
if dir >= 0
{
// Up: smallest multiple of `step` strictly greater than
// `value`. `value % step == 0` ⇒ jump a full step;
// otherwise round up to the next multiple.
let rem = value.rem_euclid( step );
if rem == 0 { step } else { step - rem }
}
else
{
// Down: largest multiple of `step` strictly less than
// `value`. Symmetric to the up case.
let rem = value.rem_euclid( step );
if rem == 0 { -step } else { -rem }
}
}
/// Time-of-day selector with up / down steppers per unit.
pub struct TimePicker<Msg: Clone>
{
pub( crate ) value: Time,
pub( crate ) on_change: Option<Arc<dyn Fn( Time ) -> Msg>>,
pub( crate ) minute_step: u8,
pub( crate ) second_step: u8,
pub( crate ) seconds: bool,
pub( crate ) twelve_hour: bool,
}
impl<Msg: Clone + 'static> TimePicker<Msg>
{
/// Create a time picker with the given current time.
pub fn new( value: Time ) -> Self
{
Self
{
value,
on_change: None,
minute_step: 1,
second_step: 1,
seconds: false,
twelve_hour: false,
}
}
pub fn on_change( mut self, f: impl Fn( Time ) -> Msg + 'static ) -> Self
{
self.on_change = Some( Arc::new( f ) );
self
}
/// Step size for the minute up / down buttons. Common values: 1
/// (default), 5, 15. Clamped to `1..=30`.
pub fn minute_step( mut self, step: u8 ) -> Self
{
self.minute_step = step.clamp( 1, 30 );
self
}
/// Step size for the second up / down buttons. Only meaningful
/// when [`Self::seconds`] is on. Clamped to `1..=30`.
pub fn second_step( mut self, step: u8 ) -> Self
{
self.second_step = step.clamp( 1, 30 );
self
}
/// Show / hide the seconds stepper. Default `false`.
pub fn seconds( mut self, on: bool ) -> Self
{
self.seconds = on;
self
}
/// Display the hour as 12-hour with an AM / PM toggle. Internal
/// storage stays 24-hour. Default `false`.
pub fn twelve_hour( mut self, on: bool ) -> Self
{
self.twelve_hour = on;
self
}
/// Build the `Element` tree representing this time picker.
pub fn build( self ) -> Element<Msg>
{
use super::{ button, container, icon_button, text, text_edit };
use super::button::ButtonVariant;
use super::text::TextAlign;
let on_chg = self.on_change.clone();
let value = self.value;
let twelve = self.twelve_hour;
// Up / down arrows load from the active theme as SVG icons
// (`icons/catalogue/filled/general/{up,down}-simple.svg`)
// tinted with `palette.text_primary` so they read against
// either light or dark surfaces. Falls back to the matching
// Unicode glyph when the icon is missing — the literal
// `▲` / `▼` characters are not present in every system
// font, so without the SVG path the buttons would render
// as missing-glyph boxes (see issue with stock fonts that
// lack U+25B2 / U+25BC).
const ARROW_PX: u32 = 16;
let arrow = | name: &str, fallback: &str | -> super::button::Button<Msg>
{
let btn = match crate::theme::icon_rgba( name, ARROW_PX )
{
Some( ( rgba, w, h ) ) =>
{
let tinted = std::sync::Arc::new(
crate::theme::tint_symbolic( &rgba, theme::text() ),
);
icon_button::<Msg>( tinted, w, h ).icon_size( ARROW_PX as f32 )
}
None => button::<Msg>( fallback ).variant( ButtonVariant::Tertiary ),
};
// Press-and-hold steps through values continuously, like
// holding an arrow key. The runtime fires `on_press`
// immediately on press and then re-fires at the same
// cadence as keyboard repeat while the button is held.
btn.repeating( true )
};
// Build a stepper column for one unit. The middle element is
// supplied by the caller (a static `text` for read-only mode,
// or an editable `text_edit` with the appropriate `on_change`
// parser when a callback is wired) so each unit can carry its
// own typing semantics.
//
// `fit_content()` + `padding(0.0)` are critical: without them
// each column would claim the full row width as its preferred
// size (the `Column` default), pushing the colon and the
// minute / second columns off-screen to the right.
let stepper = | middle: Element<Msg>, up_secs: i32, down_secs: i32 | -> crate::layout::column::Column<Msg>
{
let mut col = column::<Msg>()
.spacing( 4.0 )
.align_center_x( true )
.padding( 0.0 )
.fit_content();
let mut up = arrow( "general/up-simple", "▲" );
let mut dn = arrow( "general/down-simple", "▼" );
if let Some( ref cb ) = on_chg
{
let cb_up = cb.clone();
let cb_dn = cb.clone();
let new_up = Time::from_seconds( value.as_seconds() + up_secs );
let new_dn = Time::from_seconds( value.as_seconds() + down_secs );
up = up.on_press( cb_up( new_up ) );
dn = dn.on_press( cb_dn( new_dn ) );
}
col = col.push( up );
col = col.push( middle );
col = col.push( dn );
col
};
// Build the editable digit field for one unit. When no
// `on_change` is wired, falls back to a static `text` so the
// digits still render but the picker is read-only. With
// `on_change` wired, returns a borderless centred
// `text_edit` with `select_on_focus` so the user can click
// (or Tab) to focus and immediately retype the value.
// `parser` translates the typed string into a fresh `Time`,
// returning `None` when the input is invalid (empty / non-
// numeric) so the existing value is preserved.
let digit_field = | display: String, parser: Box<dyn Fn( &str ) -> Option<Time> > | -> Element<Msg>
{
match on_chg.as_ref()
{
None =>
{
text( display )
.size( Length::widget( theme::VAL_FS ) )
.color( theme::text() )
.align_center()
.into()
}
Some( cb ) =>
{
let cb = cb.clone();
let snapshot = value;
text_edit::<Msg>( "", display )
.borderless( true )
.fixed_width( Length::fluid( 72.0 ) )
.font_size_fluid( theme::VAL_FS )
.align( TextAlign::Center )
.select_on_focus( true )
.pin_cursor_to_end( true )
.on_change( move |s: String|
{
let new = parser( &s ).unwrap_or( snapshot );
cb( new )
} )
.into()
}
}
};
// Hour: respect 12-hour toggle for *display only*; storage
// stays 0–23. 12-hour displays 12 instead of 0.
let display_hour = if twelve
{
let h12 = value.hour % 12;
if h12 == 0 { 12 } else { h12 }
} else {
value.hour
};
let hour_field = digit_field(
format!( "{:02}", display_hour ),
Box::new( move |s: &str| -> Option<Time>
{
let typed = parse_clamped_digits( s, if twelve { 12 } else { 23 } )?;
let hour24 = if twelve
{
// 12-hour input: 12 with PM stays 12, 12 with AM
// becomes 0; 1–11 keep the AM / PM the user
// already had so retyping a value during a PM
// hour does not silently roll back to AM.
let is_pm = value.hour >= 12;
match ( typed, is_pm )
{
( 12, false ) => 0,
( 12, true ) => 12,
( h, false ) => h,
( h, true ) => h.saturating_add( 12 ).min( 23 ),
}
} else { typed };
Some( Time::with_seconds( hour24, value.minute, value.second ) )
} ),
);
let hour_col = stepper( hour_field, 3600, -3600 );
// Snap minutes / seconds to the next (or previous) multiple
// of the configured step instead of adding or subtracting the
// step verbatim. With `minute_step( 5 )` and a starting value
// of :32, the up arrow takes the user to :35 and the down
// arrow to :30 rather than perpetuating the off-step :37 /
// :27 — so the picker can never produce a value that is not a
// multiple of the step. Same logic for seconds. Typed input
// goes straight into the field — only the steppers enforce
// the snap, the user can still type any valid minute.
let minute_step = self.minute_step as i32;
let minute_up = snap_step_delta( value.minute as i32, minute_step, 1 ) * 60;
let minute_dn = snap_step_delta( value.minute as i32, minute_step, -1 ) * 60;
let minute_field = digit_field(
format!( "{:02}", value.minute ),
Box::new( move |s: &str| -> Option<Time>
{
let m = parse_clamped_digits( s, 59 )?;
Some( Time::with_seconds( value.hour, m, value.second ) )
} ),
);
let minute_col = stepper( minute_field, minute_up, minute_dn );
let mut units = row::<Msg>().spacing( Length::widget( theme::SPACING ) )
.push( hour_col )
.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
.push( minute_col );
if self.seconds
{
let second_step = self.second_step as i32;
let second_up = snap_step_delta( value.second as i32, second_step, 1 );
let second_dn = snap_step_delta( value.second as i32, second_step, -1 );
let second_field = digit_field(
format!( "{:02}", value.second ),
Box::new( move |s: &str| -> Option<Time>
{
let sec = parse_clamped_digits( s, 59 )?;
Some( Time::with_seconds( value.hour, value.minute, sec ) )
} ),
);
let second_col = stepper( second_field, second_up, second_dn );
units = units
.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
.push( second_col );
}
if twelve
{
let is_pm = value.hour >= 12;
let label = if is_pm { "PM" } else { "AM" };
let mut ampm = button::<Msg>( label ).variant( ButtonVariant::Secondary );
if let Some( ref cb ) = on_chg
{
// Toggle PM / AM = ±12 hours, modulo 24.
let toggled = Time::from_seconds( value.as_seconds() + 12 * 3600 );
ampm = ampm.on_press( cb( toggled ) );
}
// Fixed-width gap before AM/PM so the toggle sits next to
// the digits as a single cluster instead of being pushed
// to the right edge — Row's auto-centring then balances
// the whole hh:mm AM block inside the container. A flex
// spacer here would defeat the auto-centre and pin AM/PM
// to the right.
units = units.push( spacer().width( Length::widget( theme::SPACING * 2.0 ) ) ).push( ampm );
}
// `Row::layout` centres a cluster horizontally when there are
// no flex spacers inside (see `layout/row.rs` — `start_x =
// rect.x + (rect.width - fixed_w - gaps) / 2.0`). All the
// `units` rows we build here only contain fixed-width
// children (steppers, separator text, fixed-width spacer for
// AM/PM), so they centre automatically inside the container.
container::<Msg>( units )
.background( theme::surface_alt() )
.padding( Length::widget( theme::PADDING ) )
.radius( theme::RADIUS )
.into()
}
}
impl<Msg: Clone + 'static> From<TimePicker<Msg>> for Element<Msg>
{
fn from( t: TimePicker<Msg> ) -> Self { t.build() }
}
/// Create a [`TimePicker`] with the given current time.
///
/// ```rust,no_run
/// # use ltk::{ time_picker, Time, TimePicker };
/// # #[ derive( Clone ) ] enum Msg { TimeChanged( Time ) }
/// # struct App { time: Time }
/// # impl App { fn _ex( &self ) -> TimePicker<Msg> {
/// time_picker( self.time )
/// .minute_step( 5 )
/// .on_change( Msg::TimeChanged )
/// # }}
/// ```
pub fn time_picker<Msg: Clone + 'static>( value: Time ) -> TimePicker<Msg>
{
TimePicker::new( value )
}