HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
style_parser.hpp
1
2
3#pragma once
4
5#include "style_attributes.hpp"
6#include "../parser/parser.hpp"
7#include "../container/container.hpp"
8#include "../macros.hpp"
9#include <iterator>
10#include <tuple>
11#include <exception>
12
13hi_export_module(hikogui.theme : theme_tag);
14
15hi_export namespace hi {
16inline namespace v1 {
17namespace detail {
18
19template<std::input_iterator It, std::sentinel_for<It> ItEnd>
20[[nodiscard]] constexpr expected_optional<std::string, std::string> parse_style_path_id(It& it, ItEnd last)
21{
22 hi_assert(it != last);
23
24 if (*it != '#') {
25 return std::nullopt;
26 }
27
28 ++it;
29 if (it == last or *it != token::id) {
30 return std::unexpected{std::format("{}: Expected a widget-id after '#', got '{}'.", token_location(it, last), *it)};
31 }
32
33 auto r = static_cast<std::string>(*it);
34 ++it;
35 return r;
36}
37
38template<std::input_iterator It, std::sentinel_for<It> ItEnd>
39[[nodiscard]] constexpr expected_optional<std::string, std::string> parse_style_path_class(It& it, ItEnd last)
40{
41 hi_assert(it != last);
42
43 if (*it != '.') {
44 return std::nullopt;
45 }
46
47 ++it;
48 if (it == last or *it != token::id) {
49 return std::unexpected{std::format("{}: Expected a widget-class after '.', got '{}'.", token_location(it, last), *it)};
50 }
51
52 auto r = static_cast<std::string>(*it);
53 ++it;
54 return r;
55}
56
57template<std::input_iterator It, std::sentinel_for<It> ItEnd>
58[[nodiscard]] constexpr expected_optional<horizontal_alignment, std::string> parse_style_horizontal_alignment(It& it, ItEnd last)
59{
60 hi_assert(it != last);
61
62 if (*it != token::id) {
63 return std::nullopt;
64 }
65
66 if (*it == "none") {
67 ++it;
69 } else if (*it == "flush") {
70 ++it;
72 } else if (*it == "left") {
73 ++it;
75 } else if (*it == "center") {
76 ++it;
78 } else if (*it == "justified") {
79 ++it;
81 } else if (*it == "right") {
82 ++it;
84 } else {
85 return std::unexpected{std::format("{}: Unknown horizontal alignment {}.", token_location(it, last), static_cast<std::string>(*it))};
86 }
87}
88
89template<std::input_iterator It, std::sentinel_for<It> ItEnd>
90[[nodiscard]] constexpr expected_optional<vertical_alignment, std::string> parse_style_vertical_alignment(It& it, ItEnd last)
91{
92 hi_assert(it != last);
93
94 if (*it != token::id) {
95 return std::nullopt;
96 }
97
98 if (*it == "none") {
99 ++it;
101 } else if (*it == "top") {
102 ++it;
104 } else if (*it == "middle") {
105 ++it;
107 } else if (*it == "bottom") {
108 ++it;
110 } else {
111 return std::unexpected{std::format("{}: Unknown vertical alignment {}.", token_location(it, last), static_cast<std::string>(*it))};
112 }
113}
114template<std::input_iterator It, std::sentinel_for<It> ItEnd>
115[[nodiscard]] constexpr expected_optional<unit::length_f, std::string> parse_style_length(It& it, ItEnd last)
116{
117 hi_assert(it != last);
118
119 if (*it != token::integer and *it != token::real) {
120 return std::nullopt;
121 }
122
123 auto const value = static_cast<float>(*it);
124 ++it;
125
126 if (it == last or *it != token::id) {
127 // A numeric value without a suffix is in device independet pixels.
128 return unit::dips(value);
129 } else if (*it == "px") {
130 ++it;
131 return unit::pixels(value);
132 } else if (*it == "dp" or *it == "dip") {
133 ++it;
134 return unit::dips(value);
135 } else if (*it == "pt") {
136 ++it;
137 return unit::points(value);
138 } else if (*it == "in") {
139 ++it;
140 return au::inches(value);
141 } else if (*it == "cm") {
142 ++it;
143 return au::centi(au::meters)(value);
144 } else {
145 // Unknown suffix could be token for another part of the tag.
146 return unit::dips(value);
147 }
148}
149
150template<std::input_iterator It, std::sentinel_for<It> ItEnd>
151[[nodiscard]] constexpr expected_optional<color, std::string> parse_style_color(It& it, ItEnd last)
152{
153 hi_assert(it != last);
154
155 if (*it == token::id and *it == "rgb") {
156 ++it;
157 if (it == last or *it != '(') {
158 return std::unexpected{std::format("{}: Missing '(' after rgb_color.", token_location(it, last))};
159 }
160
161 ++it;
162 if (it == last or (*it != token::integer and *it != token::real)) {
163 return std::unexpected{
164 std::format("{}: Expecting a number as first argument to rgb_color.", token_location(it, last))};
165 }
166 auto const red = static_cast<float>(*it);
167
168 ++it;
169 if (it == last or *it != ',') {
170 return std::unexpected{
171 std::format("{}: Expecting a comma ',' after first argument to rgb_color.", token_location(it, last))};
172 }
173
174 ++it;
175 if (it == last or (*it != token::integer and *it != token::real)) {
176 return std::unexpected{
177 std::format("{}: Expecting a number as second argument to rgb_color.", token_location(it, last))};
178 }
179 auto const green = static_cast<float>(*it);
180
181 ++it;
182 if (it == last or *it != ',') {
183 return std::unexpected{
184 std::format("{}: Expecting a comma ',' after second argument to rgb_color.", token_location(it, last))};
185 }
186
187 ++it;
188 if (it == last or (*it != token::integer and *it != token::real)) {
189 return std::unexpected{
190 std::format("{}: Expecting a number as third argument to rgb_color.", token_location(it, last))};
191 }
192 auto const blue = static_cast<float>(*it);
193
194 ++it;
195 if (it == last or *it != ')') {
196 return std::unexpected{std::format("{}: Missing ')' after rgb_color arguments.", token_location(it, last))};
197 }
198
199 ++it;
200 return color{red, green, blue, 1.0f};
201
202 } else if (*it == token::id and *it == "rgba") {
203 ++it;
204 if (it == last or *it != '(') {
205 return std::unexpected{std::format("{}: Missing '(' after rgba_color.", token_location(it, last))};
206 }
207
208 ++it;
209 if (it == last or (*it != token::integer and *it != token::real)) {
210 return std::unexpected{
211 std::format("{}: Expecting a number as first argument to rgba_color.", token_location(it, last))};
212 }
213 auto const red = static_cast<float>(*it);
214
215 ++it;
216 if (it == last or *it != ',') {
217 return std::unexpected{
218 std::format("{}: Expecting a comma ',' after first argument to rgba_color.", token_location(it, last))};
219 }
220
221 ++it;
222 if (it == last or (*it != token::integer and *it != token::real)) {
223 return std::unexpected{
224 std::format("{}: Expecting a number as second argument to rgba_color.", token_location(it, last))};
225 }
226 auto const green = static_cast<float>(*it);
227
228 ++it;
229 if (it == last or *it != ',') {
230 return std::unexpected{
231 std::format("{}: Expecting a comma ',' after second argument to rgba_color.", token_location(it, last))};
232 }
233
234 ++it;
235 if (it == last or (*it != token::integer and *it != token::real)) {
236 return std::unexpected{
237 std::format("{}: Expecting a number as third argument to rgba_color.", token_location(it, last))};
238 }
239 auto const blue = static_cast<float>(*it);
240
241 ++it;
242 if (it == last or *it != ',') {
243 return std::unexpected{
244 std::format("{}: Expecting a comma ',' after third argument to rgba_color.", token_location(it, last))};
245 }
246
247 ++it;
248 if (it == last or (*it != token::integer and *it != token::real)) {
249 return std::unexpected{
250 std::format("{}: Expecting a number as forth argument to rgba_color.", token_location(it, last))};
251 }
252 auto const alpha = static_cast<float>(*it);
253
254 ++it;
255 if (it == last or *it != ')') {
256 return std::unexpected{std::format("{}: Missing ')' after rgba_color arguments.", token_location(it, last))};
257 }
258
259 ++it;
260 return color{red, green, blue, alpha};
261
262 } else if (*it == token::id) {
263 auto const color_name = static_cast<std::string>(*it);
264 ++it;
265
266 if (auto const* color_ptr = color::find(color_name)) {
267 return *color_ptr;
268 } else {
269 return std::unexpected{std::format("{}: Unknown color name '{}'.", token_location(it, last), color_name)};
270 }
271
272 } else if (*it == token::sstr or *it == token::dstr) {
273 auto const color_name = static_cast<std::string>(*it);
274 ++it;
275
276 if (color_name.starts_with("#")) {
277 try {
278 return color_from_sRGB(color_name);
279
280 } catch (std::exception const &e) {
281 return std::unexpected{std::format("{}: Could not parse hex color '{}': {}", token_location(it, last), color_name, e.what())};
282 }
283
284 } else if (auto const* color_ptr = color::find(color_name)) {
285 return *color_ptr;
286 } else {
287 return std::unexpected{std::format("{}: Unknown color name '{}'.", token_location(it, last), color_name)};
288 }
289
290 } else {
291 return std::unexpected{std::format("{}: Unknown color value {}.", token_location(it, last), *it)};
292 }
293}
294
295template<std::input_iterator It, std::sentinel_for<It> ItEnd>
296[[nodiscard]] constexpr expected_optional<style_attributes, std::string> parse_style_attribute(It& it, ItEnd last)
297{
298#define HIX_VALUE(VALUE_PARSER, NAME, ATTRIBUTE) \
299 if (name == NAME) { \
300 if (auto const value = VALUE_PARSER(it, last)) { \
301 auto r = style_attributes{}; \
302 r.set_##ATTRIBUTE(*value, true); \
303 return r; \
304 } else if (value.has_error()) { \
305 return std::unexpected(value.error()); \
306 } else { \
307 return std::unexpected(std::format("{}: Unknown value {} for attribute '{}'", token_location(it, last), *it, name)); \
308 } \
309 } else
310
311 hi_assert(it != last);
312
313 if (it.size() < 3 or it[0] != token::id or it[1] != '=') {
314 return std::nullopt;
315 }
316
317 auto const name = static_cast<std::string>(it[0]);
318 it += 2;
319
320 HIX_VALUE(parse_style_length, "width", width)
321 HIX_VALUE(parse_style_length, "height", height)
322 HIX_VALUE(parse_style_length, "margin-left", margin_left)
323 HIX_VALUE(parse_style_length, "margin-bottom", margin_bottom)
324 HIX_VALUE(parse_style_length, "margin-right", margin_right)
325 HIX_VALUE(parse_style_length, "margin-top", margin_top)
326 HIX_VALUE(parse_style_length, "margin", margin)
327 HIX_VALUE(parse_style_length, "padding-left", padding_left)
328 HIX_VALUE(parse_style_length, "padding-bottom", padding_bottom)
329 HIX_VALUE(parse_style_length, "padding-right", padding_right)
330 HIX_VALUE(parse_style_length, "padding-top", padding_top)
331 HIX_VALUE(parse_style_length, "padding", padding)
332 HIX_VALUE(parse_style_length, "border-width", border_width)
333 HIX_VALUE(parse_style_length, "border-bottom-left-radius", border_bottom_left_radius)
334 HIX_VALUE(parse_style_length, "border-bottom-right-radius", border_bottom_right_radius)
335 HIX_VALUE(parse_style_length, "border-top-left-radius", border_top_left_radius)
336 HIX_VALUE(parse_style_length, "border-top-right-radius", border_top_right_radius)
337 HIX_VALUE(parse_style_length, "border-radius", border_radius)
338 HIX_VALUE(parse_style_color, "foreground-color", foreground_color)
339 HIX_VALUE(parse_style_color, "background-color", background_color)
340 HIX_VALUE(parse_style_color, "border-color", border_color)
341 HIX_VALUE(parse_style_horizontal_alignment, "horizontal-alignment", horizontal_alignment)
342 HIX_VALUE(parse_style_vertical_alignment, "vertical-alignment", vertical_alignment)
343 {
344 return std::unexpected(std::format("{}: Unknown attribute '{}'.", token_location(it, last), name));
345 }
346
347#undef HIX_VALUE
348}
349
350} // namespace detail
351
352template<std::input_iterator It, std::sentinel_for<It> ItEnd>
353[[nodiscard]] constexpr expected_optional<std::tuple<style_attributes, std::string, std::vector<std::string>>, std::string> parse_style(It first, ItEnd last)
354{
355 constexpr auto config = [] {
356 auto r = lexer_config{};
357 r.has_double_quote_string_literal = 1;
358 r.has_single_quote_string_literal = 1;
359 r.filter_white_space = 1;
360 r.minus_in_identifier = 1;
361 return r;
362 }();
363
364 auto lexer_it = lexer<config>.parse(first, last);
365 auto token_it = make_lookahead_iterator<4>(lexer_it);
366
367 auto attributes = hi::style_attributes{};
368 auto id = std::string{};
369 auto classes = std::vector<std::string>{};
370 while (token_it != std::default_sentinel) {
371 if (auto attribute = detail::parse_style_attribute(token_it, std::default_sentinel)) {
372 attributes.apply(*attribute);
373 continue;
374
375 } else if (attribute.has_error()) {
376 return std::unexpected{attribute.error()};
377 }
378
379 if (auto new_id = detail::parse_style_path_id(token_it, std::default_sentinel)) {
380 if (id.empty()) {
381 id = *new_id;
382 continue;
383 } else {
384 return std::unexpected{std::format("{}: Style already has id #{}.", token_location(token_it, std::default_sentinel), id)};
385 }
386
387 } else if (new_id.has_error()) {
388 return std::unexpected{new_id.error()};
389 }
390
391 if (auto new_class = detail::parse_style_path_class(token_it, std::default_sentinel)) {
392 classes.push_back(*new_class);
393 continue;
394
395 } else if (new_class.has_error()) {
396 return std::unexpected{new_class.error()};
397 }
398
399 return std::unexpected{std::format("{}: Unexpected token '{}'.", token_location(token_it, std::default_sentinel), *token_it)};
400 }
401
402 return std::tuple{attributes, id, classes};
403}
404
405[[nodiscard]] constexpr expected_optional<std::tuple<style_attributes, std::string, std::vector<std::string>>, std::string> parse_style(std::string_view str)
406{
407 return parse_style(str.begin(), str.end());
408}
409
410} // namespace v1
411} // namespace hi::v1
color color_from_sRGB(float r, float g, float b, float a) noexcept
Convert gama corrected sRGB color to the linear color.
Definition sRGB.hpp:149
vertical_alignment
Vertical alignment.
Definition alignment.hpp:25
horizontal_alignment
Horizontal alignment.
Definition alignment.hpp:102
@ none
No alignment.
Definition alignment.hpp:28
@ middle
Align to the vertical-middle.
Definition alignment.hpp:36
@ bottom
Align to the bottom.
Definition alignment.hpp:40
@ top
Align to the top.
Definition alignment.hpp:32
@ none
No alignment.
Definition alignment.hpp:105
@ right
Align the text to the right side.
Definition alignment.hpp:136
@ left
Align the text to the left side.
Definition alignment.hpp:118
@ flush
Align the text naturally based on the writing direction of each paragraph.
Definition alignment.hpp:112
@ justified
Stretch the text to be flush to both sides.
Definition alignment.hpp:130
@ center
Align the text in the center.
Definition alignment.hpp:124
The HikoGUI namespace.
Definition array_generic.hpp:21
The HikoGUI API version 1.
Definition array_generic.hpp:22
hi_export constexpr std::string token_location(It &it, ItEnd last, std::string_view path) noexcept
Create a location string for error messages.
Definition token.hpp:163
@ color
A color value was modified.
Definition style_modify_mask.hpp:27
@ margin
A margin or padding value was modified.
Definition style_modify_mask.hpp:39
auto make_lookahead_iterator(It first, ItEnd last=std::default_sentinel) noexcept
Create a lookahead_iterator from a forward iterator.
Definition lookahead_iterator.hpp:227
static color * find(std::string const &name) noexcept
Find a color by name.
Definition color_intf.hpp:298
Definition expected_optional.hpp:18
Definition lexer.hpp:23
T unexpected(T... args)
T what(T... args)