Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion packages/desktop_ui/lib/desktop_ui/renderer/mapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule DesktopUi.Renderer.Mapper do
alias DesktopUi.Widget
alias UnifiedIUR.Element
alias UnifiedIUR.Element.Child
alias UnifiedIUR.Style, as: CanonicalStyle

@component_kinds UnifiedIUR.Widgets.Components.kinds()
@component_kind_values @component_kinds ++ Enum.map(@component_kinds, &Atom.to_string/1)
Expand Down Expand Up @@ -1274,13 +1275,107 @@ defmodule DesktopUi.Renderer.Mapper do

defp base_opts(element) do
[
styles: normalize_styles(attr(element, :styles)),
styles: canonical_styles(element),
metadata: metadata_opts(element),
disabled:
first_present([attr(element, :disabled), metadata_attr(element, :disabled)], false)
]
end

defp canonical_styles(%Element{} = element) do
element
|> attr(:styles)
|> normalize_styles()
|> Map.new()
|> Map.merge(translate_style_attachment(attr(element, :style)))
|> DesktopUi.Style.normalize()
end

defp translate_style_attachment(nil), do: %{}

defp translate_style_attachment(style) do
style = CanonicalStyle.new(style)
text = normalize_nested_map(style.text)
border = normalize_nested_map(style.border)

%{}
|> maybe_put(:fg, portable_color(style.foreground))
|> maybe_put(:bg, portable_color(style.background))
|> maybe_put(:attrs, text_attrs(text))
|> maybe_put(:border, border_style(border, style.border_color))
|> maybe_put(:padding, padding_style(style.spacing))
|> maybe_put(:tone, map_get(style.emphasis, :tone))
|> maybe_put(:semantic_role, map_get(style.emphasis, :role))
|> maybe_put(:state_variants, translate_state_variants(style.state_variants))
end

defp translate_state_variants(variants) when variants == %{}, do: nil

defp translate_state_variants(variants) do
variants
|> Map.new(fn {state, style} ->
{normalize_key(state), translate_style_attachment(style)}
end)
|> Enum.reject(fn {_state, styles} -> styles == %{} end)
|> Map.new()
|> case do
state_styles when state_styles == %{} -> nil
state_styles -> state_styles
end
end

defp border_style(border, color) do
border
|> maybe_put(:color, portable_color(color))
|> case do
values when values == %{} -> nil
values -> values
end
end

defp padding_style(spacing) do
spacing = normalize_nested_map(spacing)

spacing
|> Map.take([:padding, :padding_top, :padding_right, :padding_bottom, :padding_left])
|> case do
values when values == %{} -> nil
values -> values
end
end

defp text_attrs(text) do
text
|> Enum.reduce([], fn
{:bold?, true}, attrs -> [:bold | attrs]
{:italic?, true}, attrs -> [:italic | attrs]
{:underline?, true}, attrs -> [:underline | attrs]
{:strikethrough?, true}, attrs -> [:strikethrough | attrs]
_other, attrs -> attrs
end)
|> Enum.reverse()
|> case do
[] -> nil
attrs -> attrs
end
end

defp portable_color(nil), do: nil
defp portable_color(%{mode: :named, name: name}), do: name
defp portable_color(%{"mode" => :named, "name" => name}), do: name
defp portable_color(%{mode: :indexed, index: index}), do: {:indexed, index}
defp portable_color(%{"mode" => :indexed, "index" => index}), do: {:indexed, index}

defp portable_color(%{mode: :rgb, red: red, green: green, blue: blue}) do
"#" <> Base.encode16(<<red, green, blue>>, case: :lower)
end

defp portable_color(%{"mode" => :rgb, "red" => red, "green" => green, "blue" => blue}) do
"#" <> Base.encode16(<<red, green, blue>>, case: :lower)
end

defp portable_color(value), do: value

