diff --git a/.github/workflows/ecode-nightly.yml b/.github/workflows/ecode-nightly.yml
index 73f1c9d05..ae8e16b42 100644
--- a/.github/workflows/ecode-nightly.yml
+++ b/.github/workflows/ecode-nightly.yml
@@ -201,7 +201,7 @@ jobs:
config:
- compiler: default
arch: x86_64
- container: ubuntu-22.04
+ container: ubuntu-24.04
runs-on: ${{ matrix.config.container }}
env:
CC: gcc
@@ -222,17 +222,9 @@ jobs:
sudo apt-get update
- name: Install dependencies
run: |
- sudo apt-get install -y premake4 libfuse2 fuse mingw-w64 gcc-12 g++-12
+ sudo apt-get install -y premake4 libfuse2t64 fuse mingw-w64
wget https://cdn.ensoft.dev/eepp-assets/premake-5.0.0-beta6-linux.tar.gz
tar xvzf premake-5.0.0-beta6-linux.tar.gz
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 10
- sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 10
- sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc 30
- sudo update-alternatives --set cc /usr/bin/gcc
- sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++ 30
- sudo update-alternatives --set c++ /usr/bin/g++
- sudo update-alternatives --config gcc
- sudo update-alternatives --config g++
- name: Build ecode
run: |
bash projects/scripts/patch_commit_number.sh
diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css
index 2e84f6655..50a1429f5 100644
--- a/bin/assets/ui/breeze.css
+++ b/bin/assets/ui/breeze.css
@@ -167,6 +167,10 @@ SpinBox::input,
Tab,
TextEdit,
TextInput,
+DateTimeEdit,
+DatePicker,
+TimePicker,
+DateTimePicker,
TextArea,
textarea,
TextView,
@@ -188,6 +192,10 @@ TextEdit,
TextArea,
textarea,
TextInput,
+DateTimeEdit,
+DatePicker,
+TimePicker,
+DateTimePicker,
ComboBox::DropDownList,
SpinBox::input, {
selection-color: var(--font-selected-pressed);
@@ -250,7 +258,7 @@ SelectButton:selected:pressed {
pushbutton:disabled,
selectbutton:disabled,
-textinput:disabled,
+textinput:disabled, datetimeedit:disabled, datepicker:disabled, timepicker:disabled, datetimepicker:disabled,
textedit:disabled,
textarea:disabled,
DropDownList:disabled,
@@ -402,7 +410,7 @@ ComboBox::DropDownList::ListBox::item:selected {
tint: var(--font-selected-pressed);
}
-TextInput {
+TextInput, DateTimeEdit, DatePicker, TimePicker, DateTimePicker {
padding-left: var(--base-horizontal-padding);
padding-right: var(--base-horizontal-padding);
padding-top: var(--base-vertical-padding);
@@ -430,8 +438,8 @@ ComboBox::DropDownList {
padding-bottom: var(--base-vertical-padding);
}
-TextInput:hover,
-TextInput:focus,
+TextInput:hover, DateTimeEdit:hover, DatePicker:hover, TimePicker:hover, DateTimePicker:hover,
+TextInput:focus, DateTimeEdit:focus, DatePicker:focus, TimePicker:focus, DateTimePicker:focus,
SpinBox:hover,
SpinBox:focus {
border-color: var(--primary);
@@ -1297,3 +1305,123 @@ Menu::Separator {
}
}
+
+/* Civil date/time controls use the current light or dark palette. */
+DatePicker::button, DateTimePicker::button {
+ foreground-image: url("data:image/svg,");
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: var(--font);
+ color: var(--font);
+ background-color: transparent;
+ border-width: 0;
+ padding: 0;
+ cursor: arrow;
+}
+DatePicker::button:hover, DateTimePicker::button:hover {
+ foreground-tint: var(--font-selected-pressed);
+ background-color: var(--primary);
+ color: var(--font-selected-pressed);
+}
+Calendar {
+ color: var(--font);
+ background-color: var(--list-back);
+ border-color: var(--button-border);
+ border-width: var(--border-width);
+ border-radius: var(--button-radius);
+ width: auto;
+ height: auto;
+ min-width: 160dp;
+ min-height: 160dp;
+}
+Calendar::previous, Calendar::next, Calendar::title, Calendar::today, Calendar::day {
+ color: var(--font);
+ padding: 0;
+ border-width: 0;
+ background-color: transparent;
+}
+Calendar::previous {
+ foreground-image: url("data:image/svg,");
+}
+Calendar::next {
+ foreground-image: url("data:image/svg,");
+}
+Calendar::previous, Calendar::next {
+ cursor: arrow;
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: var(--font);
+}
+Calendar::previous:hover, Calendar::next:hover {
+ foreground-tint: var(--font-selected-pressed);
+}
+Calendar::weekday { color: var(--font-hint); }
+Calendar::day.outside-month { color: var(--font-hint); }
+Calendar::day:hover, Calendar::previous:hover, Calendar::next:hover,
+Calendar::title:hover, Calendar::today:hover, Calendar::day.selected {
+ background-color: var(--primary);
+ color: var(--font-selected-pressed);
+}
+Calendar::day.today {
+ border-width: 1dp;
+ border-color: var(--primary);
+}
+Calendar::day.focused { border-width: 1dp; border-color: var(--font-hint); }
+Calendar::day:disabled { opacity: 0.35; }
+
+DatePicker:disabled > DatePicker::button,
+DateTimePicker:disabled > DateTimePicker::button,
+DatePicker[allow-editing=false] > DatePicker::button,
+DateTimePicker[allow-editing=false] > DateTimePicker::button {
+ foreground-tint: var(--disabled-color);
+ background-color: transparent;
+ cursor: arrow;
+}
+
+/* Combined popup: a reusable calendar followed by a segmented time editor. */
+DateTimePicker::popup {
+ width: auto;
+ height: auto;
+ background-color: var(--list-back);
+ border-color: var(--button-border);
+ border-width: 1dp;
+ border-radius: 2dp;
+}
+DateTimePicker::popup > Calendar {
+ border-width: 0;
+ background-color: transparent;
+}
+DateTimePicker::time {
+ text-align: center;
+ font-size: 18dp;
+ padding: 24dp 8dp;
+ min-height: 72dp;
+ color: var(--font);
+ background-color: transparent;
+ border-color: var(--button-border);
+ border-width: 1dp 0 0 0;
+}
+DateTimePicker::time-up, DateTimePicker::time-down {
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: var(--font);
+ background-color: transparent;
+ border-width: 0;
+ padding: 0;
+ width: 24dp;
+ height: 22dp;
+ cursor: arrow;
+}
+DateTimePicker::time-up {
+ foreground-image: url("data:image/svg,");
+}
+DateTimePicker::time-down {
+ foreground-image: url("data:image/svg,");
+}
+DateTimePicker::time-up:hover, DateTimePicker::time-down:hover {
+ foreground-tint: var(--font-selected-pressed);
+ background-color: var(--primary);
+}
diff --git a/bin/assets/ui/uitheme.css b/bin/assets/ui/uitheme.css
index 8eaf58d13..edc8cb0d4 100644
--- a/bin/assets/ui/uitheme.css
+++ b/bin/assets/ui/uitheme.css
@@ -116,3 +116,103 @@ Menu::RadioButton::shortcut {
margin-right: 8dp;
}
+
+Calendar {
+ background-color: #2f3030;
+ border-color: #969696;
+ border-width: 1dp;
+ width: auto;
+ height: auto;
+ min-width: 160dp;
+ min-height: 160dp;
+}
+Calendar::previous, Calendar::next, Calendar::title, Calendar::today, Calendar::day {
+ padding: 0;
+ border-width: 0;
+ background-color: transparent;
+}
+Calendar::previous {
+ foreground-image: url("data:image/svg,");
+}
+Calendar::next {
+ foreground-image: url("data:image/svg,");
+}
+Calendar::previous, Calendar::next {
+ cursor: arrow;
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: #E6E6E6;
+}
+Calendar::previous:hover, Calendar::next:hover {
+ foreground-tint: #fff;
+}
+Calendar::weekday, Calendar::day.outside-month { color: #969696; }
+Calendar::day.selected, Calendar::day:hover { background-color: #606060; color: #fff; }
+Calendar::day.today { border-color: #969696; border-width: 1dp; }
+Calendar::day.focused { border-color: #E6E6E6; border-width: 1dp; }
+Calendar::day:disabled { opacity: 0.35; }
+DatePicker::button, DateTimePicker::button {
+ foreground-image: url("data:image/svg,");
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: #E6E6E6;
+ padding: 0;
+ cursor: arrow;
+ border-width: 0;
+ background-color: transparent;
+}
+DatePicker:disabled > DatePicker::button, DateTimePicker:disabled > DateTimePicker::button,
+DatePicker[allow-editing=false] > DatePicker::button,
+DateTimePicker[allow-editing=false] > DateTimePicker::button {
+ foreground-tint: #969696;
+ background-color: transparent;
+ cursor: arrow;
+}
+
+/* Combined popup: a reusable calendar followed by a segmented time editor. */
+DateTimePicker::popup {
+ width: auto;
+ height: auto;
+ background-color: #2f3030;
+ border-color: #969696;
+ border-width: 1dp;
+ border-radius: 2dp;
+}
+DateTimePicker::popup > Calendar {
+ border-width: 0;
+ background-color: transparent;
+}
+DateTimePicker::time {
+ text-align: center;
+ font-size: 18dp;
+ padding: 24dp 8dp;
+ min-height: 72dp;
+ color: #E6E6E6;
+ background-color: transparent;
+ border-color: #969696;
+ border-width: 1dp 0 0 0;
+}
+DateTimePicker::time-up, DateTimePicker::time-down {
+ foreground-size: 12dp 12dp;
+ foreground-position: center;
+ foreground-repeat: no-repeat;
+ foreground-tint: #E6E6E6;
+ background-color: transparent;
+ border-width: 0;
+ padding: 0;
+ width: 24dp;
+ height: 22dp;
+ cursor: arrow;
+}
+DateTimePicker::time-up {
+ foreground-image: url("data:image/svg,");
+}
+DateTimePicker::time-down {
+ foreground-image: url("data:image/svg,");
+}
+DateTimePicker::time-up:hover, DateTimePicker::time-down:hover {
+ foreground-tint: #fff;
+ background-color: #606060;
+}
diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp
index 56fdbadce..55498bf1f 100644
Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp differ
diff --git a/docs/articles/css_specification.md b/docs/articles/css_specification.md
index 693218d68..c27a24517 100644
--- a/docs/articles/css_specification.md
+++ b/docs/articles/css_specification.md
@@ -153,6 +153,37 @@ Example: `TextView + image` will match all `` elements that directly foll
---
+## Date/time control styling
+
+Native tags are `DateTimeEdit`, `DatePicker`, `TimePicker`, `DateTimePicker`, and `Calendar`.
+Picker button tags are `DatePicker::button` and `DateTimePicker::button`. Calendar subparts
+are `Calendar::header`, `::previous`, `::next`, `::title`, `::weekdays`, `::weekday`, `::grid`,
+`::day`, and `::today` (each uses the `Calendar` prefix). Day-cell classes are `selected`,
+`today`, `outside-month`, and `focused`; unavailable dates use `:disabled`.
+
+The combined popup exposes `DateTimePicker::popup`, `DateTimePicker::time`,
+`DateTimePicker::time-up`, and `DateTimePicker::time-down`. Time buttons have segment classes
+`hour`, `minute`, `second`, `millisecond`, or `am-pm`. Standard foreground image/size/tint,
+font, padding, border, and color properties style these parts:
+
+```css
+DateTimePicker { show-seconds: true; minute-step: 15; }
+Calendar { font-size: 12dp; min-width: 160dp; min-height: 160dp; }
+DateTimePicker::button { foreground-size: 12dp 12dp; cursor: arrow; }
+DateTimePicker[allow-editing=false] > DateTimePicker::button { foreground-tint: gray; }
+Calendar::day.selected { background-color: #0078d7; color: white; }
+```
+
+Calendars fit their text metrics automatically unless an explicit fixed size overrides them.
+Seconds and milliseconds default to hidden; changing display settings also updates an open
+native date-time popup. Theme icons are CSS-defined inline SVG images sized in `dp`.
+
+In HTML documents, style the `input` host. Anonymous control parts use private light defaults
+from the document's user-agent CSS and ignore author rules. HTML temporal constraints use
+`min`, `max`, `step`, `required`, `readonly`, and `disabled`, rather than the native bound and
+editing properties. See [date/time pickers](ui_date_time_pickers.md) for interactions and APIs,
+and [HTML compatibility](ui_html_compatibility.md) for the supported form behavior.
+
## CSS Properties
eepp CSS properties can be categorized in two big groups: CSS standard properties and eepp CSS
@@ -177,6 +208,19 @@ properties.
---
+### active-section
+
+Selects the active segment of a date/time editor. Values: `none`, `day`, `month`, `year`,
+`hour`, `minute`, `second`, `millisecond`, and `am-pm`. The segment must exist in the current
+display format; `none` clears segment selection.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: `none`
+
+---
+
### animation-delay
Read [animation-delay](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-delay) docs.
@@ -232,9 +276,24 @@ elastic-out, elastic-in-out, none.
### allow-editing
-Enable or disable editing on input elements.
+Enable or disable editing on input elements. Read-only date/time controls retain text
+selection, close an open popup, and mute their calendar button using theme CSS.
-* Applicable to: EE::UI::UITextEdit (TextEdit), EE::UI::UITextInput (TextInput)
+* Applicable to: EE::UI::UITextEdit (TextEdit), EE::UI::UITextInput (TextInput),
+ EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `true`
+
+---
+
+### allow-empty
+
+Allows a date/time editor to have no value. Setting this to `false` initializes an empty
+editor from the current date/time, subject to its bounds, and prevents clearing it.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
* Data Type: [boolean](#boolean-data-type)
* Default value: `true`
@@ -432,6 +491,17 @@ Defines where the border box is drawn.
---
+### calendar-view
+
+Selects the calendar view: `days`, `months`, or `years`. Changing the view does not select
+a date.
+
+* Applicable to: EE::UI::UICalendar (Calendar)
+* Data Type: [string](#string-data-type)
+* Default value: `days`
+
+---
+
### change-page-percent
Sets the percentage of scroll of a page that triggers a page change in a ViewPager.
@@ -577,6 +647,32 @@ Custom cursors not yet supported (but supported by the engine, only not implemen
---
+### date-format
+
+Sets the date display pattern, or `locale` to use the editor locale. Supported tokens are
+`d`/`dd`, `M`/`MM`, and `yy`/`yyyy`; all three date components are required. Single quotes
+escape literal text. For example: `date-format: "dd/MM/yyyy";`. This changes display and
+editing, while [value](#value) retains ISO serialization.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: `locale`
+
+---
+
+### disabled
+
+Disables an HTML input when the attribute/property is present, including an empty value.
+Disabled temporal inputs are excluded from constraint validation and form submission. This
+HTML presence attribute is distinct from the native [enabled](#enabled) boolean property.
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: HTML presence attribute
+* Default value: _Absent_
+
+---
+
### disable-editor-flags
Allows disabling specific behavior flags for the code editor component.
@@ -592,6 +688,17 @@ Multiple flags are separated by `|`.
---
+### displayed-month
+
+Sets the calendar month to display using an ISO date (`yyyy-MM-dd`). The day component is
+normalized to the first day of that month. Navigation does not change the selected value.
+
+* Applicable to: EE::UI::UICalendar (Calendar)
+* Data Type: [string](#string-data-type)
+* Default value: Current month for a standalone calendar; picker popups synchronize on opening
+
+---
+
### display-options
Allows to set what information should be displayed of the current image.
@@ -706,6 +813,28 @@ If `true` the inner element for text is expanded to occupy as much horizontal sp
---
+### first-day-of-week
+
+Sets the first weekday of the calendar: Sunday `0` through Saturday `6`. This controls
+the weekday header and day-grid ordering.
+
+* Applicable to: EE::UI::UICalendar (Calendar)
+* Data Type: [integer](#integer-data-type)
+* Default value: The calendar locale's first weekday
+
+---
+
+### focused-date
+
+Sets the calendar keyboard-focus date using ISO format (`yyyy-MM-dd`). It is clamped to
+the calendar bounds and brings its month into view without selecting it.
+
+* Applicable to: EE::UI::UICalendar (Calendar)
+* Data Type: [string](#string-data-type)
+* Default value: Today for a standalone calendar; picker popups synchronize on opening
+
+---
+
### font-family
Read [font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family) documentation.
@@ -1006,6 +1135,32 @@ Sets the hint font stroke (the outline) width.
---
+### hour-cycle
+
+Sets the hour cycle for locale-derived time patterns: `locale`, `12`, or `24`. Aliases
+`h12` and `h24` are accepted as values. An explicit [time-format](#time-format) defines its
+own hour tokens and takes precedence.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: `locale`
+
+---
+
+### hour-step
+
+Sets the positive integer hour increment for stepping the active hour segment, including
+date-time popup steppers. Time-only stepping wraps within the day; date-time stepping
+carries across date boundaries.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [integer](#integer-data-type)
+* Default value: `1`
+
+---
+
### href
Specifies the link destination of an Anchor element
@@ -1355,6 +1510,63 @@ Read [margin-top](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-top) d
---
+### max
+
+Sets the maximum valid value for HTML `date`, `time`, and `datetime-local` inputs.
+Use the input type's ISO value syntax (see [value](#value)). Invalid bounds are ignored.
+Out-of-range values report a validation error rather than being silently clamped. Time
+ranges with `min` later than `max` wrap across midnight.
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### max-date
+
+Sets the inclusive maximum for native `date` mode using ISO syntax
+(`yyyy-MM-dd`). Native editors clamp values to their active mode's bounds. Calendar bounds
+also disable unavailable dates.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker),
+ EE::UI::UICalendar (Calendar)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### max-datetime
+
+Sets the inclusive maximum for native `datetime` mode using ISO syntax
+(`yyyy-MM-ddTHH:mm:ss[.SSS]`). Native editors clamp values to their active mode's bounds.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### max-time
+
+Sets the inclusive maximum for native `time` mode using ISO syntax
+(`HH:mm:ss[.SSS]`). Native editors clamp values to their active mode's bounds.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
### max-height
Read [max-height](https://developer.mozilla.org/en-US/docs/Web/CSS/max-height) documentation.
@@ -1455,6 +1667,63 @@ Sets how is the dropdown-menu width calculated.
---
+### min
+
+Sets the minimum valid value for HTML `date`, `time`, and `datetime-local` inputs.
+Use the input type's ISO value syntax (see [value](#value)). Invalid bounds are ignored.
+Out-of-range values report a validation error rather than being silently clamped. Time
+ranges with `min` later than `max` wrap across midnight.
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### min-date
+
+Sets the inclusive minimum for native `date` mode using ISO syntax
+(`yyyy-MM-dd`). Native editors clamp values to their active mode's bounds. Calendar bounds
+also disable unavailable dates.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker),
+ EE::UI::UICalendar (Calendar)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### min-datetime
+
+Sets the inclusive minimum for native `datetime` mode using ISO syntax
+(`yyyy-MM-ddTHH:mm:ss[.SSS]`). Native editors clamp values to their active mode's bounds.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
+### min-time
+
+Sets the inclusive minimum for native `time` mode using ISO syntax
+(`HH:mm:ss[.SSS]`). Native editors clamp values to their active mode's bounds.
+An empty string removes the bound. If the bounds cross, the opposite bound moves to the
+new bound. HTML inputs use [min](#min) and [max](#max) instead.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: _No bound_
+
+---
+
### min-height
Read [min-height](https://developer.mozilla.org/en-US/docs/Web/CSS/min-height) documentation.
@@ -1496,6 +1765,19 @@ Sets the minimum value that a range element will hold.
---
+### minute-step
+
+Sets the positive integer minute increment when stepping the active minute segment or
+using the date-time popup's minute buttons. For example, `minute-step: 15;` advances by
+fifteen minutes. This controls the increment, not validation or snapping to multiples.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [integer](#integer-data-type)
+* Default value: `1`
+
+---
+
### min-width
Read [min-width](https://developer.mozilla.org/en-US/docs/Web/CSS/min-width) documentation.
@@ -1512,6 +1794,18 @@ Sets the movement speed in displaced length per second of the progress bar fille
---
+### mode
+
+Sets the shared editor mode: `date`, `time`, or `datetime`. The picker classes initialize
+their corresponding mode automatically. Values and display formats follow the active mode.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: `date` for DateTimeEdit and DatePicker; `time` for TimePicker; `datetime` for DateTimePicker
+
+---
+
### numeric
Sets the text input as a numeric input, this means that only numbers will be accepted and
@@ -1612,12 +1906,25 @@ component.
---
+### popup-open
+
+Exposes whether the calendar popup is currently open as `true` or `false`, for inspection,
+binding and attribute selectors. This property is read-only; a declaration does not open
+or close the popup. Use the picker API or normal keyboard/mouse interaction.
+
+* Applicable to: EE::UI::UIDatePicker (DatePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
### popup-to-root
-Sets if the drop down listbox should popup in the root widget node, otherwise will popup in the
-parent window.
+Sets whether a dropdown or picker popup is parented to the scene root instead of its
+parent window container. Popup placement is clamped to the scene's visible viewport.
-* Applicable to: EE::UI::UIDropDownList (DropDownList)
+* Applicable to: EE::UI::UIDropDownList (DropDownList), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UIDateTimePicker (DateTimePicker)
* Data Type: [boolean](#boolean-data-type)
* Default value: `false`
@@ -1655,6 +1962,32 @@ the last child to the first.
---
+### readonly
+
+Makes an HTML input read-only when the attribute/property is present, including an empty
+value. Temporal inputs retain text selection but prevent value edits and opening their
+popup, and are excluded from constraint validation. Use [allow-editing](#allow-editing)
+for native date/time controls.
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: HTML presence attribute
+* Default value: _Absent_
+
+---
+
+### required
+
+Requires a nonempty value for HTML `date`, `time`, and `datetime-local` inputs during
+constraint validation when the attribute/property is present, including an empty value.
+Read-only and disabled inputs are excluded from validation. Unlike native
+[allow-empty](#allow-empty), this does not initialize or prevent an empty value.
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: HTML presence attribute
+* Default value: _Absent_
+
+---
+
### rotation
Sets the element rotation in degrees.
@@ -1864,6 +2197,47 @@ Sets the scrollbar style (for the moment defines if it contains buttons at its e
---
+### second-step
+
+Sets the positive integer second increment when stepping the active second segment or
+using the date-time popup's second buttons. Show the segment with [show-seconds](#show-seconds)
+or an explicit [time-format](#time-format).
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [integer](#integer-data-type)
+* Default value: `1`
+
+---
+
+### show-milliseconds
+
+Includes milliseconds and seconds in locale-derived time patterns. Enabling this also
+enables [show-seconds](#show-seconds); disabling it leaves seconds enabled. Hidden
+precision remains in the stored value. Explicit [time-format](#time-format) patterns
+control their own segments.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
+### show-seconds
+
+Includes seconds in locale-derived time patterns, independently of milliseconds.
+Disabling seconds also disables [show-milliseconds](#show-milliseconds). Hidden precision
+remains in the stored value. Explicit [time-format](#time-format) patterns control their
+own segments. The date-time popup follows the editor's current display precision.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
### selected
Sets the element state as selected when supported by the specific widget.
@@ -2160,6 +2534,20 @@ Sets boolean flags for table and tree views. Multiple flags can be set, separate
---
+### step
+
+Sets the allowed interval for HTML `date`, `time`, and `datetime-local` input validation.
+Date steps use days; time and date-time steps use seconds and accept fractional seconds.
+`any` disables step-mismatch validation. The base is a valid `min`, otherwise the initial
+`value` attribute, otherwise the Unix civil epoch. This HTML constraint is distinct from
+native [hour-step](#hour-step), [minute-step](#minute-step), and [second-step](#second-step).
+
+* Applicable to: EE::UI::UIHTMLInput (input)
+* Data Type: Positive [number](#number-data-type) or `any`
+* Default value: `1` for date; `60` for time and datetime-local
+
+---
+
### text
Sets the text to any element that holds or contains text.
@@ -2316,6 +2704,21 @@ Sets the timing function (easing function) of a view pager.
---
+### time-format
+
+Sets the time display pattern, or `locale` to use the editor locale. Tokens: `H`/`HH`
+(24-hour), `h`/`hh` (12-hour), `m`/`mm`, `s`/`ss`, `S`/`SS`/`SSS`, and `a` (AM/PM).
+An hour and minutes are required; 12-hour patterns also require `a`. Single quotes escape
+literal text. For example: `time-format: "HH:mm:ss";`. Explicit patterns control their own
+precision and hour cycle; [value](#value) retains ISO serialization.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [string](#string-data-type)
+* Default value: `locale`
+
+---
+
### tint
Sets the tint color of an image.
@@ -2401,10 +2804,27 @@ elastic-out, elastic-in-out, cubic-bezier, none.
### value
-Sets the current value to an element that accepts values.
+Sets the current value to an element that accepts values. Numeric controls use numbers.
+Date/time controls use locale-independent ISO strings:
-* Applicable to: EE::UI::UIScrollBar (ScrollBar), EE::UI::UISlider (Slider), EE::UI::UISpinBox (SpinBox)
-* Data Type: [number](#number-data-type)
+| Control or mode | Value syntax | Example |
+| --- | --- | --- |
+| Calendar, native date mode, HTML `date` | `yyyy-MM-dd` | `2026-09-28` |
+| Native time mode | `HH:mm:ss[.SSS]` | `20:14:32.125` |
+| Native datetime mode | `yyyy-MM-ddTHH:mm:ss[.SSS]` | `2026-09-28T20:14:32.125` |
+| HTML `time` | `HH:mm[:ss[.SSS]]` | `20:14` |
+| HTML `datetime-local` | `yyyy-MM-ddTHH:mm[:ss[.SSS]]` | `2026-09-28T20:14` |
+
+An empty string clears a native editor when [allow-empty](#allow-empty) permits it, or clears
+calendar selection. Display formats and hidden precision do not change ISO serialization.
+HTML temporal inputs sanitize invalid strings to empty and validate constraints without
+clamping their supplied value. Local date-time values carry no timezone.
+
+* Applicable to: EE::UI::UIScrollBar (ScrollBar), EE::UI::UISlider (Slider), EE::UI::UISpinBox (SpinBox),
+ EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker),
+ EE::UI::UICalendar (Calendar), EE::UI::UIHTMLInput (input)
+* Data Type: [number](#number-data-type) or ISO [string](#string-data-type), depending on the control
* Default value: _Not set_
---
@@ -2452,6 +2872,19 @@ The mode defines the visibility of the vertical scroll.
---
+### wheel-editing
+
+Allows mouse-wheel stepping of the active date/time segment while the editor has focus.
+The selected segment uses its configured step and bounds. [allow-editing](#allow-editing)
+must also be enabled.
+
+* Applicable to: EE::UI::UIDateTimeEdit (DateTimeEdit), EE::UI::UIDatePicker (DatePicker),
+ EE::UI::UITimePicker (TimePicker), EE::UI::UIDateTimePicker (DateTimePicker)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
### window-border-auto-size
Sets if the window border size should be automatically calculated based on the skin size.
diff --git a/docs/articles/ui_date_time_pickers.md b/docs/articles/ui_date_time_pickers.md
new file mode 100644
index 000000000..6e04e48f2
--- /dev/null
+++ b/docs/articles/ui_date_time_pickers.md
@@ -0,0 +1,211 @@
+# Date and time controls
+
+`UIDateTimeEdit` is one segmented `UITextInput` surface with date, time, and local date-time
+modes. `UIDatePicker` adds a calendar popup, `UITimePicker` configures time mode without a
+popup, and `UIDateTimePicker` combines a calendar and time editor in one popup.
+`UICalendar` can also be used as a standalone widget.
+
+## Native XML
+
+```xml
+
+
+
+
+
+```
+
+Values are optional typed civil values: `CalendarDate`, `TimeOfDay`, and `LocalDateTime`,
+from ``. They do not carry time zones. Use `setDate()`, `setTime()`,
+or `setDateTime()` and the corresponding optional getters. `clear()` removes a value when
+`allow-empty` is true (the default). Canonical setters clamp to the active mode's bounds and
+emit `OnValueChange` only when the typed value changes. The `value` property exposes ISO
+serialization for ordinary data binding. Hidden seconds and milliseconds remain part of the
+value; serialization preserves nonzero milliseconds even if they are not displayed.
+
+## Serialization and Unix timestamps
+
+`DateTimeFormatter` in `` formats ISO values independently of
+the display locale and parses them with an explicit pattern:
+
+```cpp
+using namespace EE;
+using namespace EE::System;
+
+const CalendarDate date{ 2026, 9, 28 };
+const std::string storedDate = DateTimeFormatter::toISODate( date ); // "2026-09-28"
+const auto restoredDate = DateTimeFormatter::parseDate( storedDate, DateTimeFormat( "yyyy-MM-dd" ) );
+
+const LocalDateTime value{ date, { 20, 14, 32, 125 } };
+const std::string storedValue = DateTimeFormatter::toISOLocalDateTime( value, true );
+const auto restoredValue = DateTimeFormatter::parseDateTime(
+ storedValue, DateTimeFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS" ) );
+
+// The application knows this value uses UTC-03:00; the types do not store a timezone.
+const Int32 utcOffsetSeconds = -3 * 60 * 60;
+const auto timestamp = value.toUnixTimestampMilliseconds( utcOffsetSeconds );
+if ( timestamp ) {
+ const auto restored = LocalDateTime::fromUnixTimestampMilliseconds(
+ *timestamp, utcOffsetSeconds );
+}
+```
+
+Unix timestamps count seconds or milliseconds since `1970-01-01T00:00:00Z`, without leap
+seconds. `CalendarDate::toUnixTimestamp()` uses midnight UTC; `fromUnixTimestamp()` extracts
+the UTC date containing that instant. `LocalDateTime` conversions default to UTC and accept an offset
+in seconds, where `local = UTC + offset`. The offset is not inferred from the
+system timezone and does not resolve daylight-saving gaps or ambiguous times. Applications
+using named timezones must resolve the offset for the particular date/time before conversion.
+
+The seconds methods discard milliseconds, rounding down even before the epoch. The
+`toUnixTimestampMilliseconds()` / `fromUnixTimestampMilliseconds()` methods preserve them.
+Conversions return `std::optional`: invalid civil values, an out-of-range year, or an
+unrepresentable Int64 millisecond timestamp produce `std::nullopt`. The full Int32 year range
+fits in Int64 seconds, but only part of it fits in Int64 milliseconds. These conversions are
+independent of locale, `time_t` size, and the machine's timezone. For a date-only database
+column, ISO date text or typed date fields preserve its civil meaning directly.
+
+## Editing
+
+Click a section to select it. Left/Right navigate sections; Home/End select the first/last.
+Up/Down step the active section. Numeric entry automatically advances when a complete value
+has been entered. Pending digits are shown with underscores, commit on navigation, Enter,
+focus loss, or timeout, and can be canceled with Escape. Configure the timeout in C++ with
+`setPendingDigitTimeout()` (default: one second). Delete/Backspace clear the value when allowed.
+Copy and select-all use normal input commands. Paste replaces the whole value, including when
+only one section is selected, and accepts ISO regardless of the display format. Invalid input
+preserves the previous valid value. Wheel edits require both focus and `wheel-editing: true`;
+wheel editing is disabled by default. `allow-editing: false` prevents user mutation.
+
+Month/year edits clamp the day at month ends. Time-only arithmetic wraps within the day;
+local date-time arithmetic carries across date boundaries. AM/PM entry changes the period
+without advancing the date. Utility arithmetic outside the representable Int32 year range
+leaves the value unchanged.
+
+## Locale and properties
+
+Display defaults come from `DateTimeLocale::system()` (Windows regional settings on Windows), with a centralized locale-name fallback
+for information unavailable through the standard library. `setLocale()` accepts explicit date
+order/separator, hour cycle, month and weekday names, AM/PM labels, and first weekday; this also
+supports locales unavailable on the host. `fromLocaleName()` resolves ordering and cycle
+fallbacks; applications can supply translated names in the returned structure.
+
+| Property | Meaning |
+| --- | --- |
+| `mode` | `date`, `time`, or `datetime` on the shared editor |
+| `date-format`, `time-format` | `locale` (default), or quoted token pattern |
+| `hour-cycle` | `locale`, `12`, or `24` |
+| `show-seconds` | Show seconds without milliseconds; default `false` |
+| `show-milliseconds` | Show milliseconds and seconds; default `false` |
+| `hour-step`, `minute-step`, `second-step` | Positive integer steps |
+| `min-date`, `max-date` | Date-mode ISO bounds |
+| `min-time`, `max-time` | Time-mode ISO bounds |
+| `min-datetime`, `max-datetime` | Local date-time ISO bounds |
+| `allow-empty`, `wheel-editing` | Empty-value and focused wheel editing policies |
+| `active-section` | `none`, `day`, `month`, `year`, `hour`, `minute`, `second`, `millisecond`, `am-pm` |
+| `popup-to-root` | Parent picker popup to scene root instead of its window container |
+| `popup-open` | Inspectable picker open state |
+| `first-day-of-week` | Calendar weekday: Sunday `0` through Saturday `6` |
+| `calendar-view` | Calendar `days`, `months`, or `years` |
+| `displayed-month`, `focused-date` | Calendar ISO navigation state |
+
+Date patterns require day, month, and year. Time patterns require one hour token and minutes;
+12-hour patterns also require `a`. Tokens: `d`/`dd`, `M`/`MM`, `yy`/`yyyy`, `H`/`HH`, `h`/`hh`,
+`m`/`mm`, `s`/`ss`, `S`/`SS`/`SSS`, and `a`. Quote literal text with single quotes inside the
+pattern. Two-digit years parse into 2000–2099. Standard serialized dates use `yyyy-MM-dd`,
+times `HH:mm:ss[.SSS]`, and local date-times `yyyy-MM-ddTHH:mm:ss[.SSS]`.
+
+Setting a minimum above the maximum moves the maximum to that minimum; setting a maximum
+below the minimum moves the minimum to that maximum. Empty bound properties remove constraints.
+
+Seconds and milliseconds can be toggled separately with `setShowSeconds()` and
+`setShowMilliseconds()` or their CSS/XML properties. Both default to hidden. Enabling
+milliseconds includes seconds; hiding milliseconds leaves seconds enabled. Hiding seconds
+hides both. These options change display precision without discarding stored precision, and
+also update the date-time popup footer. An explicit `time-format` controls its own sections.
+
+## Calendar popup
+
+Click the trailing button, press F4, or press Alt+Down. Opening an empty picker displays today
+without assigning it. Date-only selection updates the value, closes the popup, and restores
+editor focus. In a date-time picker, selecting a date preserves the time and keeps the popup
+open so the footer can edit it. Selecting a date into an empty value uses midnight, then
+applies its constraints.
+
+The footer uses the same segmented editing as `UITimePicker`, with SVG up/down buttons for
+hour, minute, and any displayed seconds, milliseconds, or AM/PM sections. It follows the
+field's locale, format, steps, and boundary-date time constraints. Changing these settings while
+the popup is open updates its controls immediately. Setting the owner read-only closes the popup.
+Committed edits update the field immediately and preserve hidden precision. `getPopupTimePicker()` returns the owned
+footer editor after the first opening. Enter in the footer closes the popup; Escape cancels
+pending digits and closes it while preserving already committed changes. Focus leaving the
+field and its entire popup also closes it. The popup measures both calendar and time footer
+before choosing its placement, flips above when space permits, and clamps to scene bounds.
+`UIPopUp::findBestPosition()` supplies the shared world-pixel placement calculation used by
+field popups and `UIMenu::findBestMenuPos()` for trigger and cursor menus. Cascading menus
+retain their menu-specific rules for avoiding the previous menu.
+
+Calendar selection and navigation are separate. Arrows move focus by day/week, Home/End move
+to week boundaries, PageUp/PageDown move by month, and Ctrl+PageUp/PageDown move by year.
+Enter explicitly selects focus. Click the title to choose a month, then click it again to choose
+a year. Previous/Next page through those views. Today navigates to today without selecting it.
+`OnItemSelected` reports explicit selection, including re-selection of the same date;
+`OnValueChange` reports a changed selection. The 42 day widgets are reused across navigation.
+
+## Theme and example
+
+Breeze provides light/dark palette styling. Theme tags include `datetimeedit`, `datepicker`,
+`timepicker`, `datetimepicker`, `datepicker::button`, `datetimepicker::button`, `calendar`,
+and `calendar::header/previous/next/title/weekdays/weekday/grid/day/today`.
+The combined popup exposes `datetimepicker::popup`, `datetimepicker::time`, and
+`datetimepicker::time-up/time-down`; arrow classes identify `hour`, `minute`, `second`,
+`millisecond`, and `am-pm`. CSS controls the footer font, padding, minimum height, and SVG
+arrow size in `dp`. Picker buttons use the standard arrow cursor; editable text uses the I-beam.
+Calendars size to their text metrics by default, with a theme minimum of `160dp`
+set by the `Calendar` rule in `bin/assets/ui/breeze.css` and `bin/assets/ui/uitheme.css`.
+Override `min-width` / `min-height` to change the preferred size while retaining automatic
+text fitting.
+Changing fonts automatically updates the popup size, header, weekday row, and day grid.
+Explicit fixed width/height remain application overrides; choose enough space for the selected font.
+Picker buttons use an inline SVG in `foreground-image`, with `foreground-size: 12dp 12dp`,
+centered positioning, and theme-controlled `foreground-tint`. The SVG uses a white base fill
+so the native image tint supplies the light/dark and hover colors. No font glyph is required.
+Calendar previous/next buttons likewise use CSS-supplied left/right SVGs, centered at `12dp`.
+Read-only picker buttons use `[allow-editing=false]` selectors to mute the calendar icon and
+remove its hover highlight, while leaving the input text readable and selectable.
+Day classes are `selected`, `today`, `outside-month`, and `focused`; constrained cells use
+normal disabled state. Today uses a border, selection a fill.
+
+Build `eepp-ui-date-time-picker` and run `bin/eepp-ui-date-time-picker`; `--us` switches to a
+Sunday-first, 12-hour locale. `--light`, `--small-font`, `--large-font`, and `--narrow`
+exercise theme and sizing variants. F11 opens the inspector. Set `EEPP_PIXEL_DENSITY=2` to check
+scaling, and use the inspector to vary font size, width, and placement.
+
+## Follow-up scope
+
+Range selection, named timezone support, and time-list popups remain separate follow-ups.
+
+HTML `input` types `date`, `time`, and `datetime-local` now map to these native controls through
+`UIHTMLInput`. HTML values are sanitized and serialized independently of the display locale:
+`yyyy-MM-dd`, `HH:mm[:ss[.SSS]]`, and `yyyy-MM-ddTHH:mm[:ss[.SSS]]`. Local date-time values
+have no timezone. Valid date/time value attributes retain their ISO spelling; local date-time
+values normalize their separator to `T`, omit zero seconds, and trim trailing fractional zeros.
+
+The adapter supports `min`, `max`, `step` (including `any` and fractional seconds), `required`,
+`readonly`, and `disabled`. Range violations remain unchanged and report invalidity; native
+controls retain their default clamping policy. Time ranges can cross midnight. Step validation
+uses the minimum, otherwise the value attribute, otherwise the Unix civil epoch as its base.
+Date steps use days; time and local date-time steps use seconds, defaulting to 60.
+
+Use `UIHTMLInput::getValidity()` / `checkValidity()` and `UIHTMLForm::checkValidity()` /
+`requestSubmit()` for the implemented temporal constraints. Clicking a submit control validates;
+direct `UIHTMLForm::submit()` bypasses validation. Anonymous subparts use the light user-agent
+defaults in `getHTMLDocumentDefaultsCSS()` in `uiwidgetcreator.cpp`, independently of the application
+theme. The HTML
+adapter marks private control content with the existing `UI_IGNORE_GLOBAL_CSS`; stylesheet matching
+keeps the document's low-priority UA rules and excludes author rules for that content. Popups remain
+in the document scene and fit its visible viewport. Date/time editors measure their intrinsic width
+from the displayed text or placeholder plus padding and the calendar button, so author CSS is not
+required to give an input a usable size.
+General HTML validity for other input types, validation messages, and
+`novalidate` / `formnovalidate` are outside this temporal mapping.
diff --git a/docs/articles/ui_html_compatibility.md b/docs/articles/ui_html_compatibility.md
index 8acb664da..b5657b285 100644
--- a/docs/articles/ui_html_compatibility.md
+++ b/docs/articles/ui_html_compatibility.md
@@ -1159,16 +1159,32 @@ Current explicit native mappings include:
button / submit / reset -> UIPushButton
checkbox -> UICheckBox
hidden -> no visible child
+date -> UIDatePicker
+time -> UITimePicker
+datetime-local -> UIDateTimePicker
number -> UISpinBox
password -> password text input
radio -> UIRadioButton
other text-like types -> UIHTMLTextInput
```
+Form controls use light user-agent defaults from `getHTMLDocumentDefaultsCSS()` in
+`uiwidgetcreator.cpp`, independently of the application's native theme. Author CSS styles the
+HTML control host; anonymous implementation parts retain their private defaults. Temporal controls
+have intrinsic text/placeholder sizing, so they remain usable without an authored width.
+
`textarea` uses a `UITextEdit`-based implementation with `rows` and `cols` intrinsic sizing.
Forms collect named control values and can submit navigation requests.
+Temporal inputs sanitize invalid value strings to empty and submit normalized ISO values independently
+of their display locale. They support `min`, `max`, `step`, `required`, `readonly`, and `disabled`.
+Range validation never silently changes the supplied value; reversed time ranges wrap midnight.
+`UIHTMLInput::getValidity()` exposes the implemented temporal validity flags. `UIHTMLForm::requestSubmit()`
+and submit-button activation validate these controls, while direct `submit()` bypasses validation.
+Disabled temporal controls are excluded from form data, and named empty temporal controls submit an
+empty string. See [date/time pickers](ui_date_time_pickers.md) for APIs and the supported subset.
+
An omitted or empty form `action` submits to the full current document URL, preserving its query.
Supported submission encodings include:
diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp
index 2728c7dd1..0b2a72093 100644
--- a/include/eepp/scene/event.hpp
+++ b/include/eepp/scene/event.hpp
@@ -139,6 +139,8 @@ class EE_API Event {
MouseWheel,
OnLinkOpenRequested,
OnFaviconChanged,
+ OnAllowEditingChange,
+ OnFocusedDateChange,
NoEvent = eeINDEX_NOT_FOUND
};
diff --git a/include/eepp/system.hpp b/include/eepp/system.hpp
index ca22be668..bd97924ec 100644
--- a/include/eepp/system.hpp
+++ b/include/eepp/system.hpp
@@ -9,6 +9,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
diff --git a/include/eepp/system/datetime.hpp b/include/eepp/system/datetime.hpp
new file mode 100644
index 000000000..94165b242
--- /dev/null
+++ b/include/eepp/system/datetime.hpp
@@ -0,0 +1,119 @@
+#ifndef EE_SYSTEM_DATETIME_HPP
+#define EE_SYSTEM_DATETIME_HPP
+
+// Required for the comparison categories deduced by defaulted operator<=>.
+#include // IWYU pragma: keep
+#include
+#include
+
+namespace EE { namespace System {
+
+/** Civil Gregorian calendar date without timezone or timestamp semantics.
+ * Arithmetic outside the Int32 year range leaves the value unchanged. */
+struct EE_API CalendarDate {
+ Int32 year{ 1970 };
+ Uint8 month{ 1 };
+ Uint8 day{ 1 };
+
+ bool isValid() const;
+
+ int daysInMonth() const;
+
+ /** Signed number of Gregorian days since 1970-01-01; requires a valid date. */
+ Int64 daysSinceEpoch() const;
+
+ /** Unix seconds at midnight UTC; returns nullopt for an invalid date. */
+ std::optional toUnixTimestamp() const;
+
+ /** UTC date containing the Unix timestamp, including negative timestamps.
+ * Returns nullopt if the date is outside the Int32 year range. */
+ static std::optional fromUnixTimestamp( Int64 seconds );
+
+ CalendarDate addDays( Int64 days ) const;
+
+ CalendarDate addMonths( Int64 months ) const;
+
+ CalendarDate addYears( Int64 years ) const;
+
+ static bool isLeapYear( Int32 year );
+
+ static int daysInMonth( Int32 year, Uint8 month );
+
+ static CalendarDate today();
+
+ auto operator<=>( const CalendarDate& ) const = default;
+};
+
+/** Civil wall-clock time without timezone semantics.
+ * Arithmetic wraps within the day and preserves smaller units. */
+struct EE_API TimeOfDay {
+ Uint8 hour{ 0 };
+ Uint8 minute{ 0 };
+ Uint8 second{ 0 };
+ Uint16 millisecond{ 0 };
+
+ bool isValid() const;
+
+ TimeOfDay addHours( Int64 hours ) const;
+
+ TimeOfDay addMinutes( Int64 minutes ) const;
+
+ TimeOfDay addSeconds( Int64 seconds ) const;
+
+ static TimeOfDay now();
+
+ auto operator<=>( const TimeOfDay& ) const = default;
+};
+
+/** A local civil date and wall-clock time. This value does not imply a timezone. */
+struct EE_API LocalDateTime {
+ CalendarDate date;
+ TimeOfDay time;
+
+ bool isValid() const;
+
+ /** Unix seconds using a UTC offset in seconds (local = UTC + offset), defaulting to UTC.
+ * Discards milliseconds; returns nullopt for an invalid value.
+ * Does not infer the system timezone or resolve daylight-saving transitions. */
+ std::optional toUnixTimestamp( Int32 utcOffsetSeconds = 0 ) const;
+
+ /** Unix milliseconds preserving the millisecond field, with the same offset convention.
+ * Returns nullopt for an invalid value or an Int64 timestamp overflow. */
+ std::optional toUnixTimestampMilliseconds( Int32 utcOffsetSeconds = 0 ) const;
+
+ /** Civil date/time at the given UTC offset (default: UTC), with zero milliseconds.
+ * Returns nullopt if the result is outside the Int32 year range. */
+ static std::optional fromUnixTimestamp( Int64 seconds,
+ Int32 utcOffsetSeconds = 0 );
+
+ /** Civil date/time at the given UTC offset (default: UTC), preserving milliseconds.
+ * Returns nullopt if the result is outside the Int32 year range. */
+ static std::optional fromUnixTimestampMilliseconds( Int64 milliseconds,
+ Int32 utcOffsetSeconds = 0 );
+
+ LocalDateTime addHours( Int64 hours ) const;
+
+ LocalDateTime addMinutes( Int64 minutes ) const;
+
+ LocalDateTime addSeconds( Int64 seconds ) const;
+
+ static LocalDateTime now();
+
+ auto operator<=>( const LocalDateTime& ) const = default;
+};
+
+/** Inclusive range of civil Gregorian dates. */
+struct EE_API DateRange {
+ CalendarDate start;
+ CalendarDate end;
+
+ bool isValid() const;
+
+ bool contains( const CalendarDate& date ) const;
+
+ auto operator<=>( const DateRange& ) const = default;
+};
+
+}} // namespace EE::System
+
+#endif
diff --git a/include/eepp/system/datetimeformat.hpp b/include/eepp/system/datetimeformat.hpp
new file mode 100644
index 000000000..3706195be
--- /dev/null
+++ b/include/eepp/system/datetimeformat.hpp
@@ -0,0 +1,166 @@
+#ifndef EE_SYSTEM_DATETIMEFORMAT_HPP
+#define EE_SYSTEM_DATETIMEFORMAT_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace EE { namespace System {
+
+enum class DateTimeFormatTokenType : Uint8 {
+ Literal,
+ Day,
+ Month,
+ Year,
+ Hour24,
+ Hour12,
+ Minute,
+ Second,
+ Millisecond,
+ AmPm
+};
+
+struct EE_API DateTimeFormatToken {
+ std::string literal;
+ DateTimeFormatTokenType type{ DateTimeFormatTokenType::Literal };
+ Uint8 width{ 0 };
+};
+
+struct EE_API DateTimeFormatTokenRange {
+ DateTimeFormatTokenType type{ DateTimeFormatTokenType::Literal };
+ size_t tokenIndex{ 0 };
+ size_t start{ 0 };
+ size_t length{ 0 };
+};
+
+struct EE_API FormattedDateTime {
+ std::string text;
+ SmallVector ranges;
+};
+
+enum class DateOrder : Uint8 { DMY, MDY, YMD };
+
+enum class HourCycle : Uint8 { Locale, H12, H24 };
+
+/** Resolved locale information used by the date/time controls.
+ *
+ * The standard library is used where it exposes the information. Fields that are not available
+ * portably use one centralized locale-name fallback instead of leaking locale assumptions into UI
+ * code.
+ */
+struct EE_API DateTimeLocale {
+ DateOrder dateOrder{ DateOrder::DMY };
+ char dateSeparator{ '/' };
+ HourCycle hourCycle{ HourCycle::H24 };
+ Uint8 firstDayOfWeek{ 1 }; // 0 = Sunday, 1 = Monday
+ std::string am{ "AM" };
+ std::string pm{ "PM" };
+ std::array shortWeekdayNames{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+ std::array weekdayNames{ "Sunday", "Monday", "Tuesday", "Wednesday",
+ "Thursday", "Friday", "Saturday" };
+ std::array shortMonthNames{ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+ std::array monthNames{ "January", "February", "March", "April",
+ "May", "June", "July", "August",
+ "September", "October", "November", "December" };
+
+ std::string datePattern() const;
+
+ std::string timePattern( bool showSeconds = false, bool showMilliseconds = false,
+ HourCycle requestedCycle = HourCycle::Locale ) const;
+
+ std::string dateTimePattern( bool showSeconds = false, bool showMilliseconds = false,
+ HourCycle requestedCycle = HourCycle::Locale ) const;
+
+ bool operator==( const DateTimeLocale& ) const = default;
+
+ static DateTimeLocale system();
+
+ static DateTimeLocale fromLocaleName( std::string_view localeName );
+};
+
+class EE_API DateTimeFormat {
+ public:
+ DateTimeFormat() = default;
+
+ explicit DateTimeFormat( std::string pattern );
+
+ static DateTimeFormat compile( std::string pattern );
+
+ bool isValid() const { return mValid; }
+
+ const std::string& getPattern() const { return mPattern; }
+
+ const std::vector& getTokens() const { return mTokens; }
+
+ protected:
+ std::string mPattern;
+ std::vector mTokens;
+ bool mValid{ false };
+};
+
+class EE_API DateTimeFormatter {
+ public:
+ static FormattedDateTime
+ formatDateDetailed( const CalendarDate& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static FormattedDateTime
+ formatTimeDetailed( const TimeOfDay& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static FormattedDateTime
+ formatDateTimeDetailed( const LocalDateTime& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::string formatDate( const CalendarDate& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::string formatTime( const TimeOfDay& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::string formatDateTime( const LocalDateTime& value, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::optional
+ parseDate( std::string_view text, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::optional
+ parseTime( std::string_view text, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::optional
+ parseDateTime( std::string_view text, const DateTimeFormat& format,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ /** Parsing intended for paste/replacement. ISO is always accepted, then the display format and
+ * separator-tolerant numeric forms are attempted. */
+ static std::optional
+ parseDateTolerant( std::string_view text, const DateTimeFormat& displayFormat,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::optional
+ parseTimeTolerant( std::string_view text, const DateTimeFormat& displayFormat,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::optional
+ parseDateTimeTolerant( std::string_view text, const DateTimeFormat& displayFormat,
+ const DateTimeLocale& locale = DateTimeLocale::system() );
+
+ static std::string toISODate( const CalendarDate& value );
+
+ static std::string toISOTime( const TimeOfDay& value, bool includeMilliseconds = false );
+
+ static std::string toISOLocalDateTime( const LocalDateTime& value,
+ bool includeMilliseconds = false );
+};
+
+}} // namespace EE::System
+
+#endif
diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp
index 194a8c574..984c756b1 100644
--- a/include/eepp/ui.hpp
+++ b/include/eepp/ui.hpp
@@ -104,11 +104,15 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
+#include
#include
#include
#include
@@ -193,6 +197,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/eepp/ui/css/idnamemap.hpp b/include/eepp/ui/css/idnamemap.hpp
index c3c254750..197eccd85 100644
--- a/include/eepp/ui/css/idnamemap.hpp
+++ b/include/eepp/ui/css/idnamemap.hpp
@@ -3,6 +3,7 @@
#include
#include
+#include
#include
#include
#include
@@ -39,34 +40,39 @@ template class IdNameMap {
*
* @return true on success.
*/
- bool addBuiltin( Id id, const std::string& canonicalName ) {
+ bool addBuiltin( Id id, std::string_view canonicalName ) {
if ( mBuiltinsFinalized ) {
EE::System::Log::error(
- "IdNameMap: cannot register built-in \"%s\" after finalization.",
- canonicalName.c_str() );
+ "IdNameMap: cannot register built-in \"%.*s\" after finalization.",
+ static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return false;
}
const auto underlying = static_cast>( id );
const auto maxUnderlying = static_cast>( MaxIds );
if ( underlying == 0 ) {
- EE::System::Log::error( "IdNameMap: cannot register Invalid built-in \"%s\".",
- canonicalName.c_str() );
+ EE::System::Log::error( "IdNameMap: cannot register Invalid built-in \"%.*s\".",
+ static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return false;
}
if ( underlying >= maxUnderlying ) {
- EE::System::Log::error( "IdNameMap: built-in ID %d for \"%s\" is out of range.",
- underlying, canonicalName.c_str() );
+ EE::System::Log::error( "IdNameMap: built-in ID %d for \"%.*s\" is out of range.",
+ underlying, static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return false;
}
if ( mIdsByName.find( canonicalName ) != mIdsByName.end() ) {
- EE::System::Log::error( "IdNameMap: duplicate built-in name \"%s\".",
- canonicalName.c_str() );
+ EE::System::Log::error( "IdNameMap: duplicate built-in name \"%.*s\".",
+ static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return false;
}
if ( underlying < mNames.size() && !mNames[underlying].empty() ) {
EE::System::Log::error(
- "IdNameMap: built-in ID %d already bound to \"%s\", not \"%s\".", underlying,
- mNames[underlying].c_str(), canonicalName.c_str() );
+ "IdNameMap: built-in ID %d already bound to \"%s\", not \"%.*s\".", underlying,
+ mNames[underlying].c_str(), static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return false;
}
if ( mNames.size() <= underlying )
@@ -137,7 +143,7 @@ template class IdNameMap {
* @return Invalid when the name is unknown.
*/
Id getId( std::string_view name ) const {
- const auto it = mIdsByName.find( std::string( name ) );
+ const auto it = mIdsByName.find( name );
return it != mIdsByName.end() ? it->second : static_cast( 0 );
}
@@ -166,11 +172,12 @@ template class IdNameMap {
* Only valid after finalizeBuiltins(). Reaching the capacity returns Invalid
* and logs a clear error containing the rejected name.
*/
- Id getOrCreateId( const std::string& canonicalName ) {
+ Id getOrCreateId( std::string_view canonicalName ) {
if ( !mBuiltinsFinalized ) {
EE::System::Log::error(
- "IdNameMap: getOrCreateId(\"%s\") called before finalizeBuiltins().",
- canonicalName.c_str() );
+ "IdNameMap: getOrCreateId(\"%.*s\") called before finalizeBuiltins().",
+ static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return static_cast( 0 );
}
const auto existing = getId( canonicalName );
@@ -178,8 +185,10 @@ template class IdNameMap {
return existing;
const auto next = static_cast>( mNextCustomId );
if ( next >= static_cast>( MaxIds ) ) {
- EE::System::Log::error( "IdNameMap: ID capacity exhausted (max %zu), rejecting \"%s\".",
- MaxIds, canonicalName.c_str() );
+ EE::System::Log::error(
+ "IdNameMap: ID capacity exhausted (max %zu), rejecting \"%.*s\".", MaxIds,
+ static_cast( canonicalName.size() ),
+ canonicalName.empty() ? "" : canonicalName.data() );
return static_cast( 0 );
}
if ( mNames.size() <= next )
@@ -196,7 +205,7 @@ template class IdNameMap {
}
bool contains( std::string_view name ) const {
- return mIdsByName.find( std::string( name ) ) != mIdsByName.end();
+ return mIdsByName.find( name ) != mIdsByName.end();
}
bool contains( const std::string& name ) const {
@@ -209,7 +218,12 @@ template class IdNameMap {
private:
std::vector mNames;
- UnorderedMap mIdsByName;
+ // Preserve the selected container's string-view hash (including its avalanche marker),
+ // and allow borrowed names to find owned string keys without temporary allocations.
+ struct NameHash : UnorderedMap::hasher {
+ using is_transparent = void;
+ };
+ UnorderedMap> mIdsByName;
Id mNextCustomId;
bool mBuiltinsFinalized{ false };
};
diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp
index a3039e810..90acba8d3 100644
--- a/include/eepp/ui/css/propertydefinition.hpp
+++ b/include/eepp/ui/css/propertydefinition.hpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
namespace EE { namespace UI { namespace CSS {
@@ -44,8 +45,8 @@ enum class PropertyRelativeTarget : Uint32 {
class EE_API PropertyDefinition {
public:
- PropertyDefinition( PropertyId propertyId, const std::string& name,
- const std::string& defaultValue, const bool& inherited = false );
+ PropertyDefinition( PropertyId propertyId, std::string_view name, std::string_view defaultValue,
+ const bool& inherited = false );
const std::string& getName() const;
diff --git a/include/eepp/ui/css/propertyids.hpp b/include/eepp/ui/css/propertyids.hpp
index e4114631c..3eda81c3f 100644
--- a/include/eepp/ui/css/propertyids.hpp
+++ b/include/eepp/ui/css/propertyids.hpp
@@ -330,6 +330,35 @@ enum class PropertyId : Uint16 {
JustifyItems,
JustifySelf,
Defer,
+ DateTimeMode,
+ AllowEmpty,
+ DateFormat,
+ TimeFormat,
+ HourCycle,
+ ShowSeconds,
+ ShowMilliseconds,
+ WheelEditing,
+ HourStep,
+ MinuteStep,
+ SecondStep,
+ MinDate,
+ MaxDate,
+ MinTime,
+ MaxTime,
+ MinDateTime,
+ MaxDateTime,
+ FirstDayOfWeek,
+ CalendarView,
+ DisplayedMonth,
+ FocusedDate,
+ ActiveSection,
+ PopUpOpen,
+ Min,
+ Max,
+ Step,
+ Required,
+ ReadOnly,
+ Disabled,
NumDefinedIds,
FirstCustomId = NumDefinedIds,
MaxNumIds = 512,
diff --git a/include/eepp/ui/css/propertyspecification.hpp b/include/eepp/ui/css/propertyspecification.hpp
index 1c567dc66..ba75f8e66 100644
--- a/include/eepp/ui/css/propertyspecification.hpp
+++ b/include/eepp/ui/css/propertyspecification.hpp
@@ -16,11 +16,11 @@ class EE_API PropertySpecification {
public:
~PropertySpecification();
- PropertyDefinition& registerProperty( PropertyId id, const std::string& propertyName,
- const std::string& defaultValue, bool inherited );
+ PropertyDefinition& registerProperty( PropertyId id, std::string_view propertyName,
+ std::string_view defaultValue, bool inherited );
- PropertyDefinition* registerProperty( const std::string& propertyName,
- const std::string& defaultValue, bool inherited );
+ PropertyDefinition* registerProperty( std::string_view propertyName,
+ std::string_view defaultValue, bool inherited );
const PropertyDefinition* getProperty( const PropertyId& id ) const;
diff --git a/include/eepp/ui/css/stylesheetselector.hpp b/include/eepp/ui/css/stylesheetselector.hpp
index 8638a6817..2d9d4f89d 100644
--- a/include/eepp/ui/css/stylesheetselector.hpp
+++ b/include/eepp/ui/css/stylesheetselector.hpp
@@ -15,9 +15,9 @@ class EE_API StyleSheetSelector {
explicit StyleSheetSelector( const std::string& selectorName );
- const std::string& getName() const;
+ inline const std::string& getName() const { return mName; }
- const Int64& getSpecificity() const;
+ inline const Int64& getSpecificity() const { return mSpecificity; }
void setSpecificity( const Int64& specificity );
@@ -27,7 +27,8 @@ class EE_API StyleSheetSelector {
bool hasPseudoClasses() const;
- SmallVector getRelatedElements( UIWidget* element, bool applyPseudo = true ) const;
+ SmallVector getRelatedElements( UIWidget* element,
+ bool applyPseudo = true ) const;
bool isStructurallyVolatile() const;
diff --git a/include/eepp/ui/css/stylesheetspecification.hpp b/include/eepp/ui/css/stylesheetspecification.hpp
index bc5fe97de..f56b1dbb8 100644
--- a/include/eepp/ui/css/stylesheetspecification.hpp
+++ b/include/eepp/ui/css/stylesheetspecification.hpp
@@ -38,11 +38,11 @@ class EE_API StyleSheetSpecification {
~StyleSheetSpecification();
- PropertyDefinition& registerProperty( PropertyId id, const std::string& propertyName,
- const std::string& defaultValue, bool inherited = false );
+ PropertyDefinition& registerProperty( PropertyId id, std::string_view propertyName,
+ std::string_view defaultValue, bool inherited = false );
- PropertyDefinition* registerProperty( const std::string& propertyName,
- const std::string& defaultValue, bool inherited = false );
+ PropertyDefinition* registerProperty( std::string_view propertyName,
+ std::string_view defaultValue, bool inherited = false );
const PropertyDefinition* getProperty( const PropertyId& id ) const;
diff --git a/include/eepp/ui/css/stylesheetstyle.hpp b/include/eepp/ui/css/stylesheetstyle.hpp
index b19a1da0f..7674a114c 100644
--- a/include/eepp/ui/css/stylesheetstyle.hpp
+++ b/include/eepp/ui/css/stylesheetstyle.hpp
@@ -21,11 +21,11 @@ class EE_API StyleSheetStyle {
std::string build( bool emitMediaQueryStart = true, bool emitMediaQueryEnd = true );
- const StyleSheetSelector& getSelector() const;
+ inline const StyleSheetSelector& getSelector() const { return mSelector; }
- const StyleSheetProperties& getProperties() const;
+ inline const StyleSheetProperties& getProperties() const { return mProperties; }
- StyleSheetProperties& getPropertiesRef();
+ inline StyleSheetProperties& getPropertiesRef() { return mProperties; }
const StyleSheetVariables& getVariables() const;
diff --git a/include/eepp/ui/uicalendar.hpp b/include/eepp/ui/uicalendar.hpp
new file mode 100644
index 000000000..01c7e140a
--- /dev/null
+++ b/include/eepp/ui/uicalendar.hpp
@@ -0,0 +1,169 @@
+#ifndef EE_UI_UICALENDAR_HPP
+#define EE_UI_UICALENDAR_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+using namespace EE::System;
+using namespace EE::UI::CSS;
+
+class UILinearLayout;
+class UIGridLayout;
+class UIPushButton;
+class UITextView;
+
+enum class CalendarView : Uint8 { Days, Months, Years };
+
+/** Standalone Gregorian calendar widget used by the date picker family.
+ *
+ * Display navigation is intentionally independent from the selected value. Navigating months or
+ * years never changes the selection until a date is explicitly selected.
+ */
+class EE_API UICalendar : public UIWidget {
+ public:
+ static UICalendar* New();
+
+ virtual ~UICalendar() = default;
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ UICalendar* setSelectedDate( const CalendarDate& date );
+
+ UICalendar* setSelectedDate( std::optional date );
+
+ const std::optional& getSelectedDate() const;
+
+ UICalendar* clearSelection();
+
+ UICalendar* setDisplayedMonth( const CalendarDate& date );
+
+ const CalendarDate& getDisplayedMonth() const;
+
+ UICalendar* setFocusedDate( const CalendarDate& date );
+
+ const CalendarDate& getFocusedDate() const;
+
+ UICalendar* setMinDate( std::optional date );
+
+ UICalendar* setMaxDate( std::optional date );
+
+ const std::optional& getMinDate() const;
+
+ const std::optional& getMaxDate() const;
+
+ UICalendar* setView( CalendarView view );
+
+ CalendarView getView() const;
+
+ UICalendar* setLocale( const DateTimeLocale& locale );
+
+ const DateTimeLocale& getLocale() const;
+
+ UICalendar* setFirstDayOfWeek( Uint8 weekday );
+
+ Uint8 getFirstDayOfWeek() const;
+
+ UICalendar* previous();
+
+ UICalendar* next();
+
+ UICalendar* goToToday();
+
+ bool selectFocusedDate();
+
+ /** First date rendered by the fixed 42-cell day grid. Useful for tests/inspection. */
+ CalendarDate getFirstVisibleDate() const;
+
+ /** Returns the date represented by one day cell (0..41). */
+ CalendarDate getDayCellDate( size_t index ) const;
+
+ size_t getDayCellCount() const { return mDayCells.size(); }
+
+ virtual bool applyProperty( const StyleSheetProperty& attribute ) override;
+
+ virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex = 0 ) const override;
+
+ virtual std::vector getPropertiesImplemented() const override;
+
+ protected:
+ UICalendar();
+
+ virtual Uint32 onKeyDown( const KeyEvent& event ) override;
+
+ virtual void onSizeChange() override;
+
+ virtual void onThemeLoaded() override;
+
+ virtual void onAutoSize() override;
+
+ void buildUI();
+
+ void refresh();
+
+ void refreshHeader();
+
+ void refreshDays();
+
+ void refreshMonths();
+
+ void refreshYears();
+
+ void updateCellClasses( UIPushButton* button, bool selected, bool today, bool outside,
+ bool focused );
+
+ bool isDateEnabled( const CalendarDate& date ) const;
+
+ CalendarDate clampDate( CalendarDate date ) const;
+
+ void moveFocusedDate( Int64 days );
+
+ void setFocusedAndDisplay( const CalendarDate& date );
+
+ void handleDayClicked( size_t index );
+
+ void handleOptionClicked( size_t index );
+
+ void updateLayoutSize();
+
+ static int dayOfWeek( const CalendarDate& date ); // Sunday=0
+
+ static const char* viewToString( CalendarView view );
+
+ DateTimeLocale mLocale;
+ CalendarDate mDisplayedMonth;
+ CalendarDate mFocusedDate;
+ std::optional mSelectedDate;
+ std::optional mMinDate;
+ std::optional mMaxDate;
+ Int64 mYearPageStart{ 0 };
+ CalendarView mView{ CalendarView::Days };
+ bool mUpdatingLayout{ false };
+
+ UILinearLayout* mMainLayout{ nullptr };
+ UILinearLayout* mHeader{ nullptr };
+ UIPushButton* mPreviousButton{ nullptr };
+ UIPushButton* mTitleButton{ nullptr };
+ UIPushButton* mNextButton{ nullptr };
+ UIGridLayout* mWeekdayRow{ nullptr };
+ UIGridLayout* mDayGrid{ nullptr };
+ UIGridLayout* mOptionGrid{ nullptr };
+ UIPushButton* mTodayButton{ nullptr };
+ std::array mWeekdayLabels{};
+ std::array mDayCells{};
+ std::array mDayCellDates{};
+ std::array mOptionCells{};
+ EventConnectionList mConnections;
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uidatepicker.hpp b/include/eepp/ui/uidatepicker.hpp
new file mode 100644
index 000000000..dbf7e2401
--- /dev/null
+++ b/include/eepp/ui/uidatepicker.hpp
@@ -0,0 +1,98 @@
+#ifndef EE_UI_UIDATEPICKER_HPP
+#define EE_UI_UIDATEPICKER_HPP
+
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+using namespace EE::System;
+using namespace EE::UI::CSS;
+
+class UICalendar;
+class UIPushButton;
+
+/** Segmented date editor with a lazily created calendar popup. Opening the popup never assigns
+ * a value. Alt+Down or F4 opens it; Escape closes it and restores editor focus. */
+class EE_API UIDatePicker : public UIDateTimeEdit {
+ public:
+ static UIDatePicker* New();
+
+ virtual ~UIDatePicker();
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ virtual Float getMinIntrinsicWidth() const override;
+
+ virtual UIDatePicker* showCalendar();
+
+ virtual UIDatePicker* hideCalendar( bool restoreFocus = true );
+
+ bool isCalendarVisible() const;
+
+ /** Returns nullptr until the first opening. The picker owns the calendar, even while it is
+ * parented to the window or scene root. */
+ UICalendar* getCalendar() const;
+
+ UIPushButton* getCalendarButton() const;
+
+ virtual bool applyProperty( const StyleSheetProperty& attribute ) override;
+
+ virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex = 0 ) const override;
+
+ virtual std::vector getPropertiesImplemented() const override;
+
+ protected:
+ UIDatePicker();
+
+ UIDatePicker( DateTimeEditMode mode, const std::string& tag );
+
+ virtual Uint32 onKeyDown( const KeyEvent& event ) override;
+
+ virtual void onSizeChange() override;
+
+ virtual void onPaddingChange() override;
+
+ virtual void onPositionChange() override;
+
+ virtual void onVisibilityChange() override;
+
+ virtual void onEnabledChange() override;
+
+ virtual void draw() override;
+
+ void updateButton();
+
+ void closeOnFocusLoss();
+
+ virtual void selectCalendarDate();
+
+ virtual UIWidget* getCalendarPopup() const;
+
+ virtual void prepareCalendarPopup();
+
+ virtual void onConstraintsChange() override;
+
+ virtual void onConfigurationChange() override;
+
+ virtual void onCalendarFocusedDateChange();
+
+ EventConnection mSceneSizeConnection;
+ UICalendar* mCalendar{ nullptr };
+ UIPushButton* mCalendarButton{ nullptr };
+ Float mEditorRightPadding{ 0 };
+ Float mReservedButtonWidth{ 0 };
+ bool mUpdatingButton{ false };
+ bool mCalendarVisible{ false };
+ bool mPopUpToRoot{ false };
+ std::array mConnections;
+ std::array mCalendarConnections;
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uidatetimeedit.hpp b/include/eepp/ui/uidatetimeedit.hpp
new file mode 100644
index 000000000..69f693b4e
--- /dev/null
+++ b/include/eepp/ui/uidatetimeedit.hpp
@@ -0,0 +1,301 @@
+#ifndef EE_UI_UIDATETIMEEDIT_HPP
+#define EE_UI_UIDATETIMEEDIT_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+using namespace EE::System;
+using namespace EE::UI::CSS;
+
+enum class DateTimeEditMode : Uint8 { Date, Time, DateTime };
+
+enum class DateTimeSection : Uint8 {
+ None,
+ Day,
+ Month,
+ Year,
+ Hour,
+ Minute,
+ Second,
+ Millisecond,
+ AmPm
+};
+
+class EE_API UIDateTimeEdit : public UITextInput {
+ public:
+ static UIDateTimeEdit* New();
+
+ static UIDateTimeEdit* New( DateTimeEditMode mode );
+
+ virtual ~UIDateTimeEdit() = default;
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ virtual Float getMinIntrinsicWidth() const override;
+
+ virtual Float getMaxIntrinsicWidth() const override;
+
+ UIDateTimeEdit* setEditMode( DateTimeEditMode mode );
+
+ DateTimeEditMode getEditMode() const;
+
+ UIDateTimeEdit* setDate( const CalendarDate& date );
+
+ UIDateTimeEdit* setDate( std::optional date );
+
+ const std::optional& getDate() const;
+
+ UIDateTimeEdit* setTime( const TimeOfDay& time );
+
+ UIDateTimeEdit* setTime( std::optional time );
+
+ const std::optional& getTime() const;
+
+ UIDateTimeEdit* setDateTime( const LocalDateTime& dateTime );
+
+ UIDateTimeEdit* setDateTime( std::optional dateTime );
+
+ const std::optional& getDateTime() const;
+
+ bool hasValue() const;
+
+ UIDateTimeEdit* clear();
+
+ UIDateTimeEdit* setAllowEmpty( bool allow );
+
+ bool getAllowEmpty() const;
+
+ UIDateTimeEdit* setLocale( const DateTimeLocale& locale );
+
+ const DateTimeLocale& getLocale() const;
+
+ UIDateTimeEdit* setDateFormat( const std::string& pattern );
+
+ UIDateTimeEdit* setTimeFormat( const std::string& pattern );
+
+ const std::string& getDateFormat() const;
+
+ const std::string& getTimeFormat() const;
+
+ const DateTimeFormat& getDisplayFormat() const;
+
+ UIDateTimeEdit* setHourCycle( HourCycle cycle );
+
+ HourCycle getHourCycle() const;
+
+ /** Shows seconds in the locale format (off by default). Hiding also hides milliseconds. */
+ UIDateTimeEdit* setShowSeconds( bool show );
+
+ bool getShowSeconds() const;
+
+ /** Shows milliseconds in the locale format (off by default). Enabling also shows seconds;
+ * disabling leaves seconds visible. Hidden precision is preserved in the value. */
+ UIDateTimeEdit* setShowMilliseconds( bool show );
+
+ bool getShowMilliseconds() const;
+
+ UIDateTimeEdit* setDayStep( Uint32 step );
+
+ Uint32 getDayStep() const;
+
+ /** Native controls clamp to bounds by default. HTML adapters disable clamping so range
+ * constraints report invalid values instead of silently changing them. */
+ UIDateTimeEdit* setClampToBounds( bool clamp );
+
+ bool getClampToBounds() const;
+
+ UIDateTimeEdit* setHourStep( Uint32 step );
+
+ UIDateTimeEdit* setMinuteStep( Uint32 step );
+
+ UIDateTimeEdit* setSecondStep( Uint32 step );
+
+ Uint32 getHourStep() const;
+
+ Uint32 getMinuteStep() const;
+
+ Uint32 getSecondStep() const;
+
+ UIDateTimeEdit* setMinDate( std::optional date );
+
+ UIDateTimeEdit* setMaxDate( std::optional date );
+
+ const std::optional& getMinDate() const;
+
+ const std::optional& getMaxDate() const;
+
+ UIDateTimeEdit* setMinTime( std::optional time );
+
+ UIDateTimeEdit* setMaxTime( std::optional time );
+
+ const std::optional& getMinTime() const;
+
+ const std::optional& getMaxTime() const;
+
+ UIDateTimeEdit* setMinDateTime( std::optional dateTime );
+
+ UIDateTimeEdit* setMaxDateTime( std::optional dateTime );
+
+ const std::optional& getMinDateTime() const;
+
+ const std::optional& getMaxDateTime() const;
+
+ UIDateTimeEdit* setWheelEditingEnabled( bool enabled );
+
+ bool isWheelEditingEnabled() const;
+
+ UIDateTimeEdit* setPendingDigitTimeout( const Time& timeout );
+
+ const Time& getPendingDigitTimeout() const;
+
+ DateTimeSection getActiveSection() const;
+
+ UIDateTimeEdit* setActiveSection( DateTimeSection section );
+
+ bool stepActiveSection( Int32 direction );
+
+ std::string getSerializedValue() const;
+
+ virtual UITextView* setText( const String& text ) override;
+
+ virtual bool applyProperty( const StyleSheetProperty& attribute ) override;
+
+ virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex = 0 ) const override;
+
+ virtual std::vector getPropertiesImplemented() const override;
+
+ virtual void scheduledUpdate( const Time& time ) override;
+
+ protected:
+ virtual void onAutoSize() override;
+
+ struct Section {
+ size_t start{ 0 };
+ size_t length{ 0 };
+ Int32 minValue{ 0 };
+ Int32 maxValue{ 0 };
+ Uint32 displayDigits{ 0 };
+ DateTimeSection type{ DateTimeSection::None };
+ };
+
+ UIDateTimeEdit();
+
+ explicit UIDateTimeEdit( DateTimeEditMode mode, const std::string& tag = "datetimeedit" );
+
+ virtual Uint32 onKeyDown( const KeyEvent& event ) override;
+
+ virtual Uint32 onTextInput( const TextInputEvent& event ) override;
+
+ virtual Uint32 onMouseClick( const Vector2i& position, const Uint32& flags ) override;
+
+ virtual Uint32 onMouseWheel( const Vector2f& offset, bool flipped ) override;
+
+ virtual void onTextChanged() override;
+
+ virtual void onDocumentTextChanged( const DocumentContentChange& change ) override;
+
+ virtual Uint32 onFocus( NodeFocusReason reason ) override;
+
+ void pasteValue();
+
+ virtual Uint32 onFocusLoss() override;
+
+ void rebuildFormat();
+
+ void updateTextFromValue( bool preserveSection = true );
+
+ void rebuildSections( const FormattedDateTime& formatted );
+
+ void updateAutoHint();
+
+ void selectActiveSection();
+
+ void selectSectionAtColumn( Int64 column );
+
+ void moveActiveSection( Int32 direction );
+
+ void updatePendingText();
+
+ void resetPendingDigits();
+
+ bool commitPendingDigits( bool advance );
+
+ bool processDigit( char digit );
+
+ bool processAmPm( char value );
+
+ bool setSectionNumericValue( DateTimeSection section, Int32 value );
+
+ std::optional findSection( DateTimeSection section ) const;
+
+ SmallVector getSectionOrder() const;
+
+ bool parseAndSetDisplayText( std::string_view text );
+
+ bool parseAndSetSerializedValue( std::string_view text );
+
+ void initializeValueForEditing();
+
+ void clampCurrentValue();
+
+ virtual void onConstraintsChange();
+
+ virtual void onConfigurationChange();
+
+ static DateTimeSection tokenToSection( DateTimeFormatTokenType type );
+
+ static const char* modeToString( DateTimeEditMode mode );
+
+ static std::optional modeFromString( std::string_view mode );
+
+ static const char* hourCycleToString( HourCycle cycle );
+
+ DateTimeEditMode mEditMode{ DateTimeEditMode::Date };
+ std::optional mDate;
+ std::optional mTime;
+ std::optional mDateTime;
+ std::optional mMinDate;
+ std::optional mMaxDate;
+ std::optional mMinTime;
+ std::optional mMaxTime;
+ std::optional mMinDateTime;
+ std::optional mMaxDateTime;
+ DateTimeLocale mLocale;
+ DateTimeFormat mDisplayFormat;
+ std::string mDateFormat;
+ std::string mTimeFormat;
+ std::string mLastAutoHint;
+ HourCycle mHourCycle{ HourCycle::Locale };
+ bool mShowSeconds{ false };
+ bool mShowMilliseconds{ false };
+ bool mAllowEmpty{ true };
+ bool mWheelEditing{ false };
+ bool mUpdatingText{ false };
+ bool mClampToBounds{ true };
+ Uint32 mDayStep{ 1 };
+ Uint32 mHourStep{ 1 };
+ Uint32 mMinuteStep{ 1 };
+ Uint32 mSecondStep{ 1 };
+ DateTimeSection mActiveSection{ DateTimeSection::None };
+ SmallVector mSections;
+ std::string mPendingDigits;
+ Clock mPendingDigitClock;
+
+ Time mPendingDigitTimeout{ Milliseconds( 1000 ) };
+ EventConnection mAllowEditingConnection;
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uidatetimepicker.hpp b/include/eepp/ui/uidatetimepicker.hpp
new file mode 100644
index 000000000..5157fb6f2
--- /dev/null
+++ b/include/eepp/ui/uidatetimepicker.hpp
@@ -0,0 +1,57 @@
+#ifndef EE_UI_UIDATETIMEPICKER_HPP
+#define EE_UI_UIDATETIMEPICKER_HPP
+
+#include
+
+namespace EE { namespace UI {
+
+class UITimePicker;
+
+/** One segmented local date/time field with a calendar and time editor popup. Date selection
+ * preserves the time and keeps the popup open. Changes commit immediately; Enter in the time
+ * editor or Escape closes it. Opening an empty picker does not assign a value. */
+class EE_API UIDateTimePicker : public UIDatePicker {
+ public:
+ static UIDateTimePicker* New();
+
+ virtual ~UIDateTimePicker();
+
+ virtual UIDateTimePicker* hideCalendar( bool restoreFocus = true ) override;
+
+ /** Returns nullptr until the first popup opening. The popup owns the time editor. */
+ UITimePicker* getPopupTimePicker() const;
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ protected:
+ UIDateTimePicker();
+
+ virtual UIWidget* getCalendarPopup() const override;
+
+ virtual void prepareCalendarPopup() override;
+
+ virtual void selectCalendarDate() override;
+
+ virtual void onConstraintsChange() override;
+
+ virtual void onConfigurationChange() override;
+
+ virtual void onCalendarFocusedDateChange() override;
+
+ void synchronizeTimePopup();
+
+ void selectPopupTime();
+
+ UIWidget* mPopup{ nullptr };
+ UITimePicker* mPopupTimePicker{ nullptr };
+ bool mSynchronizingPopup{ false };
+ std::array mConnections;
+ std::array mPopupConnections;
+ std::array mTimeConnections;
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp
index 8612543f0..2e1e11e3f 100644
--- a/include/eepp/ui/uihelper.hpp
+++ b/include/eepp/ui/uihelper.hpp
@@ -143,6 +143,11 @@ enum UINodeType {
UI_TYPE_TEXTNODE,
UI_TYPE_FONTPICKERDIALOG,
UI_TYPE_SCROLLABLEMARKDOWNVIEW,
+ UI_TYPE_DATETIMEEDIT,
+ UI_TYPE_CALENDAR,
+ UI_TYPE_DATEPICKER,
+ UI_TYPE_TIMEPICKER,
+ UI_TYPE_DATETIMEPICKER,
UI_TYPE_MODULES = 10000,
UI_TYPE_TERMINAL = 10001,
UI_TYPE_USER = 200000,
diff --git a/include/eepp/ui/uihtmlform.hpp b/include/eepp/ui/uihtmlform.hpp
index a172b0d92..5e568055f 100644
--- a/include/eepp/ui/uihtmlform.hpp
+++ b/include/eepp/ui/uihtmlform.hpp
@@ -28,15 +28,24 @@ class EE_API UIHTMLForm : public UIRichText {
virtual std::vector getPropertiesImplemented() const;
+ /** Direct submission bypasses validation, as HTMLFormElement.submit() does. */
void submit();
+ /** Validates implemented input constraints before submitting. */
+ bool requestSubmit();
+
+ bool checkValidity() const;
+
const std::string& getAction() const { return mAction; }
+
void setAction( const std::string& action ) { mAction = action; }
const std::string& getMethod() const { return mMethod; }
+
void setMethod( const std::string& method ) { mMethod = method; }
const std::string& getEnctype() const { return mEnctype; }
+
void setEnctype( const std::string& enctype ) { mEnctype = enctype; }
protected:
@@ -47,7 +56,7 @@ class EE_API UIHTMLForm : public UIRichText {
virtual Uint32 onMessage( const NodeMessage* msg );
static void collectFormData( Node* node,
- std::vector>& fields );
+ std::vector>& fields );
bool isSubmitTrigger( Node* sender ) const;
};
diff --git a/include/eepp/ui/uihtmlinput.hpp b/include/eepp/ui/uihtmlinput.hpp
index 3233d1e01..bb866d446 100644
--- a/include/eepp/ui/uihtmlinput.hpp
+++ b/include/eepp/ui/uihtmlinput.hpp
@@ -1,11 +1,29 @@
#ifndef EE_UI_UIHTMLINPUT_HPP
#define EE_UI_UIHTMLINPUT_HPP
+#include
+#include
#include
#include
+#include
namespace EE { namespace UI {
+using namespace EE::System;
+
+/** Constraint flags for the implemented HTML temporal input states. */
+struct HTMLInputValidity {
+ bool valueMissing{ false };
+ bool rangeUnderflow{ false };
+ bool rangeOverflow{ false };
+ bool stepMismatch{ false };
+ bool badInput{ false };
+
+ bool valid() const {
+ return !( valueMissing || rangeUnderflow || rangeOverflow || stepMismatch || badInput );
+ }
+};
+
class EE_API UIHTMLInput : public UIHTMLWidget {
public:
static UIHTMLInput* New();
@@ -37,11 +55,21 @@ class EE_API UIHTMLInput : public UIHTMLWidget {
UIWidget* getChildWidget() const;
- String getFormValue() const;
+ virtual String getFormValue() const;
+
+ /** HTML constraint validation for date, time and datetime-local inputs. Read-only and
+ * disabled controls are barred from validation. */
+ HTMLInputValidity getValidity() const;
+
+ bool willValidate() const;
+
+ bool checkValidity() const;
protected:
std::string mInputType{ "text" };
UIWidget* mChildWidget{ nullptr };
+ EventConnection mControlChildrenConnection;
+ EventConnection mPopupTimeChildrenConnection;
std::map mImplementationProperties;
String mValue;
bool mChecked{ false };
@@ -53,15 +81,33 @@ class EE_API UIHTMLInput : public UIHTMLWidget {
CSSDisplay mDisplayBeforeHidden{ CSSDisplay::InlineBlock };
void createChildWidget();
+
void configureChildWidget();
+
void applyImplementationProperty( const StyleSheetProperty& property );
+
void syncImplementationState();
+
void syncStateFromImplementation();
+
void updateHostGeometry();
+
void updateChildGeometry();
+
void syncCheckedState();
+ bool isTemporalInput() const;
+
+ void setTemporalValue( const String& value );
+
+ void syncTemporalConfiguration();
+
+ const std::string& temporalAttribute( PropertyId id ) const;
+
+ std::optional getTemporalValue() const;
+
virtual void onSizeChange();
+
virtual void onPaddingChange();
};
diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp
index bc39c3a02..c3f639299 100644
--- a/include/eepp/ui/uinode.hpp
+++ b/include/eepp/ui/uinode.hpp
@@ -765,7 +765,7 @@ class EE_API UINode : public Node {
*
* @return The flags as a Uint64 bitmask.
*/
- const Uint64& getFlags() const;
+ inline const Uint64& getFlags() const { return mFlags; }
/**
* @brief Sets multiple flags on the node.
diff --git a/include/eepp/ui/uipopup.hpp b/include/eepp/ui/uipopup.hpp
new file mode 100644
index 000000000..8e3b829a4
--- /dev/null
+++ b/include/eepp/ui/uipopup.hpp
@@ -0,0 +1,44 @@
+#ifndef EE_UI_UIPOPUP_HPP
+#define EE_UI_UIPOPUP_HPP
+
+#include
+#include
+
+namespace EE { namespace Scene {
+class Node;
+}} // namespace EE::Scene
+
+namespace EE { namespace UI {
+
+using namespace EE::Math;
+using namespace EE::Scene;
+
+class UIWidget;
+
+/** Shared popup placement and visibility helpers for anchored UI controls.
+ *
+ * The helper deliberately owns no widget lifetime or selection semantics. Callers decide when a
+ * popup should close and where focus returns; this only centralizes root/window parenting,
+ * below/above placement, scene-bound clamping and the standard eepp fade effects.
+ */
+class EE_API UIPopUp {
+ public:
+ /** Returns a world-pixel position without reparenting or moving the popup. With an anchor,
+ * prefer below then above; trySides also permits right/left placement for trigger menus.
+ * Without an anchor, try the four quadrants around position for cursor menus. */
+ static Vector2f findBestPosition( UIWidget* popup, const Vector2f& position,
+ Node* anchor = nullptr, bool trySides = false );
+
+ static void align( UIWidget* anchor, UIWidget* popup, bool popUpToRoot = false,
+ bool centered = false );
+
+ static bool hasFocus( UIWidget* anchor, UIWidget* popup );
+
+ static void show( UIWidget* popup );
+
+ static void hide( UIWidget* popup );
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uitimepicker.hpp b/include/eepp/ui/uitimepicker.hpp
new file mode 100644
index 000000000..b4d58c2b5
--- /dev/null
+++ b/include/eepp/ui/uitimepicker.hpp
@@ -0,0 +1,23 @@
+#ifndef EE_UI_UITIMEPICKER_HPP
+#define EE_UI_UITIMEPICKER_HPP
+
+#include
+
+namespace EE { namespace UI {
+
+/** Time-only segmented editor. Time steps wrap within the day; no popup is required. */
+class EE_API UITimePicker : public UIDateTimeEdit {
+ public:
+ static UITimePicker* New();
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ protected:
+ UITimePicker();
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/premake4.lua b/premake4.lua
index 64ff2e968..e5c2f2fe4 100644
--- a/premake4.lua
+++ b/premake4.lua
@@ -1783,6 +1783,12 @@ solution "eepp"
files { "src/examples/ui_application_multi_window/*.cpp" }
build_link_configuration( "eepp-ui-application-multi-window", true )
+ project "eepp-ui-date-time-picker"
+ set_kind()
+ language "C++"
+ files { "src/examples/ui_date_time_picker/*.cpp" }
+ build_link_configuration( "eepp-ui-date-time-picker", true )
+
project "eepp-ui-font-picker"
set_kind()
language "C++"
diff --git a/premake5.lua b/premake5.lua
index ac447ce5e..fe73f0e1d 100644
--- a/premake5.lua
+++ b/premake5.lua
@@ -1820,6 +1820,12 @@ workspace "eepp"
files { "src/examples/ui_application_multi_window/*.cpp" }
build_link_configuration( "eepp-ui-application-multi-window", true )
+ project "eepp-ui-date-time-picker"
+ set_kind()
+ language "C++"
+ files { "src/examples/ui_date_time_picker/*.cpp" }
+ build_link_configuration( "eepp-ui-date-time-picker", true )
+
project "eepp-ui-font-picker"
set_kind()
language "C++"
diff --git a/src/eepp/system/datetime.cpp b/src/eepp/system/datetime.cpp
new file mode 100644
index 000000000..17aeb800b
--- /dev/null
+++ b/src/eepp/system/datetime.cpp
@@ -0,0 +1,320 @@
+#include
+
+#include
+#include
+#include
+#include
+
+namespace EE { namespace System {
+
+namespace {
+
+// Howard Hinnant's civil calendar conversion, adapted to eepp integer types. The epoch is
+// arbitrary; only differences and round trips matter here.
+constexpr Int64 daysFromCivil( Int64 year, unsigned month, unsigned day ) {
+ year -= month <= 2;
+ const Int64 era = ( year >= 0 ? year : year - 399 ) / 400;
+ const unsigned yoe = static_cast( year - era * 400 );
+ const unsigned doy = ( 153 * ( month > 2 ? month - 3 : month + 9 ) + 2 ) / 5 + day - 1;
+ const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
+ return era * 146097 + static_cast( doe );
+}
+
+CalendarDate civilFromDays( Int64 days ) {
+ const Int64 era = ( days >= 0 ? days : days - 146096 ) / 146097;
+ const unsigned doe = static_cast( days - era * 146097 );
+ const unsigned yoe = ( doe - doe / 1460 + doe / 36524 - doe / 146096 ) / 365;
+ Int64 year = static_cast( yoe ) + era * 400;
+ const unsigned doy = doe - ( 365 * yoe + yoe / 4 - yoe / 100 );
+ const unsigned mp = ( 5 * doy + 2 ) / 153;
+ const unsigned day = doy - ( 153 * mp + 2 ) / 5 + 1;
+ const unsigned month = mp < 10 ? mp + 3 : mp - 9;
+ year += month <= 2;
+
+ if ( year < std::numeric_limits::min() || year > std::numeric_limits::max() )
+ return {};
+ return { static_cast( year ), static_cast( month ), static_cast( day ) };
+}
+
+Int64 floorDiv( Int64 numerator, Int64 denominator ) {
+ const Int64 quotient = numerator / denominator;
+ const Int64 remainder = numerator % denominator;
+ return quotient - ( remainder != 0 && ( remainder < 0 ) != ( denominator < 0 ) );
+}
+
+Int64 positiveModulo( Int64 value, Int64 modulus ) {
+ const Int64 remainder = value % modulus;
+ return remainder < 0 ? remainder + modulus : remainder;
+}
+
+bool getLocalTime( std::time_t time, std::tm& out ) {
+#if EE_PLATFORM == EE_PLATFORM_WIN
+ return localtime_s( &out, &time ) == 0;
+#else
+ return localtime_r( &time, &out ) != nullptr;
+#endif
+}
+
+constexpr Int64 SecondsPerDay = 24 * 60 * 60;
+constexpr Int64 EpochCivilDays = daysFromCivil( 1970, 1, 1 );
+
+std::optional dateFromEpochDays( Int64 days ) {
+ constexpr Int64 minimum =
+ daysFromCivil( std::numeric_limits::min(), 1, 1 ) - EpochCivilDays;
+ constexpr Int64 maximum =
+ daysFromCivil( std::numeric_limits::max(), 12, 31 ) - EpochCivilDays;
+ if ( days < minimum || days > maximum )
+ return std::nullopt;
+ return civilFromDays( days + EpochCivilDays );
+}
+
+std::optional dateTimeFromUnixTimestamp( Int64 timestamp, Int32 utcOffsetSeconds,
+ Int64 unitsPerSecond ) {
+ const Int64 unitsPerDay = SecondsPerDay * unitsPerSecond;
+ // Apply the offset after splitting the timestamp so even Int64 endpoints cannot overflow.
+ const Int64 dayUnits = positiveModulo( timestamp, unitsPerDay ) +
+ static_cast( utcOffsetSeconds ) * unitsPerSecond;
+ const auto date =
+ dateFromEpochDays( floorDiv( timestamp, unitsPerDay ) + floorDiv( dayUnits, unitsPerDay ) );
+ if ( !date )
+ return std::nullopt;
+ const Int64 wrapped = positiveModulo( dayUnits, unitsPerDay );
+ const Int64 seconds = wrapped / unitsPerSecond;
+ return LocalDateTime{ *date,
+ { static_cast( seconds / 3600 ),
+ static_cast( seconds / 60 % 60 ),
+ static_cast( seconds % 60 ),
+ static_cast( unitsPerSecond == 1000 ? wrapped % 1000 : 0 ) } };
+}
+
+} // namespace
+
+bool CalendarDate::isLeapYear( Int32 year ) {
+ return year % 4 == 0 && ( year % 100 != 0 || year % 400 == 0 );
+}
+
+int CalendarDate::daysInMonth( Int32 year, Uint8 month ) {
+ static constexpr int Days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+ if ( month < 1 || month > 12 )
+ return 0;
+ if ( month == 2 && isLeapYear( year ) )
+ return 29;
+ return Days[month - 1];
+}
+
+bool CalendarDate::isValid() const {
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth( year, month );
+}
+
+int CalendarDate::daysInMonth() const {
+ return daysInMonth( year, month );
+}
+
+Int64 CalendarDate::daysSinceEpoch() const {
+ return daysFromCivil( year, month, day ) - EpochCivilDays;
+}
+
+std::optional CalendarDate::toUnixTimestamp() const {
+ if ( !isValid() )
+ return std::nullopt;
+ return daysSinceEpoch() * SecondsPerDay;
+}
+
+std::optional CalendarDate::fromUnixTimestamp( Int64 seconds ) {
+ return dateFromEpochDays( floorDiv( seconds, SecondsPerDay ) );
+}
+
+CalendarDate CalendarDate::addDays( Int64 days ) const {
+ if ( !isValid() )
+ return *this;
+ const Int64 current = daysFromCivil( year, month, day );
+ const Int64 minimum = daysFromCivil( std::numeric_limits::min(), 1, 1 );
+ const Int64 maximum = daysFromCivil( std::numeric_limits::max(), 12, 31 );
+ if ( days < minimum - current || days > maximum - current )
+ return *this;
+ return civilFromDays( current + days );
+}
+
+CalendarDate CalendarDate::addMonths( Int64 months ) const {
+ if ( !isValid() )
+ return *this;
+
+ const Int64 current = static_cast( year ) * 12 + ( month - 1 );
+ const Int64 minimum = static_cast( std::numeric_limits::min() ) * 12;
+ const Int64 maximum = static_cast( std::numeric_limits::max() ) * 12 + 11;
+ if ( months < minimum - current || months > maximum - current )
+ return *this;
+ const Int64 totalMonths = current + months;
+ const Int64 newYear = floorDiv( totalMonths, 12 );
+ if ( newYear < std::numeric_limits::min() ||
+ newYear > std::numeric_limits::max() )
+ return *this;
+ const Uint8 newMonth = static_cast( positiveModulo( totalMonths, 12 ) + 1 );
+ const Uint8 newDay = static_cast(
+ std::min( day, daysInMonth( static_cast( newYear ), newMonth ) ) );
+ return { static_cast( newYear ), newMonth, newDay };
+}
+
+CalendarDate CalendarDate::addYears( Int64 years ) const {
+ if ( !isValid() )
+ return *this;
+ if ( years < static_cast( std::numeric_limits::min() ) - year ||
+ years > static_cast( std::numeric_limits::max() ) - year )
+ return *this;
+ const Int64 newYear = static_cast( year ) + years;
+ if ( newYear < std::numeric_limits::min() ||
+ newYear > std::numeric_limits::max() )
+ return *this;
+ const Uint8 newDay = static_cast(
+ std::min( day, daysInMonth( static_cast( newYear ), month ) ) );
+ return { static_cast( newYear ), month, newDay };
+}
+
+CalendarDate CalendarDate::today() {
+ const std::time_t current = std::time( nullptr );
+ std::tm local{};
+ if ( !getLocalTime( current, local ) )
+ return {};
+ return { static_cast( local.tm_year + 1900 ), static_cast( local.tm_mon + 1 ),
+ static_cast( local.tm_mday ) };
+}
+
+bool TimeOfDay::isValid() const {
+ return hour < 24 && minute < 60 && second < 60 && millisecond < 1000;
+}
+
+TimeOfDay TimeOfDay::addHours( Int64 hours ) const {
+ if ( !isValid() )
+ return *this;
+ TimeOfDay result = *this;
+ result.hour =
+ static_cast( positiveModulo( static_cast( hour ) + hours % 24, 24 ) );
+ return result;
+}
+
+TimeOfDay TimeOfDay::addMinutes( Int64 minutes ) const {
+ if ( !isValid() )
+ return *this;
+ const Int64 totalMinutes = static_cast( hour ) * 60 + minute + minutes % ( 24 * 60 );
+ const Int64 wrapped = positiveModulo( totalMinutes, 24 * 60 );
+ TimeOfDay result = *this;
+ result.hour = static_cast( wrapped / 60 );
+ result.minute = static_cast( wrapped % 60 );
+ return result;
+}
+
+TimeOfDay TimeOfDay::addSeconds( Int64 seconds ) const {
+ if ( !isValid() )
+ return *this;
+ const Int64 totalSeconds =
+ static_cast( hour ) * 3600 + minute * 60 + second + seconds % SecondsPerDay;
+ const Int64 wrapped = positiveModulo( totalSeconds, SecondsPerDay );
+ TimeOfDay result = *this;
+ result.hour = static_cast( wrapped / 3600 );
+ result.minute = static_cast( ( wrapped % 3600 ) / 60 );
+ result.second = static_cast( wrapped % 60 );
+ return result;
+}
+
+TimeOfDay TimeOfDay::now() {
+ return LocalDateTime::now().time;
+}
+
+bool LocalDateTime::isValid() const {
+ return date.isValid() && time.isValid();
+}
+
+std::optional LocalDateTime::toUnixTimestamp( Int32 utcOffsetSeconds ) const {
+ if ( !isValid() )
+ return std::nullopt;
+ // The complete Int32 year range fits in Int64 seconds, including any Int32 offset.
+ return date.daysSinceEpoch() * SecondsPerDay + time.hour * 3600 + time.minute * 60 +
+ time.second - static_cast( utcOffsetSeconds );
+}
+
+std::optional LocalDateTime::toUnixTimestampMilliseconds( Int32 utcOffsetSeconds ) const {
+ const auto seconds = toUnixTimestamp( utcOffsetSeconds );
+ if ( !seconds )
+ return std::nullopt;
+ constexpr Int64 minimum = std::numeric_limits::min();
+ constexpr Int64 maximum = std::numeric_limits::max();
+ const Int64 minimumSeconds = floorDiv( minimum, 1000 );
+ const Int64 minimumFraction = positiveModulo( minimum, 1000 );
+ if ( *seconds < minimumSeconds || *seconds > maximum / 1000 ||
+ ( *seconds == minimumSeconds && time.millisecond < minimumFraction ) ||
+ ( *seconds == maximum / 1000 && time.millisecond > maximum % 1000 ) )
+ return std::nullopt;
+ // The floor-divided minimum second cannot be multiplied by 1000 without overflowing.
+ if ( *seconds == minimumSeconds )
+ return minimum + ( time.millisecond - minimumFraction );
+ return *seconds * 1000 + time.millisecond;
+}
+
+std::optional LocalDateTime::fromUnixTimestamp( Int64 seconds,
+ Int32 utcOffsetSeconds ) {
+ return dateTimeFromUnixTimestamp( seconds, utcOffsetSeconds, 1 );
+}
+
+std::optional
+LocalDateTime::fromUnixTimestampMilliseconds( Int64 milliseconds, Int32 utcOffsetSeconds ) {
+ return dateTimeFromUnixTimestamp( milliseconds, utcOffsetSeconds, 1000 );
+}
+
+namespace {
+
+LocalDateTime addLocalUnits( const LocalDateTime& value, Int64 amount, Int64 unitsPerDay,
+ Int64 secondsPerUnit ) {
+ if ( !value.isValid() )
+ return value;
+ const Int64 current = value.time.hour * 3600 + value.time.minute * 60 + value.time.second;
+ // Divide before multiplying: arbitrary Int64 steps must not overflow intermediates.
+ const Int64 total = current + ( amount % unitsPerDay ) * secondsPerUnit;
+ const Int64 dayDelta = amount / unitsPerDay + floorDiv( total, SecondsPerDay );
+ const CalendarDate date = value.date.addDays( dayDelta );
+ if ( dayDelta != 0 && date == value.date )
+ return value;
+ const Int64 wrapped = positiveModulo( total, SecondsPerDay );
+ return { date,
+ { static_cast( wrapped / 3600 ), static_cast( ( wrapped % 3600 ) / 60 ),
+ static_cast( wrapped % 60 ), value.time.millisecond } };
+}
+
+} // namespace
+
+LocalDateTime LocalDateTime::addHours( Int64 hours ) const {
+ return addLocalUnits( *this, hours, 24, 3600 );
+}
+
+LocalDateTime LocalDateTime::addMinutes( Int64 minutes ) const {
+ return addLocalUnits( *this, minutes, 1440, 60 );
+}
+
+LocalDateTime LocalDateTime::addSeconds( Int64 seconds ) const {
+ return addLocalUnits( *this, seconds, SecondsPerDay, 1 );
+}
+
+LocalDateTime LocalDateTime::now() {
+ // Sample once so crossing midnight cannot combine yesterday's date with today's time.
+ const auto now = std::chrono::system_clock::now();
+ const std::time_t current = std::chrono::system_clock::to_time_t( now );
+ std::tm local{};
+ if ( !getLocalTime( current, local ) )
+ return {};
+ const auto milliseconds =
+ std::chrono::duration_cast