TimeField

A segmented input for a time of day. Each part — hour, minute, optional second and optional AM/PM — is edited on its own with the keyboard, and the field keeps its width while digits are typed. DatePicker uses it to edit the time of a date.

Install

#Import

use gpui_kit::component::time_field::{
    HourCycle, TimeField, TimeFieldEvent, TimeFieldState, TimePrecision,
};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { TimePrecision, HourCycle, TimeField } from "time-field.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};

#Usage

#Basic Time Field

let time = cx.new(|cx| TimeFieldState::new(window, cx));

TimeField::new(&time)
VerticalLayout {
    spacing: 12px;
    cross-axis-alignment: center;
    TimeField {
        hour: 9;
        minute: 30;
    }
    TimeField {
        hour: 0;
        minute: 0;
    }
    TimeField {
        hour: 23;
        minute: 59;
    }
}
// Slint's first box is three fields — 09:30, 00:00 and 23:59 — over a
// 24-hour clock, each one the caller's own value.
let basic = column![
    TimeField::new("basic-0")
        .hour(app.basic[0].hour)
        .minute(app.basic[0].minute)
        .on_change(|change| Message::Basic { index: 0, change }),
    TimeField::new("basic-1")
        .hour(app.basic[1].hour)
        .minute(app.basic[1].minute)
        .on_change(|change| Message::Basic { index: 1, change }),
    TimeField::new("basic-2")
        .hour(app.basic[2].hour)
        .minute(app.basic[2].minute)
        .on_change(|change| Message::Basic { index: 2, change }),
    muted_line(&format!(
        "The first field reads {}. Each segment is a field of its own, and \
         the caller owns the value.",
        app.basic[0].hhmm()
    )),
]
.spacing(12.0);

The field starts at 00:00. Set a value with set_time, which does not emit an event:

use chrono::NaiveTime;

time.update(cx, |state, cx| {
    state.set_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap(), window, cx);
});
SlintNo Slint version of this example yet.

#With Seconds

let time = cx.new(|cx| {
    TimeFieldState::new(window, cx).precision(TimePrecision::Second)
});

TimeField::new(&time) // 09:30:15
HorizontalLayout {
    alignment: center;
    TimeField {
        hour: 14;
        minute: 5;
        second: 42;
        precision: TimePrecision.hours_minutes_seconds;
    }
}
// `precision(TimePrecision::HoursMinutesSeconds)` draws the third segment,
// the Slint `with-seconds`.
let seconds = column![
    TimeField::new("seconds")
        .hour(app.seconds.hour)
        .minute(app.seconds.minute)
        .second(app.seconds.second)
        .precision(TimePrecision::HoursMinutesSeconds)
        .on_change(Message::Seconds),
    muted_line(&format!(
        "With seconds: {} — the third segment appears with the precision.",
        app.seconds.hhmmss()
    )),
]
.spacing(12.0);

#12-Hour Clock

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `hour_cycle(HourCycle::H12)` shows 12–11 and the AM/PM label. iced hands a
// widget no key event, so GPUI's `a`/`p` toggle has nowhere to live and the
// label is read-only, as it is in the Slint component.
let h12 = column![
    TimeField::new("h12-0")
        .hour(app.h12[0].hour)
        .minute(app.h12[0].minute)
        .hour_cycle(HourCycle::H12)
        .on_change(|change| Message::H12 { index: 0, change }),
    TimeField::new("h12-1")
        .hour(app.h12[1].hour)
        .minute(app.h12[1].minute)
        .hour_cycle(HourCycle::H12)
        .on_change(|change| Message::H12 { index: 1, change }),
    TimeField::new("h12-2")
        .hour(app.h12[2].hour)
        .minute(app.h12[2].minute)
        .hour_cycle(HourCycle::H12)
        .on_change(|change| Message::H12 { index: 2, change }),
    muted_line(&format!(
        "12-hour clock: {}, {} and {}.",
        app.h12[0].h12(),
        app.h12[1].h12(),
        app.h12[2].h12()
    )),
]
.spacing(12.0);