defp metadata_opts(element) do
[
label: label_text(element),
Expand Down Expand Up @@ -1449,6 +1544,9 @@ defmodule DesktopUi.Renderer.Mapper do
defp normalize_styles(styles) when is_map(styles), do: Map.to_list(styles)
defp normalize_styles(styles) when is_list(styles), do: styles

defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)

defp placeholder_child(id, role) do
Widget.new(:spacer, id: "#{id}-#{role}-placeholder")
end
Expand Down
37 changes: 37 additions & 0 deletions packages/desktop_ui/test/desktop_ui/renderer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule DesktopUi.RendererTest do

alias UnifiedIUR.Element
alias UnifiedIUR.Element.Child
alias UnifiedIUR.Style

test "renderer maps foundational canonical widgets and layouts into native desktop widgets" do
element =
Expand Down Expand Up @@ -48,6 +49,42 @@ defmodule DesktopUi.RendererTest do
assert DesktopUi.Renderer.validation_state() == :advanced_mapper_ready
end

test "renderer realizes CSS-derived canonical style without parsing authored CSS" do
element =
Element.new(:widget, :text,
id: "css-copy",
attributes: %{
content: "CSS-derived",
style:
Style.new(%{
foreground: "#2563eb",
background: "#eff6ff",
border_color: "#1d4ed8",
text: %{underline?: true},
spacing: %{padding_top: "4px"},
state_variants: %{focused: %{foreground: "#ffffff"}},
extra: %{
css: %{
properties: ["color"],
declarations: [%{selector: "#css-copy", property: "color"}]
}
}
})
}
)

assert {:ok, widget} = DesktopUi.Renderer.render(element)

assert widget.styles.fg == "#2563eb"
assert widget.styles.bg == "#eff6ff"
assert widget.styles.border == %{color: "#1d4ed8"}
assert widget.styles.attrs == [:underline]
assert widget.styles.padding == %{padding_top: "4px"}
assert widget.styles.state_variants.focused.fg == "#ffffff"
refute Map.has_key?(widget.styles, :css)
refute inspect(widget.styles) =~ "declarations"
end

