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 }