The field uses a 24-hour clock by default. HourCycle::H12 shows hours from 12 to 11 followed by an AM/PM segment:

let time = cx.new(|cx| {
    TimeFieldState::new(window, cx).hour_cycle(HourCycle::H12)
});

TimeField::new(&time) // 09:30 PM
VerticalLayout {
    spacing: 12px;
    cross-axis-alignment: center;
    TimeField {
        hour: 9;
        minute: 30;
        hour-cycle: HourCycle.h12;
    }
    TimeField {
        hour: 14;
        minute: 5;
        hour-cycle: HourCycle.h12;
    }
    TimeField {
        hour: 0;
        minute: 0;
        hour-cycle: HourCycle.h12;
    }
}

#Sizes, Disabled and Invalid

TimeField::new(&time).small()
TimeField::new(&time).large()
TimeField::new(&time).disabled(true)

// Show the owner's validation result; edits are not rejected.
TimeField::new(&time).invalid(true)
VerticalLayout {
    spacing: 12px;
    alignment: start;
    TimeField {
        hour: 8;
        minute: 15;
        size: ControlSize.small;
    }
    TimeField {
        hour: 8;
        minute: 15;
    }
    TimeField {
        hour: 8;
        minute: 15;
        size: ControlSize.large;
    }
    TimeField {
        hour: 8;
        minute: 15;
        enabled: false;
    }
    TimeField {
        hour: 99;
        minute: 99;
        invalid: true;
    }
}
// The size ladder is 20 / 24 / 32 / 40, the Slint `control-height`; a
// disabled field is the muted fill and text, because iced has no whole-field
// opacity; and an invalid field keeps the danger edge while it is edited.
let sizes = column![
    row![
        TimeField::new("size-small")
            .hour(app.sizes[0].hour)
            .minute(app.sizes[0].minute)
            .size(ControlSize::Small)
            .on_change(|change| Message::Size { index: 0, change }),
        TimeField::new("size-medium")
            .hour(app.sizes[1].hour)
            .minute(app.sizes[1].minute)
            .size(ControlSize::Medium)
            .on_change(|change| Message::Size { index: 1, change }),
        TimeField::new("size-large")
            .hour(app.sizes[2].hour)
            .minute(app.sizes[2].minute)
            .size(ControlSize::Large)
            .on_change(|change| Message::Size { index: 2, change }),
    ]
    .spacing(16.0)
    .align_y(Alignment::Center),
    muted_line("The size ladder: 24 / 32 / 40, the Slint theme's control-height."),
    TimeField::new("disabled")
        .hour(app.disabled.hour)
        .minute(app.disabled.minute)
        .disabled(true),
    muted_line("disabled(true) refuses input and draws the muted field."),
    TimeField::new("invalid")
        .hour(app.invalid.hour)
        .minute(app.invalid.minute)
        .invalid(true)
        .on_change(Message::Invalid),
    muted_line("invalid(true): the frame's edge takes the danger colour, and edits are still accepted."),
]
.spacing(12.0);

#Handle Changes

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `on_change` reports the time an edit left behind — the Slint
// `changed(hour, minute, second)` — and the caller stores it. Type into a
// segment and the readout below moves.
let changes = column![
    TimeField::new("changes")
        .hour(app.changes.hour)
        .minute(app.changes.minute)
        .on_change(Message::Changes),
    muted_line(&format!("on_change reported: {}.", app.last)),
]
.spacing(8.0);

User edits emit TimeFieldEvent::Change with the new time:

cx.subscribe(&time, |this, _, event, cx| match event {
    TimeFieldEvent::Change(time) => {
        this.reminder = *time;
        cx.notify();
    }
});
SlintNo Slint version of this example yet.

#Keyboard

KeyAction
Up / DownStep the selected segment; it wraps without changing the next unit
Left / RightMove between segments
Tab / Shift-TabMove between segments, then leave the field
0–9Type the selected segment and move on once it is complete
a / pSet AM or PM on a 12-hour clock
Backspace / DeleteReset the selected segment