test "renderer maps advanced canonical widgets, display systems, and layers into native desktop widgets" do
element =
Element.new(:layer, :multi_window,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ defmodule ElmUi.FrontendRuntime.StyleRealization do
css_vars:
%{}
|> maybe_put_var("--ui-tone", Map.get(resolved, :tone))
|> maybe_put_var("--ui-foreground", Map.get(resolved, :foreground))
|> maybe_put_var("--ui-surface", Map.get(resolved, :surface))
|> maybe_put_var("--ui-background", Map.get(resolved, :background))
|> maybe_put_var("--ui-border-color", Map.get(resolved, :border_color))
|> maybe_put_var("--ui-border", Map.get(resolved, :border))
|> maybe_put_var("--ui-emphasis", Map.get(resolved, :emphasis))
|> maybe_put_var("--ui-typography", Map.get(resolved, :typography)),
Expand Down
40 changes: 39 additions & 1 deletion packages/elm_ui/lib/elm_ui/renderer/canonical.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1576,14 +1576,23 @@ defmodule ElmUi.Renderer.Canonical do
defp translate_style_attachment(style) when style == %{}, do: %{}

defp translate_style_attachment(style) do
style = normalize_map(style)
style = UnifiedIUR.Style.new(style)
text = normalize_map(map_get(style, :text, %{}))
visibility = normalize_map(map_get(style, :visibility, %{}))
emphasis = normalize_map(map_get(style, :emphasis, %{}))
border = normalize_map(map_get(style, :border, %{}))
state_variants = normalize_map(map_get(style, :state_variants, %{}))

translated =
%{}
|> maybe_put(:foreground, portable_color(map_get(style, :foreground)))
|> maybe_put(:background, portable_color(map_get(style, :background)))
|> maybe_put(:border_color, portable_color(map_get(style, :border_color)))
|> maybe_put(:spacing, compact_map(normalize_map(map_get(style, :spacing, %{}))))
|> maybe_put(:sizing, compact_map(normalize_map(map_get(style, :sizing, %{}))))
|> maybe_put(:alignment, compact_map(normalize_map(map_get(style, :alignment, %{}))))
|> maybe_put(:border, if(border == %{}, do: nil, else: border))
|> maybe_put(:text, text_attrs(text))
|> maybe_put(:tone, map_get(emphasis, :tone))
|> maybe_put(:visibility, if(map_get(visibility, :hidden?) == true, do: :hidden, else: nil))
|> maybe_put(:emphasis, text_emphasis(text))
Expand Down Expand Up @@ -1638,6 +1647,35 @@ defmodule ElmUi.Renderer.Canonical do
end
end

defp text_attrs(text) do
text
|> Enum.filter(fn
{_key, true} -> true
_other -> false
end)
|> Map.new()
|> case do
attrs when attrs == %{} -> nil
attrs -> attrs
end
end

defp portable_color(nil), do: nil
defp portable_color(%{mode: :named, name: name}), do: name
defp portable_color(%{"mode" => :named, "name" => name}), do: name
defp portable_color(%{mode: :indexed, index: index}), do: {:indexed, index}
defp portable_color(%{"mode" => :indexed, "index" => index}), do: {:indexed, index}

defp portable_color(%{mode: :rgb, red: red, green: green, blue: blue}) do
"#" <> Base.encode16(<<red, green, blue>>, case: :lower)
end

defp portable_color(%{"mode" => :rgb, "red" => red, "green" => green, "blue" => blue}) do
"#" <> Base.encode16(<<red, green, blue>>, case: :lower)
end

defp portable_color(value), do: value

defp content_text(%Element{} = element, default \\ nil) do
first_present([group_attr(element, :content, :text), attr(element, :content)], default)
end
Expand Down
5 changes: 5 additions & 0 deletions packages/elm_ui/lib/elm_ui/style.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ defmodule ElmUi.Style do
:typography,
:tone,
:color_role,
:foreground,
:size,
:spacing,
:sizing,
:align,
:alignment,
:surface,
:background,
:border_color,
:border,
:text,
:visibility,
:emphasis
]
Expand Down
37 changes: 37 additions & 0 deletions packages/elm_ui/test/elm_ui/renderer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule ElmUi.RendererTest do
use ExUnit.Case, async: true

alias UnifiedIUR.Element
alias UnifiedIUR.Style
alias ElmUi.Renderer.Error
alias ElmUi.Widget

Expand All @@ -18,6 +19,42 @@ defmodule ElmUi.RendererTest do
assert ElmUi.Renderer.render(element) == ElmUi.Renderer.render(element)
end

test "renderer realizes CSS-derived canonical style without parsing authored CSS" do
element =
Element.new(:widget, :text,
id: :css_copy,
attributes: %{
content: "CSS-derived",
style:
Style.new(%{
foreground: "#2563eb",
background: "#eff6ff",
border_color: "#1d4ed8",
text: %{underline?: true},
spacing: %{padding_top: "4px"},
state_variants: %{focused: %{foreground: "#ffffff"}},
extra: %{
css: %{
properties: ["color"],
declarations: [%{selector: "#css-copy", property: "color"}]
}
}
})
}
)

assert {:ok, %Widget{} = widget} = ElmUi.Renderer.render(element)

assert widget.styles.foreground == "#2563eb"
assert widget.styles.background == "#eff6ff"
assert widget.styles.border_color == "#1d4ed8"
assert widget.styles.text == %{underline?: true}
assert widget.styles.spacing == %{padding_top: "4px"}
assert widget.styles.state_variants.focused.foreground == "#ffffff"
refute Map.has_key?(widget.styles, :css)
refute inspect(widget.styles) =~ "declarations"
end

test "renderer rejects unsupported canonical kinds with structured diagnostics" do
element =
Element.new(:widget, :timeline, id: :timeline_root, attributes: %{title: "Unsupported"})
Expand Down
Loading
Loading