morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
widget_hints.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file forms/widget_hints.hpp
6/// @brief Typed wrappers that carry rendering-control intent for a form field.
7///
8/// `morph::forms::Multiline` and `morph::forms::Ranged<Min, Max, Step>` are
9/// thin field-type wrappers in the same family as `morph::forms::Choice`
10/// (choice.hpp): each carries a rendering *control* preference in the C++
11/// type itself — `Multiline` says "edit me as a text area", `Ranged` says
12/// "edit me as a slider with these bounds" — and `morph::forms::schemaJson`
13/// surfaces that preference as the `x-widget` (and, for `Ranged`, `x-min` /
14/// `x-max` / `x-step`) schema annotation(s). See docs/spec/forms/widget_hints.md
15/// for the full design.
16///
17/// Neither wrapper changes what travels on the wire: the `glz::meta`
18/// specialisations below reflect each type's bare payload directly, exactly
19/// as `Choice` does, so `Multiline` serialises as a plain JSON string and
20/// `Ranged` as a nullable number.
21
22#include <concepts>
23#include <glaze/glaze.hpp>
24#include <optional>
25#include <string>
26#include <string_view>
27#include <type_traits>
28#include <utility>
29
31
32namespace morph::forms {
33
34/// @brief A string field edited as a multi-line text area.
35///
36/// Wire form: a plain JSON string (via the `glz::meta` specialisation below),
37/// exactly like an unwrapped `std::string` member — `Multiline` changes only
38/// the *rendering intent*, never what travels. Because the payload is a bare
39/// `std::string` (not a `std::optional`), `Multiline` does **not** define
40/// `hasValue()`: like a plain `std::string` member, it is always considered
41/// engaged by `allRequiredEngaged` (see docs/spec/forms/forms.md, "Empty state").
42struct Multiline {
43 /// @brief The text content.
44 std::string value;
45
46 /// @brief Constructs the empty string.
47 constexpr Multiline() noexcept = default;
48
49 /// @brief Engages with @p text.
50 /// @param text The initial text content.
51 Multiline(std::string text) noexcept(std::is_nothrow_move_constructible_v<std::string>) : value{std::move(text)} {}
52
53 /// @brief The preferred control id for this field.
54 /// @return `"textarea"`.
55 [[nodiscard]] static constexpr std::string_view widget() noexcept { return "textarea"; }
56
57 /// @brief Equality on the text content.
58 /// @param other Multiline to compare against.
59 /// @return `true` when both hold the same text.
60 [[nodiscard]] constexpr bool operator==(const Multiline& other) const = default;
61};
62
63/// @brief A bounded numeric field edited as a slider.
64/// @tparam Min Inclusive lower bound of the control track.
65/// @tparam Max Inclusive upper bound of the control track (must be > `Min`).
66/// @tparam Step Track increment (default `1`; must be > 0). Must be the same
67/// arithmetic type as `Min`/`Max` — a floating-point `Ranged`
68/// must name `Step` explicitly (the default `1` is `int`).
69template <auto Min, auto Max, auto Step = 1>
70 requires(std::is_arithmetic_v<decltype(Min)> && std::same_as<decltype(Min), decltype(Max)> &&
71 std::same_as<decltype(Min), decltype(Step)>)
72struct Ranged {
73 static_assert(Min < Max, "Ranged: Min must be less than Max");
74 static_assert(Step > decltype(Step){0}, "Ranged: Step must be strictly positive");
75
76 /// @brief The payload; `std::nullopt` means "not entered".
77 std::optional<decltype(Min)> value;
78
79 /// @brief Constructs the empty state.
80 constexpr Ranged() noexcept = default;
81
82 /// @brief Engages with @p selected.
83 /// @param selected The selected value.
84 constexpr Ranged(decltype(Min) selected) noexcept : value{selected} {}
85
86 /// @brief Adopts an optional payload as-is.
87 /// @param payload Engaged or empty payload.
88 constexpr Ranged(std::optional<decltype(Min)> payload) noexcept : value{payload} {}
89
90 /// @brief Whether a value has been entered.
91 /// @return `true` if the payload is engaged.
92 [[nodiscard]] constexpr bool hasValue() const noexcept { return value.has_value(); }
93
94 /// @brief Unchecked access to the engaged value (UB when empty, exactly
95 /// like `std::optional::operator*`).
96 /// @return The engaged value.
97 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
98 [[nodiscard]] constexpr decltype(Min) operator*() const noexcept { return *value; }
99
100 /// @brief The slider's inclusive lower bound.
101 /// @return `Min`.
102 [[nodiscard]] static constexpr auto min() noexcept { return Min; }
103
104 /// @brief The slider's inclusive upper bound.
105 /// @return `Max`.
106 [[nodiscard]] static constexpr auto max() noexcept { return Max; }
107
108 /// @brief The slider's track increment.
109 /// @return `Step`.
110 [[nodiscard]] static constexpr auto step() noexcept { return Step; }
111
112 /// @brief The preferred control id for this field.
113 /// @return `"slider"`.
114 [[nodiscard]] static constexpr std::string_view widget() noexcept { return "slider"; }
115
116 /// @brief Equality on the payload; empty equals only empty.
117 /// @param other Ranged to compare against.
118 /// @return `true` when both are empty or both hold equal values.
119 [[nodiscard]] constexpr bool operator==(const Ranged& other) const = default;
120};
121
122} // namespace morph::forms
123
124/// @brief On the wire a Multiline is a plain string — the widget hint lives
125/// in the C++ type and the generated schema only.
126template <>
127struct glz::meta<morph::forms::Multiline> {
128 static constexpr auto value = &morph::forms::Multiline::value;
129 static constexpr std::string_view name = "Multiline";
130};
131
132/// @brief On the wire a Ranged is its nullable underlying value — the slider
133/// bounds live in the C++ type and the generated schema only.
134///
135/// `name` carries the payload type, because that is the whole of what the
136/// `$defs` entry describes — the bounds are emitted as property-level
137/// `x-min`/`x-max`/`x-step` and never reach the definition. With one shared
138/// `"Ranged"` name, a `double` slider and an `int` slider in the same action
139/// would collapse into a single entry whose type is wrong for one of them.
140/// Two `Ranged` fields of the *same* payload type do share one entry, because
141/// their entries are identical. See
142/// `forms/detail/schema_name.hpp`.
143/// @tparam Min Inclusive lower bound.
144/// @tparam Max Inclusive upper bound.
145/// @tparam Step Track increment.
146template <auto Min, auto Max, auto Step>
147struct glz::meta<morph::forms::Ranged<Min, Max, Step>> {
148 static constexpr auto value = &morph::forms::Ranged<Min, Max, Step>::value;
149 static constexpr std::string_view name = morph::forms::detail::rangedSchemaName<decltype(Min)>;
150};
Compile-time $defs keys for the forms layer's field wrappers.
A string field edited as a multi-line text area.
constexpr bool operator==(const Multiline &other) const =default
Equality on the text content.
static constexpr std::string_view widget() noexcept
The preferred control id for this field.
std::string value
The text content.
constexpr Multiline() noexcept=default
Constructs the empty string.
static constexpr auto min() noexcept
The slider's inclusive lower bound.
static constexpr auto step() noexcept
The slider's track increment.
static constexpr auto max() noexcept
The slider's inclusive upper bound.
constexpr Ranged() noexcept=default
Constructs the empty state.
std::optional< decltype(Min)> value
The payload; std::nullopt means "not entered".
constexpr bool hasValue() const noexcept
Whether a value has been entered.
constexpr Ranged(std::optional< decltype(Min)> payload) noexcept
Adopts an optional payload as-is.
static constexpr std::string_view widget() noexcept
The preferred control id for this field.
constexpr bool operator==(const Ranged &other) const =default
Equality on the payload; empty equals only empty.