lib/ui/src/style/computed.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const arrange = @import("arrange");
3 const css = @import("css");
4 const abi = @import("../abi/root.zig");
5 const asset = @import("../asset/root.zig");
6 const paint_mod = @import("paint.zig");
7
8 const Value = css.value.Value;
9
10 pub const Metrics = struct {
11 root_font_size: f32 = 16,
12 viewport_width: f32 = 0,
13 viewport_height: f32 = 0,
14 };
15
16 pub const FontSource = struct {
17 registry: *const asset.Registry,
18 source: []const u8,
19 };
20
21 pub const FontEnvironment = struct {
22 source: FontSource,
23 fallback_out: []asset.AssetHandle,
24 first: u32,
25 };
26
27 pub const Record = struct {
28 computed: css.cascade.Computed = .{},
29 layout: arrange.Style = .{},
30 dimensions: arrange.Dimensions = .{},
31 paint: paint_mod.Paint = .{},
32 text: paint_mod.TextStyle = .{},
33 layer: i16 = 0,
34 };
35
36 fn value(style: *const css.cascade.Computed, id: css.property.Id) Value {
37 return style.get(id);
38 }
39
40 fn number(item: Value, fallback: f32) f32 {
41 return switch (item.valueKind()) {
42 .number, .length, .percent => item.asNumber(),
43 else => fallback,
44 };
45 }
46
47 fn length(item: Value, metrics: Metrics, font_size: f32, line_height: f32) ?f32 {
48 if (item.valueKind() != .length) return null;
49 const amount = item.asNumber();
50 return amount * switch (item.valueUnit()) {
51 .px => @as(f32, 1),
52 .em => font_size,
53 .rem => metrics.root_font_size,
54 .ch => font_size * 0.5,
55 .lh => line_height,
56 .ex => font_size * 0.5,
57 .vw => metrics.viewport_width / 100,
58 .vh => metrics.viewport_height / 100,
59 .vmin => @min(metrics.viewport_width, metrics.viewport_height) / 100,
60 .vmax => @max(metrics.viewport_width, metrics.viewport_height) / 100,
61 else => return null,
62 };
63 }
64
65 fn color(item: Value, foreground: abi.Color) abi.Color {
66 if (item.asKeyword() == .currentcolor) return foreground;
67 if (item.valueKind() != .color) return .{};
68 return .{
69 .r = css.value.color.red(item.a),
70 .g = css.value.color.green(item.a),
71 .b = css.value.color.blue(item.a),
72 .a = css.value.color.alpha(item.a),
73 };
74 }
75
76 fn styleCode(item: Value) u8 {
77 return switch (item.asKeyword()) {
78 .solid => 1,
79 .dashed => 2,
80 .dotted => 3,
81 .double => 4,
82 .groove => 5,
83 .ridge => 6,
84 .inset => 7,
85 .outset => 8,
86 else => 0,
87 };
88 }
89
90 fn textFlags(style: *const css.cascade.Computed) u16 {
91 const white_space: u16 = switch (value(style, .white_space).asKeyword()) {
92 .nowrap => 1,
93 .pre => 2,
94 .pre_wrap => 3,
95 .pre_line => 4,
96 .break_spaces => 5,
97 else => 0,
98 };
99 const word_break: u16 = switch (value(style, .word_break).asKeyword()) {
100 .break_all => 1,
101 .keep_all => 2,
102 else => 0,
103 };
104 const overflow_wrap: u16 = switch (value(style, .overflow_wrap).asKeyword()) {
105 .break_word => 1,
106 .anywhere => 2,
107 else => 0,
108 };
109 const direction: u16 = @intFromBool(value(style, .direction).asKeyword() == .rtl);
110 const user_select: u16 = switch (value(style, .user_select).asKeyword()) {
111 .none => 1,
112 .text => 2,
113 .all => 3,
114 else => 0,
115 };
116 const slant: u16 = switch (value(style, .font_style).asKeyword()) {
117 .italic => 1,
118 .oblique => 2,
119 else => 0,
120 };
121 return (white_space << paint_mod.TextFlags.white_space_shift) |
122 (word_break << paint_mod.TextFlags.word_break_shift) |
123 (overflow_wrap << paint_mod.TextFlags.overflow_wrap_shift) |
124 (direction << paint_mod.TextFlags.direction_shift) |
125 (user_select << paint_mod.TextFlags.user_select_shift) |
126 (slant << paint_mod.TextFlags.slant_shift);
127 }
128
129 fn inset(style: *const css.cascade.Computed, metrics: Metrics, size: f32, leading: f32, ids: [4]css.property.Id) arrange.Insets {
130 return .{
131 .top = length(value(style, ids[0]), metrics, size, leading),
132 .right = length(value(style, ids[1]), metrics, size, leading),
133 .bottom = length(value(style, ids[2]), metrics, size, leading),
134 .left = length(value(style, ids[3]), metrics, size, leading),
135 };
136 }
137
138 pub fn lower(style: css.cascade.Computed, metrics: Metrics, fonts: ?FontEnvironment) Record {
139 var record = Record{ .computed = style };
140 const font = length(value(&style, .font_size), metrics, metrics.root_font_size, metrics.root_font_size) orelse metrics.root_font_size;
141 const leading_value = value(&style, .line_height);
142 const leading = if (leading_value.valueKind() == .number)
143 font * leading_value.asNumber()
144 else
145 length(leading_value, metrics, font, font) orelse font * 1.2;
146 record.layout.flex_direction = switch (value(&style, .flex_direction).asKeyword()) {
147 .column, .column_reverse => .column,
148 else => .row,
149 };
150 record.layout.wrap = switch (value(&style, .flex_wrap).asKeyword()) {
151 .wrap, .wrap_reverse => .wrap,
152 else => .no_wrap,
153 };
154 record.layout.justify_content = switch (value(&style, .justify_content).asKeyword()) {
155 .flex_end, .end => .flex_end,
156 .center => .center,
157 .space_between => .space_between,
158 .space_around => .space_around,
159 .space_evenly => .space_evenly,
160 else => .flex_start,
161 };
162 record.layout.align_items = switch (value(&style, .align_items).asKeyword()) {
163 .flex_start, .start => .flex_start,
164 .flex_end, .end => .flex_end,
165 .center => .center,
166 else => .stretch,
167 };
168 record.layout.align_self = switch (value(&style, .align_self).asKeyword()) {
169 .flex_start, .start => .flex_start,
170 .flex_end, .end => .flex_end,
171 .center => .center,
172 .stretch => .stretch,
173 else => .auto,
174 };
175 record.layout.gap = length(value(&style, .row_gap), metrics, font, leading) orelse 0;
176 record.layout.padding = inset(&style, metrics, font, leading, .{ .padding_top, .padding_right, .padding_bottom, .padding_left });
177 record.layout.constraints = .{
178 .min_width = length(value(&style, .min_width), metrics, font, leading),
179 .min_height = length(value(&style, .min_height), metrics, font, leading),
180 .max_width = length(value(&style, .max_width), metrics, font, leading),
181 .max_height = length(value(&style, .max_height), metrics, font, leading),
182 };
183 record.layout.flex_grow = number(value(&style, .flex_grow), 0);
184 record.layout.flex_shrink = number(value(&style, .flex_shrink), 1);
185 record.layout.flex_basis = length(value(&style, .flex_basis), metrics, font, leading);
186 record.layout.position = if (value(&style, .position).asKeyword() == .absolute) .absolute else .relative;
187 record.layout.inset = inset(&style, metrics, font, leading, .{ .top, .right, .bottom, .left });
188 record.dimensions = .{
189 .width = length(value(&style, .width), metrics, font, leading),
190 .height = length(value(&style, .height), metrics, font, leading),
191 };
192
193 record.paint.foreground = color(value(&style, .color), .{});
194 record.paint.background = color(value(&style, .background_color), record.paint.foreground);
195 record.paint.border_color = .{
196 color(value(&style, .border_top_color), record.paint.foreground),
197 color(value(&style, .border_right_color), record.paint.foreground),
198 color(value(&style, .border_bottom_color), record.paint.foreground),
199 color(value(&style, .border_left_color), record.paint.foreground),
200 };
201 record.paint.outline_color = color(value(&style, .outline_color), record.paint.foreground);
202 record.paint.border_width = .{
203 length(value(&style, .border_top_width), metrics, font, leading) orelse 0,
204 length(value(&style, .border_right_width), metrics, font, leading) orelse 0,
205 length(value(&style, .border_bottom_width), metrics, font, leading) orelse 0,
206 length(value(&style, .border_left_width), metrics, font, leading) orelse 0,
207 };
208 record.paint.radius = .{
209 length(value(&style, .border_top_left_radius), metrics, font, leading) orelse 0,
210 length(value(&style, .border_top_right_radius), metrics, font, leading) orelse 0,
211 length(value(&style, .border_bottom_right_radius), metrics, font, leading) orelse 0,
212 length(value(&style, .border_bottom_left_radius), metrics, font, leading) orelse 0,
213 };
214 record.paint.border_style = .{
215 styleCode(value(&style, .border_top_style)),
216 styleCode(value(&style, .border_right_style)),
217 styleCode(value(&style, .border_bottom_style)),
218 styleCode(value(&style, .border_left_style)),
219 };
220 record.paint.outline_style = styleCode(value(&style, .outline_style));
221 record.paint.outline_width = length(value(&style, .outline_width), metrics, font, leading) orelse 0;
222 record.paint.outline_offset = length(value(&style, .outline_offset), metrics, font, leading) orelse 0;
223 record.paint.outset = if (record.paint.outline_style == 0) 0 else @max(0, record.paint.outline_width + record.paint.outline_offset);
224 record.paint.opacity = @intFromFloat(@round(std.math.clamp(number(value(&style, .opacity), 1), 0, 1) * 255));
225
226 record.text.size = font;
227 record.text.line_height = leading;
228 record.text.letter_spacing = length(value(&style, .letter_spacing), metrics, font, leading) orelse 0;
229 record.text.word_spacing = length(value(&style, .word_spacing), metrics, font, leading) orelse 0;
230 record.text.indent = length(value(&style, .text_indent), metrics, font, leading) orelse 0;
231 record.text.underline_offset = length(value(&style, .text_underline_offset), metrics, font, leading) orelse 0;
232 record.text.decoration_thickness = length(value(&style, .text_decoration_thickness), metrics, font, leading) orelse 0;
233 record.text.decoration_color = color(value(&style, .text_decoration_color), record.paint.foreground);
234 const weight = value(&style, .font_weight);
235 record.text.weight = @intFromFloat(std.math.clamp(switch (weight.asKeyword()) {
236 .bold, .bolder => @as(f32, 700),
237 .lighter => @as(f32, 300),
238 else => number(weight, 400),
239 }, 1, 1000));
240 record.text.flags = textFlags(&style);
241 if (fonts) |environment| {
242 const raw_family = value(&style, .font_family);
243 if (raw_family.valueKind() == .asset) {
244 const handle = asset.AssetHandle{ .index = raw_family.a, .generation = raw_family.b };
245 if (environment.source.registry.hasFont(handle)) record.text.face = handle;
246 } else {
247 const family = if (raw_family.valueKind() == .string and
248 raw_family.a <= environment.source.source.len and
249 raw_family.b <= environment.source.source.len - raw_family.a)
250 raw_family.asString(environment.source.source)
251 else
252 "";
253 const selection = asset.resolveAuthored(
254 environment.source.registry,
255 family,
256 record.text.weight,
257 @intCast((record.text.flags >> paint_mod.TextFlags.slant_shift) & 3),
258 environment.fallback_out,
259 ) catch unreachable;
260 if (selection.face) |face| record.text.face = face;
261 record.text.fallback_first = environment.first;
262 record.text.fallback_count = @intCast(selection.fallback.len);
263 }
264 }
265 record.text.text_align = @intCast(@backingInt(value(&style, .text_align).asKeyword()));
266 record.text.transform = @intCast(@backingInt(value(&style, .text_transform).asKeyword()));
267 record.text.decoration_line = @intCast(@backingInt(value(&style, .text_decoration_line).asKeyword()));
268 record.text.decoration_style = styleCode(value(&style, .text_decoration_style));
269 const z = number(value(&style, .z_index), 0);
270 record.layer = @intFromFloat(std.math.clamp(z, -32768, 32767));
271 return record;
272 }
273
274 test "lowering places outline reach and z index in computed records" {
275 var computed = css.cascade.Computed.initial();
276 computed.set(.outline_style, Value.keyword(.solid));
277 computed.set(.outline_width, Value.length(2, .px));
278 computed.set(.outline_offset, Value.length(2, .px));
279 computed.set(.z_index, Value.number(7));
280 const record = lower(computed, .{}, null);
281 try std.testing.expectEqual(@as(f32, 4), record.paint.outset);
282 try std.testing.expectEqual(@as(i16, 7), record.layer);
283 try std.testing.expectEqual(@as(u32, 0), record.text.face.index);
284 }
285
286 test "lowering fills all text flags and keyword weight" {
287 var source = css.cascade.Computed.initial();
288 source.set(.white_space, Value.keyword(.pre_wrap));
289 source.set(.word_break, Value.keyword(.keep_all));
290 source.set(.overflow_wrap, Value.keyword(.anywhere));
291 source.set(.direction, Value.keyword(.rtl));
292 source.set(.user_select, Value.keyword(.text));
293 source.set(.font_style, Value.keyword(.oblique));
294 source.set(.font_weight, Value.keyword(.bold));
295 const record = lower(source, .{}, null);
296 try std.testing.expectEqual(@as(u16, 700), record.text.weight);
297 try std.testing.expectEqual(@as(u16, 3 | (2 << 3) | (2 << 5) | (1 << 7) | (2 << 8) | (2 << 10)), record.text.flags);
298 }