From 26feb2d590337f4afd7593253a70964cd5a7ac31 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Sun, 14 Nov 2021 18:56:45 -0800 Subject: [PATCH 01/12] add support for theme libraries --- lib/scenic/component/button.ex | 14 +- lib/scenic/component/input/caret.ex | 4 +- lib/scenic/component/input/checkbox.ex | 6 +- lib/scenic/component/input/dropdown.ex | 6 +- lib/scenic/component/input/radio_button.ex | 6 +- lib/scenic/component/input/slider.ex | 6 +- lib/scenic/component/input/text_field.ex | 6 +- lib/scenic/component/input/toggle.ex | 4 +- lib/scenic/graph/compiler.ex | 4 +- lib/scenic/primitive/style/style.ex | 5 +- lib/scenic/primitive/style/theme.ex | 374 ++++++++++----------- lib/scenic/scenes/error.ex | 2 +- lib/scenic/themes.ex | 193 +++++++++++ lib/scenic/view_port.ex | 16 +- test/scenic/graph/compile_test.exs | 6 +- test/scenic/primitive/style/theme_test.exs | 108 +++--- test/scenic/scene_test.exs | 4 +- test/scenic/themes_test.exs | 122 +++++++ test/scenic/view_port_test.exs | 2 +- test/support/themes.ex | 6 + test/support/view_port.ex | 2 +- test/test_helper.exs | 2 + 22 files changed, 611 insertions(+), 287 deletions(-) create mode 100644 lib/scenic/themes.ex create mode 100644 test/scenic/themes_test.exs create mode 100644 test/support/themes.ex diff --git a/lib/scenic/component/button.ex b/lib/scenic/component/button.ex index 2591f6e1..51983d1c 100644 --- a/lib/scenic/component/button.ex +++ b/lib/scenic/component/button.ex @@ -131,7 +131,7 @@ defmodule Scenic.Component.Button do alias Scenic.Graph alias Scenic.Scene - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes alias Scenic.Assets.Static import Scenic.Primitives, only: [{:rrect, 3}, {:text, 3}, {:update_opts, 2}] @@ -164,12 +164,12 @@ defmodule Scenic.Component.Button do # theme is passed in as an inherited style theme = case opts[:theme] do - nil -> Theme.preset(:primary) - :dark -> Theme.preset(:primary) - :light -> Theme.preset(:primary) + nil -> Themes.preset({:scenic, :primary}) + {:scenic, :dark} -> Themes.preset({:scenic, :primary}) + {:scenic, :light} -> Themes.preset({:scenic, :primary}) theme -> theme end - |> Theme.normalize() + |> Themes.normalize() # font related info font = Keyword.get(styles, :font, @default_font) @@ -278,11 +278,11 @@ defmodule Scenic.Component.Button do ) end - defp do_special_theme_outline(graph, :dark, border) do + defp do_special_theme_outline(graph, {:scenic, :dark}, border) do Graph.modify(graph, :btn, &update_opts(&1, stroke: {1, border})) end - defp do_special_theme_outline(graph, :light, border) do + defp do_special_theme_outline(graph, {:scenic, :light}, border) do Graph.modify(graph, :btn, &update_opts(&1, stroke: {1, border})) end diff --git a/lib/scenic/component/input/caret.ex b/lib/scenic/component/input/caret.ex index 91c64890..61d5955d 100644 --- a/lib/scenic/component/input/caret.ex +++ b/lib/scenic/component/input/caret.ex @@ -47,7 +47,7 @@ defmodule Scenic.Component.Input.Caret do ] alias Scenic.Graph - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes @width 2 @inset_v 4 @@ -86,7 +86,7 @@ defmodule Scenic.Component.Input.Caret do case opts[:color] do nil -> opts[:theme] - |> Theme.normalize() + |> Themes.normalize() |> Map.get(:highlight) c -> diff --git a/lib/scenic/component/input/checkbox.ex b/lib/scenic/component/input/checkbox.ex index aed368a5..7a9d5318 100644 --- a/lib/scenic/component/input/checkbox.ex +++ b/lib/scenic/component/input/checkbox.ex @@ -54,7 +54,7 @@ defmodule Scenic.Component.Input.Checkbox do alias Scenic.Graph alias Scenic.Scene alias Scenic.Primitive - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes alias Scenic.Script alias Scenic.Assets.Static @@ -95,8 +95,8 @@ defmodule Scenic.Component.Input.Checkbox do # theme is passed in as an inherited style theme = - (opts[:theme] || Theme.preset(:dark)) - |> Theme.normalize() + (opts[:theme] || Themes.preset({:scenic, :dark})) + |> Themes.normalize() # font related info {:ok, {Static.Font, fm}} = Static.meta(@default_font) diff --git a/lib/scenic/component/input/dropdown.ex b/lib/scenic/component/input/dropdown.ex index 4a767975..110ef270 100644 --- a/lib/scenic/component/input/dropdown.ex +++ b/lib/scenic/component/input/dropdown.ex @@ -84,7 +84,7 @@ defmodule Scenic.Component.Input.Dropdown do alias Scenic.Graph alias Scenic.Scene - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes import Scenic.Primitives alias Scenic.Assets.Static @@ -188,8 +188,8 @@ defmodule Scenic.Component.Input.Dropdown do # theme is passed in as an inherited style theme = - (opts[:theme] || Theme.preset(:dark)) - |> Theme.normalize() + (opts[:theme] || Themes.preset(:dark)) + |> Themes.normalize() # font related info {:ok, {Static.Font, fm}} = Static.meta(@default_font) diff --git a/lib/scenic/component/input/radio_button.ex b/lib/scenic/component/input/radio_button.ex index 42e0bf1e..842a98f3 100644 --- a/lib/scenic/component/input/radio_button.ex +++ b/lib/scenic/component/input/radio_button.ex @@ -30,7 +30,7 @@ defmodule Scenic.Component.Input.RadioButton do alias Scenic.Scene alias Scenic.Graph alias Scenic.Primitive - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes alias Scenic.Assets.Static require Logger @@ -70,8 +70,8 @@ defmodule Scenic.Component.Input.RadioButton do def init(scene, {text, id, checked?}, opts) do # theme is passed in as an inherited style theme = - (opts[:theme] || Theme.preset(:dark)) - |> Theme.normalize() + (opts[:theme] || Themes.preset(:dark)) + |> Themes.normalize() # font related info {:ok, {Static.Font, fm}} = Static.meta(@default_font) diff --git a/lib/scenic/component/input/slider.ex b/lib/scenic/component/input/slider.ex index fe511735..4dc0f1d6 100644 --- a/lib/scenic/component/input/slider.ex +++ b/lib/scenic/component/input/slider.ex @@ -69,7 +69,7 @@ defmodule Scenic.Component.Input.Slider do use Scenic.Component, has_children: false alias Scenic.Graph - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes import Scenic.Primitives, only: [{:rect, 3}, {:line, 3}, {:rrect, 3}, {:update_opts, 2}] require Logger @@ -192,8 +192,8 @@ defmodule Scenic.Component.Input.Slider do # theme is passed in as an inherited style theme = - (opts[:theme] || Theme.preset(:primary)) - |> Theme.normalize() + (opts[:theme] || Themes.preset(:primary)) + |> Themes.normalize() # get button specific styles width = opts[:width] || @default_width diff --git a/lib/scenic/component/input/text_field.ex b/lib/scenic/component/input/text_field.ex index 1563476b..d7369eac 100644 --- a/lib/scenic/component/input/text_field.ex +++ b/lib/scenic/component/input/text_field.ex @@ -80,7 +80,7 @@ defmodule Scenic.Component.Input.TextField do alias Scenic.Graph alias Scenic.Component.Input.Caret - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes # alias Scenic.Assets.Static require Logger @@ -138,8 +138,8 @@ defmodule Scenic.Component.Input.TextField do # theme is passed in as an inherited style theme = - (opts[:theme] || Theme.preset(:dark)) - |> Theme.normalize() + (opts[:theme] || Themes.preset({:scenic, :dark})) + |> Themes.normalize() # get the text_field specific opts hint = opts[:hint] || @default_hint diff --git a/lib/scenic/component/input/toggle.ex b/lib/scenic/component/input/toggle.ex index 9c0aa0cd..dd659974 100644 --- a/lib/scenic/component/input/toggle.ex +++ b/lib/scenic/component/input/toggle.ex @@ -59,7 +59,7 @@ defmodule Scenic.Component.Input.Toggle do alias Scenic.Graph alias Scenic.Primitive alias Scenic.Primitive.Group - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes alias Scenic.ViewPort import Scenic.Primitives @@ -131,7 +131,7 @@ defmodule Scenic.Component.Input.Toggle do # theme is passed in as an inherited style theme = opts[:theme] - |> Theme.normalize() + |> Themes.normalize() # get toggle specific opts thumb_radius = Keyword.get(opts, :thumb_radius, @default_thumb_radius) diff --git a/lib/scenic/graph/compiler.ex b/lib/scenic/graph/compiler.ex index 45bebe88..6a6f5a65 100644 --- a/lib/scenic/graph/compiler.ex +++ b/lib/scenic/graph/compiler.ex @@ -12,7 +12,7 @@ defmodule Scenic.Graph.Compiler do alias Scenic.Primitive alias Scenic.Graph alias Scenic.Color - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes alias Scenic.Graph.Compiler # import IEx @@ -271,7 +271,7 @@ defmodule Scenic.Graph.Compiler do defp do_text_color(ops, %{reqs: %{theme: theme}} = state) do color = theme - |> Theme.normalize() + |> Themes.normalize() |> Map.get(:text) |> Color.to_rgba() diff --git a/lib/scenic/primitive/style/style.ex b/lib/scenic/primitive/style/style.ex index f709392d..2e79690d 100644 --- a/lib/scenic/primitive/style/style.ex +++ b/lib/scenic/primitive/style/style.ex @@ -49,6 +49,7 @@ defmodule Scenic.Primitive.Style do """ alias Scenic.Primitive.Style + alias Scenic.Themes # import IEx @@ -85,7 +86,7 @@ defmodule Scenic.Primitive.Style do :stroke => Style.Stroke, :text_align => Style.TextAlign, :text_base => Style.TextBase, - :theme => Style.Theme + :theme => Themes } @valid_styles @opts_map @@ -106,7 +107,7 @@ defmodule Scenic.Primitive.Style do stroke: [type: {:custom, Style.Stroke, :validate, []}], text_align: [type: {:custom, Style.TextAlign, :validate, []}], text_base: [type: {:custom, Style.TextBase, :validate, []}], - theme: [type: {:custom, Style.Theme, :validate, []}] + theme: [type: {:custom, Themes, :validate, []}] ] @callback validate(data :: any) :: {:ok, data :: any} | {:error, String.t()} diff --git a/lib/scenic/primitive/style/theme.ex b/lib/scenic/primitive/style/theme.ex index 6da4d952..ff636bc5 100644 --- a/lib/scenic/primitive/style/theme.ex +++ b/lib/scenic/primitive/style/theme.ex @@ -1,187 +1,187 @@ -# -# Created by Boyd Multerer on 2018-08-18. -# Copyright © 2018 Kry10 Limited. All rights reserved. -# - -defmodule Scenic.Primitive.Style.Theme do - @moduledoc """ - Themes are a way to bundle up a set of colors that are intended to be used - by components invoked by a scene. - - There are a set of pre-defined themes. - You can also pass in a map of color values. - - Unlike other styles, The currently set theme is given to child components. - Each component gets to pick, choose, or ignore any colors in a given style. - - ### Predefined Themes - * `:dark` - This is the default and most common. Use when the background is dark. - * `:light` - Use when the background is light colored. - - ### Specialty Themes - - The remaining themes are designed to color the standard components and don't really - make much sense when applied to the root of a graph. You could, but it would be... - interesting. - - The most obvious place to use them is with [`Button`](Scenic.Component.Button.html) - components. - - * `:primary` - Blue background. This is the primary button type indicator. - * `:secondary` - Grey background. Not primary type indicator. - * `:success` - Green background. - * `:danger` - Red background. Use for irreversible or dangerous actions. - * `:warning` - Orange background. - * `:info` - Lightish blue background. - * `:text` - Transparent background. - """ - - use Scenic.Primitive.Style - alias Scenic.Primitive.Style.Paint.Color - - @theme_light %{ - text: :black, - background: :white, - border: :dark_grey, - active: {215, 215, 215}, - thumb: :cornflower_blue, - focus: :blue, - highlight: :saddle_brown - } - - @theme_dark %{ - text: :white, - background: :black, - border: :light_grey, - active: {40, 40, 40}, - thumb: :cornflower_blue, - focus: :cornflower_blue, - highlight: :sandy_brown - } - - # specialty themes - @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) - @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) - @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) - @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) - @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) - @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) - @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) - - @themes %{ - light: @theme_light, - dark: @theme_dark, - primary: @primary, - secondary: @secondary, - success: @success, - danger: @danger, - warning: @warning, - info: @info, - text: @text - } - - # ============================================================================ - # data verification and serialization - @doc false - def validate(theme) - def validate(:light), do: {:ok, :light} - def validate(:dark), do: {:ok, :dark} - def validate(:primary), do: {:ok, :primary} - def validate(:secondary), do: {:ok, :secondary} - def validate(:success), do: {:ok, :success} - def validate(:danger), do: {:ok, :danger} - def validate(:warning), do: {:ok, :warning} - def validate(:info), do: {:ok, :info} - def validate(:text), do: {:ok, :text} - - def validate( - %{ - text: _, - background: _, - border: _, - active: _, - thumb: _, - focus: _ - } = theme - ) do - # we know all the required colors are there. - # now make sure they are all valid colors, including any custom added ones. - theme - |> Enum.reduce({:ok, theme}, fn - _, {:error, msg} -> - {:error, msg} - - {key, color}, {:ok, _} = acc -> - case Color.validate(color) do - {:ok, _} -> acc - {:error, msg} -> err_color(key, msg) - end - end) - end - - def validate(name) when is_atom(name) do - { - :error, - """ - #{IO.ANSI.red()}Invalid theme name - Received: #{inspect(name)} - #{IO.ANSI.yellow()} - Named themes must be from the following list: - :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text#{IO.ANSI.default_color()} - """ - } - end - - def validate(%{} = map) do - { - :error, - """ - #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(map)} - #{IO.ANSI.yellow()} - You passed in a map, but it didn't include all the required color specifications. - It must contain a valid color for each of the following entries. - :text, :background, :border, :active, :thumb, :focus - #{IO.ANSI.default_color()} - """ - } - end - - def validate(data) do - { - :error, - """ - #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(data)} - #{IO.ANSI.yellow()} - Themes can be a name from this list: - :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text - - Or it may also be a map defining colors for the values of - :text, :background, :border, :active, :thumb, :focus - - If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} - """ - } - end - - defp err_color(key, msg) do - { - :error, - """ - #{IO.ANSI.red()}Invalid color in map - Map entry: #{inspect(key)} - #{msg} - """ - } - end - - # -------------------------------------------------------- - @doc false - def normalize(theme) when is_atom(theme), do: Map.get(@themes, theme) - def normalize(theme) when is_map(theme), do: theme - - # -------------------------------------------------------- - @doc false - def preset(theme), do: Map.get(@themes, theme) -end +# # +# # Created by Boyd Multerer on 2018-08-18. +# # Copyright © 2018 Kry10 Limited. All rights reserved. +# # + +# defmodule Scenic.Primitive.Style.Theme do +# @moduledoc """ +# Themes are a way to bundle up a set of colors that are intended to be used +# by components invoked by a scene. + +# There are a set of pre-defined themes. +# You can also pass in a map of color values. + +# Unlike other styles, The currently set theme is given to child components. +# Each component gets to pick, choose, or ignore any colors in a given style. + +# ### Predefined Themes +# * `:dark` - This is the default and most common. Use when the background is dark. +# * `:light` - Use when the background is light colored. + +# ### Specialty Themes + +# The remaining themes are designed to color the standard components and don't really +# make much sense when applied to the root of a graph. You could, but it would be... +# interesting. + +# The most obvious place to use them is with [`Button`](Scenic.Component.Button.html) +# components. + +# * `:primary` - Blue background. This is the primary button type indicator. +# * `:secondary` - Grey background. Not primary type indicator. +# * `:success` - Green background. +# * `:danger` - Red background. Use for irreversible or dangerous actions. +# * `:warning` - Orange background. +# * `:info` - Lightish blue background. +# * `:text` - Transparent background. +# """ + +# use Scenic.Primitive.Style +# alias Scenic.Primitive.Style.Paint.Color + +# @theme_light %{ +# text: :black, +# background: :white, +# border: :dark_grey, +# active: {215, 215, 215}, +# thumb: :cornflower_blue, +# focus: :blue, +# highlight: :saddle_brown +# } + +# @theme_dark %{ +# text: :white, +# background: :black, +# border: :light_grey, +# active: {40, 40, 40}, +# thumb: :cornflower_blue, +# focus: :cornflower_blue, +# highlight: :sandy_brown +# } + +# # specialty themes +# @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) +# @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) +# @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) +# @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) +# @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) +# @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) +# @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) + +# @themes %{ +# light: @theme_light, +# dark: @theme_dark, +# primary: @primary, +# secondary: @secondary, +# success: @success, +# danger: @danger, +# warning: @warning, +# info: @info, +# text: @text +# } + +# # ============================================================================ +# # data verification and serialization +# @doc false +# def validate(theme) +# def validate(:light), do: {:ok, :light} +# def validate(:dark), do: {:ok, :dark} +# def validate(:primary), do: {:ok, :primary} +# def validate(:secondary), do: {:ok, :secondary} +# def validate(:success), do: {:ok, :success} +# def validate(:danger), do: {:ok, :danger} +# def validate(:warning), do: {:ok, :warning} +# def validate(:info), do: {:ok, :info} +# def validate(:text), do: {:ok, :text} + +# def validate( +# %{ +# text: _, +# background: _, +# border: _, +# active: _, +# thumb: _, +# focus: _ +# } = theme +# ) do +# # we know all the required colors are there. +# # now make sure they are all valid colors, including any custom added ones. +# theme +# |> Enum.reduce({:ok, theme}, fn +# _, {:error, msg} -> +# {:error, msg} + +# {key, color}, {:ok, _} = acc -> +# case Color.validate(color) do +# {:ok, _} -> acc +# {:error, msg} -> err_color(key, msg) +# end +# end) +# end + +# def validate(name) when is_atom(name) do +# { +# :error, +# """ +# #{IO.ANSI.red()}Invalid theme name +# Received: #{inspect(name)} +# #{IO.ANSI.yellow()} +# Named themes must be from the following list: +# :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text#{IO.ANSI.default_color()} +# """ +# } +# end + +# def validate(%{} = map) do +# { +# :error, +# """ +# #{IO.ANSI.red()}Invalid theme specification +# Received: #{inspect(map)} +# #{IO.ANSI.yellow()} +# You passed in a map, but it didn't include all the required color specifications. +# It must contain a valid color for each of the following entries. +# :text, :background, :border, :active, :thumb, :focus +# #{IO.ANSI.default_color()} +# """ +# } +# end + +# def validate(data) do +# { +# :error, +# """ +# #{IO.ANSI.red()}Invalid theme specification +# Received: #{inspect(data)} +# #{IO.ANSI.yellow()} +# Themes can be a name from this list: +# :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text + +# Or it may also be a map defining colors for the values of +# :text, :background, :border, :active, :thumb, :focus + +# If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} +# """ +# } +# end + +# defp err_color(key, msg) do +# { +# :error, +# """ +# #{IO.ANSI.red()}Invalid color in map +# Map entry: #{inspect(key)} +# #{msg} +# """ +# } +# end + +# # -------------------------------------------------------- +# @doc false +# def normalize(theme) when is_atom(theme), do: Map.get(@themes, theme) +# def normalize(theme) when is_map(theme), do: theme + +# # -------------------------------------------------------- +# @doc false +# def preset(theme), do: Map.get(@themes, theme) +# end diff --git a/lib/scenic/scenes/error.ex b/lib/scenic/scenes/error.ex index 41503bb1..fbece629 100644 --- a/lib/scenic/scenes/error.ex +++ b/lib/scenic/scenes/error.ex @@ -66,7 +66,7 @@ defmodule Scenic.Scenes.Error do graph = Graph.build(font: @default_font, font_size: @size, translate: {@margin_h, @margin_v}) - |> button("Try Again", id: :try_again, theme: :warning) + |> button("Try Again", id: :try_again, theme: {:scenic, :warning}) |> text(head_msg, translate: {0, head_v}, font_size: @size + 4) |> text(args_msg, translate: {0, args_v}, fill: @args_color) |> text(err_msg, translate: {0, err_v}, fill: @error_color) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex new file mode 100644 index 00000000..db7a5ff4 --- /dev/null +++ b/lib/scenic/themes.ex @@ -0,0 +1,193 @@ +defmodule Scenic.Themes do + @theme_light %{ + text: :black, + background: :white, + border: :dark_grey, + active: {215, 215, 215}, + thumb: :cornflower_blue, + focus: :blue, + highlight: :saddle_brown + } + + @theme_dark %{ + text: :white, + background: :black, + border: :light_grey, + active: {40, 40, 40}, + thumb: :cornflower_blue, + focus: :cornflower_blue, + highlight: :sandy_brown + } + + @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) + @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) + @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) + @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) + @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) + @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) + @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) + @themes %{ + light: @theme_light, + dark: @theme_dark, + primary: @primary, + secondary: @secondary, + success: @success, + danger: @danger, + warning: @warning, + info: @info, + text: @text + } + + @callback load() :: list + + defmacro __using__(using_opts \\ []) do + quote do + alias Scenic.Primitive.Style.Paint.Color + @behaviour Scenic.Themes + @sources Keyword.get(unquote(using_opts), :sources, []) + @library_themes Enum.reduce(@sources, %{}, fn + {lib, module}, acc when is_atom(module) -> + themes = module.load() + Map.put_new(acc, lib, themes) + {lib, themes}, acc -> + Map.put_new(acc, lib, themes) + _, acc -> acc + end) + + def validate(theme) + def validate({lib, theme} = lib_theme) when is_atom(theme) do + case normalize(lib_theme) do + map when is_map(map) -> + {:ok, lib_theme} + nil -> + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(lib_theme)} + #{IO.ANSI.yellow()} + You passed in a tuple representing a library theme, but it could not be found. + Please ensure you've imported the the library correctly in your Themes module. + #{IO.ANSI.default_color()} + """ + } + end + end + + def validate( + %{ + text: _, + background: _, + border: _, + active: _, + thumb: _, + focus: _ + } = theme + ) do + # we know all the required colors are there. + # now make sure they are all valid colors, including any custom added ones. + theme + |> Enum.reduce({:ok, theme}, fn + _, {:error, msg} -> + {:error, msg} + + {key, color}, {:ok, _} = acc -> + case Color.validate(color) do + {:ok, _} -> acc + {:error, msg} -> err_color(key, msg) + end + end) + end + + def validate(%{} = map) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(map)} + #{IO.ANSI.yellow()} + You passed in a map, but it didn't include all the required color specifications. + It must contain a valid color for each of the following entries. + :text, :background, :border, :active, :thumb, :focus + #{IO.ANSI.default_color()} + """ + } + end + + def validate(data) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(data)} + #{IO.ANSI.yellow()} + Themes can be a tuple represent a theme for example: + {:scenic, :light}, {:scenic, :dark} + + Or it may also be a map defining colors for the values of + :text, :background, :border, :active, :thumb, :focus + + If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} + """ + } + end + + defp err_color(key, msg) do + { + :error, + """ + #{IO.ANSI.red()}Invalid color in map + Map entry: #{inspect(key)} + #{msg} + """ + } + end + + @doc false + def normalize({lib, theme}) when is_atom(theme), do: Map.get(Map.get(@library_themes, lib), theme) + def normalize(theme) when is_map(theme), do: theme + + @doc false + def preset({lib, theme}), do: Map.get(Map.get(@library_themes, lib), theme) + end + end + + @moduledoc false + def module() do + with {:ok, config} <- Application.fetch_env(:scenic, :themes), + {:ok, module} <- Keyword.fetch(config, :module) do + module + else + _ -> + raise """ + No Themes module is configured. + You need to create themes module in your application. + Then connect it to Scenic with some config. + + Example Themes module that includes an optional alias: + + defmodule MyApplication.Themes do + use Scenic.Assets.Static, + otp_app: :my_application, + alias: [ + scenic: Scenic.Themes, + ] + end + + Example configuration script (this goes in your config.exs file): + + config :scenic, :themes, + module: MyApplication.Themes + """ + end + end + + def validate(theme), do: module().validate(theme) + + def normalize(theme), do: module().normalize(theme) + + def preset(theme), do: module().preset(theme) + + @doc false + def load(), do: @themes +end diff --git a/lib/scenic/view_port.ex b/lib/scenic/view_port.ex index 4f9c5636..1e3cd155 100644 --- a/lib/scenic/view_port.ex +++ b/lib/scenic/view_port.ex @@ -19,7 +19,7 @@ defmodule Scenic.ViewPort do # alias Scenic.Utilities alias Scenic.Utilities.Validators - alias Scenic.Primitive.Style.Theme + alias Scenic.Themes require Logger @@ -118,7 +118,7 @@ defmodule Scenic.ViewPort do required: true, type: {:custom, Validators, :validate_scene, [:default_scene]} ], - theme: [type: {:custom, Theme, :validate, []}, default: :dark], + theme: [type: {:custom, Themes, :validate, []}, default: {:scenic, :dark}], drivers: [type: {:custom, Driver, :validate, []}, default: []], input_filter: [type: {:custom, __MODULE__, :validate_input_filter, []}, default: :all], opts: [ @@ -395,7 +395,7 @@ defmodule Scenic.ViewPort do def set_theme(viewport, theme) def set_theme(%ViewPort{pid: pid}, theme) do - case Theme.validate(theme) do + case Themes.validate(theme) do # {:ok, theme} -> GenServer.cast( pid, {:set_theme, theme} ) {:ok, theme} -> GenServer.call(pid, {:set_theme, theme}) err -> err @@ -516,7 +516,7 @@ defmodule Scenic.ViewPort do # ets table for scripts. Public. Readable and Writable by others. The intended # use is that Scenes compile graphs in their own process and insert the scripts # in parallel to each other. (Trying to avoid serializing the VP on large messages) - # containing either script of graph data. The scripts can be read by multiple + # containing either script of graph data. The scripts can be read by multiple # drivers at the same time, so is read parallel optimized. If the public write # becomes problematic, the next step is to have the scripts compile, then send # finished scripts to the VP for writing. @@ -847,7 +847,7 @@ defmodule Scenic.ViewPort do # get the background from the theme background = theme - |> Theme.normalize() + |> Themes.normalize() |> Map.get(:background) send(pid, {@clear_color, background}) @@ -1171,7 +1171,7 @@ defmodule Scenic.ViewPort do background = theme - |> Theme.normalize() + |> Themes.normalize() |> Map.get(:background) case DynamicSupervisor.start_child(driver_sup, {Driver, {info, opts}}) do @@ -1188,7 +1188,7 @@ defmodule Scenic.ViewPort do # get the background from the theme background = theme - |> Theme.normalize() + |> Themes.normalize() |> Map.get(:background) # tell the drivers the background changed @@ -1641,7 +1641,7 @@ defmodule Scenic.ViewPort do # skip script primitives - no input handlers there defp comp_input_prim(input, _uid, %Primitive{module: Primitive.Script}, _, _tx), do: input - # it is a group. Calc the local transform if there one, but doesn't go into the + # it is a group. Calc the local transform if there one, but doesn't go into the # list as a component itself... defp comp_input_prim( input, diff --git a/test/scenic/graph/compile_test.exs b/test/scenic/graph/compile_test.exs index c65f7ed7..ba3b3fba 100644 --- a/test/scenic/graph/compile_test.exs +++ b/test/scenic/graph/compile_test.exs @@ -76,7 +76,7 @@ defmodule Scenic.Graph.CompilerTest do # --------------------------------------------------------- test "simple_theme graph works" do {:ok, list} = - Graph.build(font: :roboto, font_size: 26, theme: :dark) + Graph.build(font: :roboto, font_size: 26, theme: {:scenic, :dark}) |> text("theme") |> Compiler.compile() @@ -250,7 +250,7 @@ defmodule Scenic.Graph.CompilerTest do end # --------------------------------------------------------- - # Should correctly compile fill and stroke. Note that + # Should correctly compile fill and stroke. Note that # primitives with neither fill nor stroke are eliminated completely test "fill and stroke are compiled correctly" do {:ok, list} = @@ -380,7 +380,7 @@ defmodule Scenic.Graph.CompilerTest do # --------------------------------------------------------- test "font_styles graph works" do {:ok, list} = - Graph.build(theme: :dark) + Graph.build(theme: {:scenic, :dark}) |> text("roboto", font: :roboto) |> text("size_64", font_size: 64) |> text("left", text_align: :left) diff --git a/test/scenic/primitive/style/theme_test.exs b/test/scenic/primitive/style/theme_test.exs index ee811847..95479c92 100644 --- a/test/scenic/primitive/style/theme_test.exs +++ b/test/scenic/primitive/style/theme_test.exs @@ -3,66 +3,66 @@ # Copyright © 2018-2021 Kry10 Limited. All rights reserved. # -defmodule Scenic.Primitive.Style.ThemeTest do - use ExUnit.Case, async: true - doctest Scenic.Primitive.Style.Theme +# defmodule Scenic.Primitive.Style.ThemeTest do +# use ExUnit.Case, async: true +# doctest Scenic.Primitive.Style.Theme - alias Scenic.Primitive.Style.Theme +# alias Scenic.Primitive.Style.Theme - test "validate accepts the named themes" do - assert Theme.validate(:dark) == {:ok, :dark} - assert Theme.validate(:light) == {:ok, :light} - assert Theme.validate(:primary) == {:ok, :primary} - assert Theme.validate(:secondary) == {:ok, :secondary} - assert Theme.validate(:success) == {:ok, :success} - assert Theme.validate(:danger) == {:ok, :danger} - assert Theme.validate(:warning) == {:ok, :warning} - assert Theme.validate(:info) == {:ok, :info} - assert Theme.validate(:text) == {:ok, :text} - end +# test "validate accepts the named themes" do +# assert Theme.validate(:dark) == {:ok, :dark} +# assert Theme.validate(:light) == {:ok, :light} +# assert Theme.validate(:primary) == {:ok, :primary} +# assert Theme.validate(:secondary) == {:ok, :secondary} +# assert Theme.validate(:success) == {:ok, :success} +# assert Theme.validate(:danger) == {:ok, :danger} +# assert Theme.validate(:warning) == {:ok, :warning} +# assert Theme.validate(:info) == {:ok, :info} +# assert Theme.validate(:text) == {:ok, :text} +# end - test "validate rejects invalid theme names" do - {:error, msg} = Theme.validate(:invalid) - assert msg =~ "Named themes must be from the following list" - end +# test "validate rejects invalid theme names" do +# {:error, msg} = Theme.validate(:invalid) +# assert msg =~ "Named themes must be from the following list" +# end - test "validate accepts maps of colors" do - color_map = %{ - text: :red, - background: :green, - border: :blue, - active: :magenta, - thumb: :cyan, - focus: :yellow, - my_color: :black - } +# test "validate accepts maps of colors" do +# color_map = %{ +# text: :red, +# background: :green, +# border: :blue, +# active: :magenta, +# thumb: :cyan, +# focus: :yellow, +# my_color: :black +# } - assert Theme.validate(color_map) == {:ok, color_map} - end +# assert Theme.validate(color_map) == {:ok, color_map} +# end - test "validate rejects maps with invalid colors" do - color_map = %{ - text: :red, - background: :green, - border: :invalid, - active: :magenta, - thumb: :cyan, - focus: :yellow, - my_color: :black - } +# test "validate rejects maps with invalid colors" do +# color_map = %{ +# text: :red, +# background: :green, +# border: :invalid, +# active: :magenta, +# thumb: :cyan, +# focus: :yellow, +# my_color: :black +# } - {:error, msg} = Theme.validate(color_map) - assert msg =~ "Map entry: :border" - assert msg =~ "Invalid Color specification: :invalid" - end +# {:error, msg} = Theme.validate(color_map) +# assert msg =~ "Map entry: :border" +# assert msg =~ "Invalid Color specification: :invalid" +# end - test "verify rejects maps without the standard colors" do - color_map = %{some_name: :red} - {:error, msg} = Theme.validate(color_map) - assert msg =~ "didn't include all the required color" - end +# test "verify rejects maps without the standard colors" do +# color_map = %{some_name: :red} +# {:error, msg} = Theme.validate(color_map) +# assert msg =~ "didn't include all the required color" +# end - test "verify rejects invalid values" do - {:error, _msg} = Theme.validate("totally wrong") - end -end +# test "verify rejects invalid values" do +# {:error, _msg} = Theme.validate("totally wrong") +# end +# end diff --git a/test/scenic/scene_test.exs b/test/scenic/scene_test.exs index 545a45d3..95f6f458 100644 --- a/test/scenic/scene_test.exs +++ b/test/scenic/scene_test.exs @@ -134,7 +134,7 @@ defmodule Scenic.SceneTest do %ViewPort{} = vp assert is_pid(pid) assert module == TestSceneNoKids - assert theme == :dark + assert theme == {:scenic, :dark} assert is_pid(parent) assert children == nil assert child_supervisor == nil @@ -167,7 +167,7 @@ defmodule Scenic.SceneTest do %ViewPort{} = vp assert is_pid(pid) assert module == TestSceneKids - assert theme == :dark + assert theme == {:scenic, :dark} assert is_pid(parent) %{} = children assert is_pid(child_supervisor) diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs new file mode 100644 index 00000000..cc515f6f --- /dev/null +++ b/test/scenic/themes_test.exs @@ -0,0 +1,122 @@ +defmodule Scenic.ThemesTest do + use ExUnit.Case, async: true + doctest Scenic.Themes + + alias Scenic.Themes + + # we expect errors to be logged in this set of tests. This happens when we purposefully + # attempted to load an asset that has been tampered with. So turn off the logging to + # keep the tests clean. + @moduletag :capture_log + + @theme_light %{ + text: :black, + background: :white, + border: :dark_grey, + active: {215, 215, 215}, + thumb: :cornflower_blue, + focus: :blue, + highlight: :saddle_brown + } + + @theme_dark %{ + text: :white, + background: :black, + border: :light_grey, + active: {40, 40, 40}, + thumb: :cornflower_blue, + focus: :cornflower_blue, + highlight: :sandy_brown + } + + @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) + @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) + @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) + @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) + @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) + @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) + @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) + + @themes %{ + light: @theme_light, + dark: @theme_dark, + primary: @primary, + secondary: @secondary, + success: @success, + danger: @danger, + warning: @warning, + info: @info, + text: @text + } + + # import IEx + + test "module returns the configured library module" do + assert Themes.module() == Scenic.Test.Themes + end + + test "load returns the themes" do + assert Themes.load() == @themes + end + + test "normalize returns the correct theme" do + assert Themes.normalize({:scenic, :dark}) == @theme_dark + end + + test "validate accepts the named themes" do + assert Themes.validate({:scenic, :dark}) == {:ok, {:scenic, :dark}} + assert Themes.validate({:scenic, :light}) == {:ok, {:scenic, :light}} + assert Themes.validate({:scenic, :primary}) == {:ok, {:scenic, :primary}} + assert Themes.validate({:scenic, :secondary}) == {:ok, {:scenic, :secondary}} + assert Themes.validate({:scenic, :success}) == {:ok, {:scenic, :success}} + assert Themes.validate({:scenic, :danger}) == {:ok, {:scenic, :danger}} + assert Themes.validate({:scenic, :warning}) == {:ok, {:scenic, :warning}} + assert Themes.validate({:scenic, :info}) == {:ok, {:scenic, :info}} + assert Themes.validate({:scenic, :text}) == {:ok, {:scenic, :text}} + end + + test "validate rejects invalid theme names" do + {:error, msg} = Themes.validate(:invalid) + assert msg =~ "Themes can be a tuple represent a theme for example:" + end + + test "validate accepts maps of colors" do + color_map = %{ + text: :red, + background: :green, + border: :blue, + active: :magenta, + thumb: :cyan, + focus: :yellow, + my_color: :black + } + + assert Themes.validate(color_map) == {:ok, color_map} + end + + test "validate rejects maps with invalid colors" do + color_map = %{ + text: :red, + background: :green, + border: :invalid, + active: :magenta, + thumb: :cyan, + focus: :yellow, + my_color: :black + } + + {:error, msg} = Themes.validate(color_map) + assert msg =~ "Map entry: :border" + assert msg =~ "Invalid Color specification: :invalid" + end + + test "verify rejects maps without the standard colors" do + color_map = %{some_name: :red} + {:error, msg} = Themes.validate(color_map) + assert msg =~ "didn't include all the required color" + end + + test "verify rejects invalid values" do + {:error, _msg} = Themes.validate("totally wrong") + end +end diff --git a/test/scenic/view_port_test.exs b/test/scenic/view_port_test.exs index 48ae744d..88390905 100644 --- a/test/scenic/view_port_test.exs +++ b/test/scenic/view_port_test.exs @@ -266,7 +266,7 @@ defmodule Scenic.ViewPortTest do # set the theme - this should restart the current scene test "set_theme works", %{vp: vp} do - assert ViewPort.set_theme(vp, :dark) == :ok + assert ViewPort.set_theme(vp, {:scenic, :dark}) == :ok end # --------------------------------------------------------------------------- diff --git a/test/support/themes.ex b/test/support/themes.ex new file mode 100644 index 00000000..5cd27492 --- /dev/null +++ b/test/support/themes.ex @@ -0,0 +1,6 @@ +defmodule Scenic.Test.Themes do + use Scenic.Themes, + sources: [ + {:scenic, Scenic.Themes} + ] +end diff --git a/test/support/view_port.ex b/test/support/view_port.ex index 8a5ea6a4..fc885535 100644 --- a/test/support/view_port.ex +++ b/test/support/view_port.ex @@ -26,7 +26,7 @@ defmodule Scenic.Test.ViewPort do ) scene_state = %{ - theme: :dark, + theme: {:scenic, :dark}, has_children: true, name: nil, module: __MODULE__, diff --git a/test/test_helper.exs b/test/test_helper.exs index e15d1f04..d557f56c 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1,6 @@ # dynamically update the config to point to the test assets Application.put_env(:scenic, :assets, module: Scenic.Test.Assets) +Application.put_env(:scenic, :themes, module: Scenic.Test.Themes) + ExUnit.start() From c2816381cc931984da69bcdcee5a57427a44bf11 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Sun, 14 Nov 2021 19:06:33 -0800 Subject: [PATCH 02/12] add some documentation for themes --- lib/scenic/themes.ex | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index db7a5ff4..36e7c0fd 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -1,4 +1,48 @@ defmodule Scenic.Themes do + @moduledoc """ + Manages theme libraries by registering your map of themes to a library key. + By registering themes in this way you can safely pull in themes from external libraries, + without theme names colliding, as well as get all the validation. + + ### Required Configuration + Setting up themes requires some initial setup. + + Example: + + ```elixir + defmodule MyApplication.Themes do + @themes %{ + light: @theme_light, + dark: @theme_dark, + primary: @primary, + secondary: @secondary, + success: @success, + danger: @danger, + warning: @warning, + info: @info, + text: @text + } + + use Scenic.Themes, + sources: [ + {:scenic, Scenic.Themes"}, + {:my_app, load()} + ] + + def load(), do: @themes + end + ``` + + After the Themes modules has been defined you need to configure it in your config file.any() + + ```elixir + config :scenic, :themes, + module: MyApplication.Themes + ``` + + Now themes are passed around scenic in the form of `{:library_name, :theme_name}` as opposed to just :theme_name. + """ + @theme_light %{ text: :black, background: :white, From 5d4fa6f21e9217739d88f5d109f0859ff5532cde Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Sun, 14 Nov 2021 23:09:18 -0800 Subject: [PATCH 03/12] update theme signature for missed components --- lib/scenic/component/input/dropdown.ex | 2 +- lib/scenic/component/input/radio_button.ex | 2 +- lib/scenic/component/input/slider.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/scenic/component/input/dropdown.ex b/lib/scenic/component/input/dropdown.ex index 110ef270..b388085e 100644 --- a/lib/scenic/component/input/dropdown.ex +++ b/lib/scenic/component/input/dropdown.ex @@ -188,7 +188,7 @@ defmodule Scenic.Component.Input.Dropdown do # theme is passed in as an inherited style theme = - (opts[:theme] || Themes.preset(:dark)) + (opts[:theme] || Themes.preset({:scenic, :dark})) |> Themes.normalize() # font related info diff --git a/lib/scenic/component/input/radio_button.ex b/lib/scenic/component/input/radio_button.ex index 842a98f3..990a86a3 100644 --- a/lib/scenic/component/input/radio_button.ex +++ b/lib/scenic/component/input/radio_button.ex @@ -70,7 +70,7 @@ defmodule Scenic.Component.Input.RadioButton do def init(scene, {text, id, checked?}, opts) do # theme is passed in as an inherited style theme = - (opts[:theme] || Themes.preset(:dark)) + (opts[:theme] || Themes.preset({:scenic, :dark})) |> Themes.normalize() # font related info diff --git a/lib/scenic/component/input/slider.ex b/lib/scenic/component/input/slider.ex index 4dc0f1d6..0bb5a4a5 100644 --- a/lib/scenic/component/input/slider.ex +++ b/lib/scenic/component/input/slider.ex @@ -192,7 +192,7 @@ defmodule Scenic.Component.Input.Slider do # theme is passed in as an inherited style theme = - (opts[:theme] || Themes.preset(:primary)) + (opts[:theme] || Themes.preset({:scenic, :dark})) |> Themes.normalize() # get button specific styles From 2ccac95c8269164640d5017fb5f300247159fe61 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Mon, 15 Nov 2021 13:28:26 -0800 Subject: [PATCH 04/12] add custom validation mechanism. --- lib/scenic/themes.ex | 179 +++++++++++++++++++++++----------- test/scenic/themes_test.exs | 18 ++++ test/support/custom_themes.ex | 62 ++++++++++++ test/support/themes.ex | 42 +++++++- 4 files changed, 244 insertions(+), 57 deletions(-) create mode 100644 test/support/custom_themes.ex diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index 36e7c0fd..b99d4b87 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -4,6 +4,11 @@ defmodule Scenic.Themes do By registering themes in this way you can safely pull in themes from external libraries, without theme names colliding, as well as get all the validation. + You can add additional keys to be validated on your custom themes by returning a tuple with your map of themes and a list of keys to be validated + from your load function. + + All themes will validate against the default schema. If you provide additional keys, the list will get merged with the list of default keys. + ### Required Configuration Setting up themes requires some initial setup. @@ -23,13 +28,15 @@ defmodule Scenic.Themes do text: @text } + schema [:surface] # add additional required keys to your theme + use Scenic.Themes, sources: [ {:scenic, Scenic.Themes"}, {:my_app, load()} ] - def load(), do: @themes + def load(), do: {@themes, @schema} end ``` @@ -42,67 +49,41 @@ defmodule Scenic.Themes do Now themes are passed around scenic in the form of `{:library_name, :theme_name}` as opposed to just :theme_name. """ - - @theme_light %{ - text: :black, - background: :white, - border: :dark_grey, - active: {215, 215, 215}, - thumb: :cornflower_blue, - focus: :blue, - highlight: :saddle_brown - } - - @theme_dark %{ - text: :white, - background: :black, - border: :light_grey, - active: {40, 40, 40}, - thumb: :cornflower_blue, - focus: :cornflower_blue, - highlight: :sandy_brown - } - - @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) - @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) - @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) - @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) - @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) - @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) - @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) - @themes %{ - light: @theme_light, - dark: @theme_dark, - primary: @primary, - secondary: @secondary, - success: @success, - danger: @danger, - warning: @warning, - info: @info, - text: @text - } - - @callback load() :: list + @callback load() :: {map, list} | map + @optional_callbacks load: 0 defmacro __using__(using_opts \\ []) do quote do alias Scenic.Primitive.Style.Paint.Color @behaviour Scenic.Themes @sources Keyword.get(unquote(using_opts), :sources, []) + @default_schema [:text, :background, :border, :active, :thumb, :focus] + @library_themes Enum.reduce(@sources, %{}, fn {lib, module}, acc when is_atom(module) -> - themes = module.load() - Map.put_new(acc, lib, themes) + case module.load() do + {themes, schema} -> + Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])}) + themes -> + Map.put_new(acc, lib, {themes, @default_schema}) + end + {lib, {themes, schema}}, acc -> + Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])}) {lib, themes}, acc -> - Map.put_new(acc, lib, themes) + Map.put_new(acc, lib, {themes, @default_schema}) _, acc -> acc end) def validate(theme) - def validate({lib, theme} = lib_theme) when is_atom(theme) do + def validate({lib, theme_name} = lib_theme) when is_atom(theme_name) do + {_, schema} = Map.get(@library_themes, lib) case normalize(lib_theme) do - map when is_map(map) -> - {:ok, lib_theme} + theme -> + # validate against the schema + case validate(theme, schema) do + {:ok, _} -> {:ok, lib_theme} + error -> error + end nil -> { :error, @@ -118,6 +99,23 @@ defmodule Scenic.Themes do end end + def validate( + theme, + schema + ) do + # we have the schema so we can validate against it. + schema + |> Enum.reduce({:ok, theme}, fn + _, {:error, msg} -> + {:error, msg} + key, {:ok, _} = acc -> + case Map.has_key?(theme, key) do + true -> acc + false -> err_key(key, theme) + end + end) + end + def validate( %{ text: _, @@ -128,6 +126,8 @@ defmodule Scenic.Themes do focus: _ } = theme ) do + # we dont have the schema so validate against the default, + # this is not ideal, but should be fine for now. # we know all the required colors are there. # now make sure they are all valid colors, including any custom added ones. theme @@ -153,6 +153,7 @@ defmodule Scenic.Themes do You passed in a map, but it didn't include all the required color specifications. It must contain a valid color for each of the following entries. :text, :background, :border, :active, :thumb, :focus + If you're using a custom theme please check the documentation for that specific theme. #{IO.ANSI.default_color()} """ } @@ -176,6 +177,37 @@ defmodule Scenic.Themes do } end + @doc false + def normalize({lib, theme_name}) when is_atom(theme_name) do + case Map.get(@library_themes, lib) do + {themes, schema} -> Map.get(themes, theme_name) + nil -> nil + end + end + + def normalize(theme) when is_map(theme), do: theme + + @doc false + def preset({lib, theme_name}) do + case Map.get(@library_themes, lib) do + {themes, schema} -> Map.get(themes, theme_name) + nil -> nil + end + end + + defp err_key(key, map) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(map)} + #{IO.ANSI.yellow()} + Map entry: #{inspect(key)} + #{IO.ANSI.default_color()} + """ + } + end + defp err_color(key, msg) do { :error, @@ -186,17 +218,10 @@ defmodule Scenic.Themes do """ } end - - @doc false - def normalize({lib, theme}) when is_atom(theme), do: Map.get(Map.get(@library_themes, lib), theme) - def normalize(theme) when is_map(theme), do: theme - - @doc false - def preset({lib, theme}), do: Map.get(Map.get(@library_themes, lib), theme) end end - @moduledoc false + @doc false def module() do with {:ok, config} <- Application.fetch_env(:scenic, :themes), {:ok, module} <- Keyword.fetch(config, :module) do @@ -226,12 +251,54 @@ defmodule Scenic.Themes do end end + @doc false def validate(theme), do: module().validate(theme) + @doc false def normalize(theme), do: module().normalize(theme) + @doc false def preset(theme), do: module().preset(theme) + @theme_light %{ + text: :black, + background: :white, + border: :dark_grey, + active: {215, 215, 215}, + thumb: :cornflower_blue, + focus: :blue, + highlight: :saddle_brown + } + + @theme_dark %{ + text: :white, + background: :black, + border: :light_grey, + active: {40, 40, 40}, + thumb: :cornflower_blue, + focus: :cornflower_blue, + highlight: :sandy_brown + } + + @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) + @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) + @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) + @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) + @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) + @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) + @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) + @themes %{ + light: @theme_light, + dark: @theme_dark, + primary: @primary, + secondary: @secondary, + success: @success, + danger: @danger, + warning: @warning, + info: @info, + text: @text + } + @doc false def load(), do: @themes end diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs index cc515f6f..01c96dcf 100644 --- a/test/scenic/themes_test.exs +++ b/test/scenic/themes_test.exs @@ -63,6 +63,24 @@ defmodule Scenic.ThemesTest do assert Themes.normalize({:scenic, :dark}) == @theme_dark end + test "custom validate method accepts names themes" do + assert Themes.validate({:custom_scenic, :custom_dark}) == {:ok, {:custom_scenic, :custom_dark}} + assert Themes.validate({:custom_scenic, :custom_light}) == {:ok, {:custom_scenic, :custom_light}} + assert Themes.validate({:custom_scenic, :custom_primary}) == {:ok, {:custom_scenic, :custom_primary}} + assert Themes.validate({:custom_scenic, :custom_secondary}) == {:ok, {:custom_scenic, :custom_secondary}} + assert Themes.validate({:custom_scenic, :custom_success}) == {:ok, {:custom_scenic, :custom_success}} + assert Themes.validate({:custom_scenic, :custom_danger}) == {:ok, {:custom_scenic, :custom_danger}} + assert Themes.validate({:custom_scenic, :custom_warning}) == {:ok, {:custom_scenic, :custom_warning}} + assert Themes.validate({:custom_scenic, :custom_info}) == {:ok, {:custom_scenic, :custom_info}} + assert Themes.validate({:custom_scenic, :custom_text}) == {:ok, {:custom_scenic, :custom_text}} + end + + test "custom validate method rejects map without custom standard color" do + {:error, msg} = Themes.validate({:custom_scenic, :custom_invalid}) + assert msg =~ "Invalid theme specification" + assert msg =~ "Map entry: :surface" + end + test "validate accepts the named themes" do assert Themes.validate({:scenic, :dark}) == {:ok, {:scenic, :dark}} assert Themes.validate({:scenic, :light}) == {:ok, {:scenic, :light}} diff --git a/test/support/custom_themes.ex b/test/support/custom_themes.ex new file mode 100644 index 00000000..50af2cfe --- /dev/null +++ b/test/support/custom_themes.ex @@ -0,0 +1,62 @@ +defmodule Scenic.Test.CustomThemes do + @theme_light %{ + text: :black, + background: :white, + surface: :gainsboro, + border: :dark_grey, + active: {215, 215, 215}, + thumb: :cornflower_blue, + focus: :blue, + highlight: :saddle_brown + } + + @theme_dark %{ + text: :white, + background: :black, + surface: :gainsboro, + border: :light_grey, + active: {40, 40, 40}, + thumb: :cornflower_blue, + focus: :cornflower_blue, + highlight: :sandy_brown + } + + @theme_dark_invalid %{ + text: :white, + background: :black, + border: :light_grey, + active: {40, 40, 40}, + thumb: :cornflower_blue, + focus: :cornflower_blue + } + + @primary Map.merge(@theme_dark, %{surface: :gainsboro, background: {72, 122, 252}, active: {58, 94, 201}}) + @secondary Map.merge(@theme_dark, %{surface: :gainsboro, background: {111, 117, 125}, active: {86, 90, 95}}) + @success Map.merge(@theme_dark, %{surface: :gainsboro, background: {99, 163, 74}, active: {74, 123, 56}}) + @danger Map.merge(@theme_dark, %{surface: :gainsboro, background: {191, 72, 71}, active: {164, 54, 51}}) + @warning Map.merge(@theme_light, %{surface: :gainsboro, background: {239, 196, 42}, active: {197, 160, 31}}) + @info Map.merge(@theme_dark, %{surface: :gainsboro, background: {94, 159, 183}, active: {70, 119, 138}}) + @text Map.merge(@theme_dark, %{text: {72, 122, 252}, surface: :gainsboro, background: :clear, active: :clear}) + + @themes %{ + custom_light: @theme_light, + custom_dark: @theme_dark, + custom_primary: @primary, + custom_secondary: @secondary, + custom_success: @success, + custom_danger: @danger, + custom_warning: @warning, + custom_info: @info, + custom_text: @text, + custom_invalid: @theme_dark_invalid + } + + @schema [:surface] + + use Scenic.Themes, + sources: [ + {:custom_scenic, {@themes, @schema}} + ] + + def load(), do: {@themes, @schema} +end diff --git a/test/support/themes.ex b/test/support/themes.ex index 5cd27492..c8f87ddc 100644 --- a/test/support/themes.ex +++ b/test/support/themes.ex @@ -1,6 +1,46 @@ defmodule Scenic.Test.Themes do + # @theme_light %{ + # text: :black, + # background: :white, + # border: :dark_grey, + # active: {215, 215, 215}, + # thumb: :cornflower_blue, + # focus: :blue, + # highlight: :saddle_brown + # } + + # @theme_dark %{ + # text: :white, + # background: :black, + # border: :light_grey, + # active: {40, 40, 40}, + # thumb: :cornflower_blue, + # focus: :cornflower_blue, + # highlight: :sandy_brown + # } + + # @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) + # @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) + # @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) + # @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) + # @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) + # @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) + # @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) + # @themes %{ + # light: @theme_light, + # dark: @theme_dark, + # primary: @primary, + # secondary: @secondary, + # success: @success, + # danger: @danger, + # warning: @warning, + # info: @info, + # text: @text + # } + use Scenic.Themes, sources: [ - {:scenic, Scenic.Themes} + {:scenic, Scenic.Themes}, + {:custom_scenic, Scenic.Test.CustomThemes} ] end From 912688910c2b63273dd75d96d2d8ffe60e46b6f6 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Tue, 16 Nov 2021 15:59:35 -0800 Subject: [PATCH 05/12] implement a extendable color palette and improve config --- lib/scenic/color.ex | 6 +- lib/scenic/component/button.ex | 7 +- lib/scenic/palette.ex | 155 +++++++++++++++++++++++++++++++++ lib/scenic/themes.ex | 128 ++++++++++++++++++++------- test/scenic/themes_test.exs | 26 +++++- test/support/custom_themes.ex | 11 +-- test/support/themes.ex | 6 +- 7 files changed, 291 insertions(+), 48 deletions(-) create mode 100644 lib/scenic/palette.ex diff --git a/lib/scenic/color.ex b/lib/scenic/color.ex index 4388adfa..3e54af16 100644 --- a/lib/scenic/color.ex +++ b/lib/scenic/color.ex @@ -4,6 +4,8 @@ # defmodule Scenic.Color do + alias Scenic.Themes + @named_colors %{ alice_blue: {0xF0, 0xF8, 0xFF}, antique_white: {0xFA, 0xEB, 0xD7}, @@ -173,7 +175,7 @@ defmodule Scenic.Color do Most of the time, you will use one of the pre-defined named colors from the Named Colors table. However, there are times when you want to work with - other color formats ranging from simple grayscale to rgb to hsl. + other color formats ranging from simple grayscale to rgb to hsl. The following formats are all supported by the `Scenic.Color` module. The values of r, g, b, and a are integers between 0 and 255. @@ -616,7 +618,7 @@ defmodule Scenic.Color do @doc """ Return map of all named colors and their values """ - def named(), do: @named_colors + def named(), do: Themes.get_palette() # -------------------------------------------------------- # @doc """ diff --git a/lib/scenic/component/button.ex b/lib/scenic/component/button.ex index 51983d1c..847a3310 100644 --- a/lib/scenic/component/button.ex +++ b/lib/scenic/component/button.ex @@ -167,9 +167,12 @@ defmodule Scenic.Component.Button do nil -> Themes.preset({:scenic, :primary}) {:scenic, :dark} -> Themes.preset({:scenic, :primary}) {:scenic, :light} -> Themes.preset({:scenic, :primary}) - theme -> theme + theme -> + case Themes.normalize(theme) do + nil -> Themes.preset({:scenic, :primary}) + theme -> theme + end end - |> Themes.normalize() # font related info font = Keyword.get(styles, :font, @default_font) diff --git a/lib/scenic/palette.ex b/lib/scenic/palette.ex new file mode 100644 index 00000000..8177600f --- /dev/null +++ b/lib/scenic/palette.ex @@ -0,0 +1,155 @@ +defmodule Scenic.Palette do + @palette %{ + alice_blue: {0xF0, 0xF8, 0xFF}, + antique_white: {0xFA, 0xEB, 0xD7}, + aqua: {0x00, 0xFF, 0xFF}, + aquamarine: {0x7F, 0xFF, 0xD4}, + azure: {0xF0, 0xFF, 0xFF}, + beige: {0xF5, 0xF5, 0xDC}, + bisque: {0xFF, 0xE4, 0xC4}, + black: {0x00, 0x00, 0x00}, + blanched_almond: {0xFF, 0xEB, 0xCD}, + blue: {0x00, 0x00, 0xFF}, + blue_violet: {0x8A, 0x2B, 0xE2}, + brown: {0xA5, 0x2A, 0x2A}, + burly_wood: {0xDE, 0xB8, 0x87}, + cadet_blue: {0x5F, 0x9E, 0xA0}, + chartreuse: {0x7F, 0xFF, 0x00}, + chocolate: {0xD2, 0x69, 0x1E}, + coral: {0xFF, 0x7F, 0x50}, + cornflower_blue: {0x64, 0x95, 0xED}, + cornsilk: {0xFF, 0xF8, 0xDC}, + crimson: {0xDC, 0x14, 0x3C}, + cyan: {0x00, 0xFF, 0xFF}, + dark_blue: {0x00, 0x00, 0x8B}, + dark_cyan: {0x00, 0x8B, 0x8B}, + dark_golden_rod: {0xB8, 0x86, 0x0B}, + dark_gray: {0xA9, 0xA9, 0xA9}, + dark_grey: {0xA9, 0xA9, 0xA9}, + dark_green: {0x00, 0x64, 0x00}, + dark_khaki: {0xBD, 0xB7, 0x6B}, + dark_magenta: {0x8B, 0x00, 0x8B}, + dark_olive_green: {0x55, 0x6B, 0x2F}, + dark_orange: {0xFF, 0x8C, 0x00}, + dark_orchid: {0x99, 0x32, 0xCC}, + dark_red: {0x8B, 0x00, 0x00}, + dark_salmon: {0xE9, 0x96, 0x7A}, + dark_sea_green: {0x8F, 0xBC, 0x8F}, + dark_slate_blue: {0x48, 0x3D, 0x8B}, + dark_slate_gray: {0x2F, 0x4F, 0x4F}, + dark_slate_grey: {0x2F, 0x4F, 0x4F}, + dark_turquoise: {0x00, 0xCE, 0xD1}, + dark_violet: {0x94, 0x00, 0xD3}, + deep_pink: {0xFF, 0x14, 0x93}, + deep_sky_blue: {0x00, 0xBF, 0xFF}, + dim_gray: {0x69, 0x69, 0x69}, + dim_grey: {0x69, 0x69, 0x69}, + dodger_blue: {0x1E, 0x90, 0xFF}, + fire_brick: {0xB2, 0x22, 0x22}, + floral_white: {0xFF, 0xFA, 0xF0}, + forest_green: {0x22, 0x8B, 0x22}, + fuchsia: {0xFF, 0x00, 0xFF}, + gainsboro: {0xDC, 0xDC, 0xDC}, + ghost_white: {0xF8, 0xF8, 0xFF}, + gold: {0xFF, 0xD7, 0x00}, + golden_rod: {0xDA, 0xA5, 0x20}, + gray: {0x80, 0x80, 0x80}, + grey: {0x80, 0x80, 0x80}, + green: {0x00, 0x80, 0x00}, + green_yellow: {0xAD, 0xFF, 0x2F}, + honey_dew: {0xF0, 0xFF, 0xF0}, + hot_pink: {0xFF, 0x69, 0xB4}, + indian_red: {0xCD, 0x5C, 0x5C}, + indigo: {0x4B, 0x00, 0x82}, + ivory: {0xFF, 0xFF, 0xF0}, + khaki: {0xF0, 0xE6, 0x8C}, + lavender: {0xE6, 0xE6, 0xFA}, + lavender_blush: {0xFF, 0xF0, 0xF5}, + lawn_green: {0x7C, 0xFC, 0x00}, + lemon_chiffon: {0xFF, 0xFA, 0xCD}, + light_blue: {0xAD, 0xD8, 0xE6}, + light_coral: {0xF0, 0x80, 0x80}, + light_cyan: {0xE0, 0xFF, 0xFF}, + light_golden_rod: {0xFA, 0xFA, 0xD2}, + light_golden_rod_yellow: {0xFA, 0xFA, 0xD2}, + light_gray: {0xD3, 0xD3, 0xD3}, + light_grey: {0xD3, 0xD3, 0xD3}, + light_green: {0x90, 0xEE, 0x90}, + light_pink: {0xFF, 0xB6, 0xC1}, + light_salmon: {0xFF, 0xA0, 0x7A}, + light_sea_green: {0x20, 0xB2, 0xAA}, + light_sky_blue: {0x87, 0xCE, 0xFA}, + light_slate_gray: {0x77, 0x88, 0x99}, + light_slate_grey: {0x77, 0x88, 0x99}, + light_steel_blue: {0xB0, 0xC4, 0xDE}, + light_yellow: {0xFF, 0xFF, 0xE0}, + lime: {0x00, 0xFF, 0x00}, + lime_green: {0x32, 0xCD, 0x32}, + linen: {0xFA, 0xF0, 0xE6}, + magenta: {0xFF, 0x00, 0xFF}, + maroon: {0x80, 0x00, 0x00}, + medium_aqua_marine: {0x66, 0xCD, 0xAA}, + medium_blue: {0x00, 0x00, 0xCD}, + medium_orchid: {0xBA, 0x55, 0xD3}, + medium_purple: {0x93, 0x70, 0xDB}, + medium_sea_green: {0x3C, 0xB3, 0x71}, + medium_slate_blue: {0x7B, 0x68, 0xEE}, + medium_spring_green: {0x00, 0xFA, 0x9A}, + medium_turquoise: {0x48, 0xD1, 0xCC}, + medium_violet_red: {0xC7, 0x15, 0x85}, + midnight_blue: {0x19, 0x19, 0x70}, + mint_cream: {0xF5, 0xFF, 0xFA}, + misty_rose: {0xFF, 0xE4, 0xE1}, + moccasin: {0xFF, 0xE4, 0xB5}, + navajo_white: {0xFF, 0xDE, 0xAD}, + navy: {0x00, 0x00, 0x80}, + old_lace: {0xFD, 0xF5, 0xE6}, + olive: {0x80, 0x80, 0x00}, + olive_drab: {0x6B, 0x8E, 0x23}, + orange: {0xFF, 0xA5, 0x00}, + orange_red: {0xFF, 0x45, 0x00}, + orchid: {0xDA, 0x70, 0xD6}, + pale_golden_rod: {0xEE, 0xE8, 0xAA}, + pale_green: {0x98, 0xFB, 0x98}, + pale_turquoise: {0xAF, 0xEE, 0xEE}, + pale_violet_red: {0xDB, 0x70, 0x93}, + papaya_whip: {0xFF, 0xEF, 0xD5}, + peach_puff: {0xFF, 0xDA, 0xB9}, + peru: {0xCD, 0x85, 0x3F}, + pink: {0xFF, 0xC0, 0xCB}, + plum: {0xDD, 0xA0, 0xDD}, + powder_blue: {0xB0, 0xE0, 0xE6}, + purple: {0x80, 0x00, 0x80}, + rebecca_purple: {0x66, 0x33, 0x99}, + red: {0xFF, 0x00, 0x00}, + rosy_brown: {0xBC, 0x8F, 0x8F}, + royal_blue: {0x41, 0x69, 0xE1}, + saddle_brown: {0x8B, 0x45, 0x13}, + salmon: {0xFA, 0x80, 0x72}, + sandy_brown: {0xF4, 0xA4, 0x60}, + sea_green: {0x2E, 0x8B, 0x57}, + sea_shell: {0xFF, 0xF5, 0xEE}, + sienna: {0xA0, 0x52, 0x2D}, + silver: {0xC0, 0xC0, 0xC0}, + sky_blue: {0x87, 0xCE, 0xEB}, + slate_blue: {0x6A, 0x5A, 0xCD}, + slate_gray: {0x70, 0x80, 0x90}, + slate_grey: {0x70, 0x80, 0x90}, + snow: {0xFF, 0xFA, 0xFA}, + spring_green: {0x00, 0xFF, 0x7F}, + steel_blue: {0x46, 0x82, 0xB4}, + tan: {0xD2, 0xB4, 0x8C}, + teal: {0x00, 0x80, 0x80}, + thistle: {0xD8, 0xBF, 0xD8}, + tomato: {0xFF, 0x63, 0x47}, + turquoise: {0x40, 0xE0, 0xD0}, + violet: {0xEE, 0x82, 0xEE}, + wheat: {0xF5, 0xDE, 0xB3}, + white: {0xFF, 0xFF, 0xFF}, + white_smoke: {0xF5, 0xF5, 0xF5}, + yellow: {0xFF, 0xFF, 0x00}, + yellow_green: {0x9A, 0xCD, 0x32} + } + + def get(), do: @palette +end diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index b99d4b87..21e2e6cf 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -1,4 +1,5 @@ defmodule Scenic.Themes do + alias Scenic.Palette @moduledoc """ Manages theme libraries by registering your map of themes to a library key. By registering themes in this way you can safely pull in themes from external libraries, @@ -31,7 +32,7 @@ defmodule Scenic.Themes do schema [:surface] # add additional required keys to your theme use Scenic.Themes, - sources: [ + [ {:scenic, Scenic.Themes"}, {:my_app, load()} ] @@ -49,40 +50,73 @@ defmodule Scenic.Themes do Now themes are passed around scenic in the form of `{:library_name, :theme_name}` as opposed to just :theme_name. """ - @callback load() :: {map, list} | map + @callback load() :: keyword @optional_callbacks load: 0 - defmacro __using__(using_opts \\ []) do + defmacro __using__(sources \\ []) do quote do alias Scenic.Primitive.Style.Paint.Color @behaviour Scenic.Themes - @sources Keyword.get(unquote(using_opts), :sources, []) + @opts_schema [ + name: [required: true, type: :atom], + themes: [required: true, type: :any], + schema: [required: false, type: :any], + palette: [required: false, type: :any] + ] @default_schema [:text, :background, :border, :active, :thumb, :focus] - - @library_themes Enum.reduce(@sources, %{}, fn - {lib, module}, acc when is_atom(module) -> - case module.load() do - {themes, schema} -> - Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])}) - themes -> - Map.put_new(acc, lib, {themes, @default_schema}) - end - {lib, {themes, schema}}, acc -> - Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])}) - {lib, themes}, acc -> - Map.put_new(acc, lib, {themes, @default_schema}) - _, acc -> acc + @palette %{} + + @library_themes Enum.reduce(unquote(sources), %{}, fn lib_opts, acc -> + case NimbleOptions.validate(lib_opts, @opts_schema) do + {:ok, lib_opts} -> + name = lib_opts[:name] + themes = lib_opts[:themes] + schema = lib_opts[:schema] || [] + palette = lib_opts[:palette] || %{} + @palette Map.merge(@palette, palette) + case themes do + themes when is_map(themes) -> + # not a module so we can load in the settings directly + Map.put_new(acc, name, {themes, List.flatten([@default_schema | schema])}) + themes -> + # this is a module so we have to load the settings + lib_opts = themes.load() + themes = lib_opts[:themes] + schema = lib_opts[:schema] || [] + palette = lib_opts[:palette] || %{} + IO.inspect palette + @palette Map.merge(@palette, palette) + Map.put_new(acc, name, {themes, List.flatten([@default_schema | schema])}) + end + {:error, error} -> + raise Exception.message(error) + end end) + # validate the passed options def validate(theme) def validate({lib, theme_name} = lib_theme) when is_atom(theme_name) do - {_, schema} = Map.get(@library_themes, lib) - case normalize(lib_theme) do - theme -> + case Map.get(@library_themes, lib) do + {themes, schema} -> # validate against the schema - case validate(theme, schema) do - {:ok, _} -> {:ok, lib_theme} - error -> error + case Map.get(themes, theme_name) do + nil -> + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(theme_name)} + #{IO.ANSI.yellow()} + The theme could not be found in library #{inspect(lib)}. + Ensure you got the name correct. + #{IO.ANSI.default_color()} + """ + } + theme -> + case validate(theme, schema) do + {:ok, _} -> {:ok, lib_theme} + error -> error + end end nil -> { @@ -177,10 +211,23 @@ defmodule Scenic.Themes do } end + @doc false + def get_schema(lib) do + case Map.get(@library_themes, lib) do + {_, schema} -> schema + nil -> nil + end + end + + @doc false + def get_palette() do + @palette + end + @doc false def normalize({lib, theme_name}) when is_atom(theme_name) do case Map.get(@library_themes, lib) do - {themes, schema} -> Map.get(themes, theme_name) + {themes, _schema} -> Map.get(themes, theme_name) nil -> nil end end @@ -236,10 +283,8 @@ defmodule Scenic.Themes do Example Themes module that includes an optional alias: defmodule MyApplication.Themes do - use Scenic.Assets.Static, - otp_app: :my_application, - alias: [ - scenic: Scenic.Themes, + use Scenic.Themes, [ + [name: scenic: themes: Scenic.Themes], ] end @@ -251,15 +296,34 @@ defmodule Scenic.Themes do end end - @doc false + @spec validate({atom, atom} | {atom, map} | map) :: {:ok, term} | {:error, term} + @doc """ + Validate a theme + """ def validate(theme), do: module().validate(theme) + @spec get_schema(atom) :: list + @doc """ + Retrieve a library's schema + """ + def get_schema(lib), do: module().get_schema(lib) + + @spec normalize({atom, atom} | map) :: map | nil @doc false def normalize(theme), do: module().normalize(theme) - @doc false + @spec preset({atom, atom} | map) :: map | nil + @doc """ + Get a theme. + """ def preset(theme), do: module().preset(theme) + @spec get_palette() :: map + @doc """ + Get the palette of colors. + """ + def get_palette(), do: module().get_palette() + @theme_light %{ text: :black, background: :white, @@ -300,5 +364,5 @@ defmodule Scenic.Themes do } @doc false - def load(), do: @themes + def load(), do: [name: :scenic, themes: @themes, palette: Palette.get()] end diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs index 01c96dcf..dd9d00f5 100644 --- a/test/scenic/themes_test.exs +++ b/test/scenic/themes_test.exs @@ -3,6 +3,7 @@ defmodule Scenic.ThemesTest do doctest Scenic.Themes alias Scenic.Themes + alias Scenic.Color # we expect errors to be logged in this set of tests. This happens when we purposefully # attempted to load an asset that has been tampered with. So turn off the logging to @@ -49,21 +50,27 @@ defmodule Scenic.ThemesTest do text: @text } + @properly_configured_module [ + name: :scenic, + themes: @themes, + palette: Scenic.Palette.get() + ] + # import IEx - test "module returns the configured library module" do + test "module returns the module" do assert Themes.module() == Scenic.Test.Themes end - test "load returns the themes" do - assert Themes.load() == @themes + test "load returns the properly configured themes" do + assert Themes.load() == @properly_configured_module end test "normalize returns the correct theme" do assert Themes.normalize({:scenic, :dark}) == @theme_dark end - test "custom validate method accepts names themes" do + test "custom validate method accepts custom named themes" do assert Themes.validate({:custom_scenic, :custom_dark}) == {:ok, {:custom_scenic, :custom_dark}} assert Themes.validate({:custom_scenic, :custom_light}) == {:ok, {:custom_scenic, :custom_light}} assert Themes.validate({:custom_scenic, :custom_primary}) == {:ok, {:custom_scenic, :custom_primary}} @@ -137,4 +144,15 @@ defmodule Scenic.ThemesTest do test "verify rejects invalid values" do {:error, _msg} = Themes.validate("totally wrong") end + + @default_schema [:text, :background, :border, :active, :thumb, :focus] + + test "get_schema returns the correct schema" do + assert Themes.get_schema(:scenic) == @default_schema + end + + test "custom color can be retrieved" do + correct_color = {0xFF, 0xF6, 0x00} + assert Color.to_rgb(:yellow_1) == {:color_rgb, {255, 246, 0}} + end end diff --git a/test/support/custom_themes.ex b/test/support/custom_themes.ex index 50af2cfe..f2c21473 100644 --- a/test/support/custom_themes.ex +++ b/test/support/custom_themes.ex @@ -53,10 +53,11 @@ defmodule Scenic.Test.CustomThemes do @schema [:surface] - use Scenic.Themes, - sources: [ - {:custom_scenic, {@themes, @schema}} - ] + @colors %{ + yellow_1: {0xFF, 0xF6, 0x00} + } + + use Scenic.Themes, [] - def load(), do: {@themes, @schema} + def load(), do: [themes: @themes, schema: @schema, palette: @colors] end diff --git a/test/support/themes.ex b/test/support/themes.ex index c8f87ddc..f004da97 100644 --- a/test/support/themes.ex +++ b/test/support/themes.ex @@ -39,8 +39,8 @@ defmodule Scenic.Test.Themes do # } use Scenic.Themes, - sources: [ - {:scenic, Scenic.Themes}, - {:custom_scenic, Scenic.Test.CustomThemes} + [ + [name: :scenic, themes: Scenic.Themes], + [name: :custom_scenic, themes: Scenic.Test.CustomThemes] ] end From 79ab71ceaff86df819c343f1de44bab885cd8d27 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Tue, 7 Dec 2021 19:58:41 -0800 Subject: [PATCH 06/12] cleanup, fallback to Scenic.Themes if no config set. --- lib/scenic/color.ex | 155 +----------- lib/scenic/primitive/style/theme.ex | 187 -------------- lib/scenic/themes.ex | 376 +++++++++++++--------------- test/scenic/themes_test.exs | 1 - 4 files changed, 180 insertions(+), 539 deletions(-) delete mode 100644 lib/scenic/primitive/style/theme.ex diff --git a/lib/scenic/color.ex b/lib/scenic/color.ex index 3e54af16..f2d5459b 100644 --- a/lib/scenic/color.ex +++ b/lib/scenic/color.ex @@ -6,158 +6,6 @@ defmodule Scenic.Color do alias Scenic.Themes - @named_colors %{ - alice_blue: {0xF0, 0xF8, 0xFF}, - antique_white: {0xFA, 0xEB, 0xD7}, - aqua: {0x00, 0xFF, 0xFF}, - aquamarine: {0x7F, 0xFF, 0xD4}, - azure: {0xF0, 0xFF, 0xFF}, - beige: {0xF5, 0xF5, 0xDC}, - bisque: {0xFF, 0xE4, 0xC4}, - black: {0x00, 0x00, 0x00}, - blanched_almond: {0xFF, 0xEB, 0xCD}, - blue: {0x00, 0x00, 0xFF}, - blue_violet: {0x8A, 0x2B, 0xE2}, - brown: {0xA5, 0x2A, 0x2A}, - burly_wood: {0xDE, 0xB8, 0x87}, - cadet_blue: {0x5F, 0x9E, 0xA0}, - chartreuse: {0x7F, 0xFF, 0x00}, - chocolate: {0xD2, 0x69, 0x1E}, - coral: {0xFF, 0x7F, 0x50}, - cornflower_blue: {0x64, 0x95, 0xED}, - cornsilk: {0xFF, 0xF8, 0xDC}, - crimson: {0xDC, 0x14, 0x3C}, - cyan: {0x00, 0xFF, 0xFF}, - dark_blue: {0x00, 0x00, 0x8B}, - dark_cyan: {0x00, 0x8B, 0x8B}, - dark_golden_rod: {0xB8, 0x86, 0x0B}, - dark_gray: {0xA9, 0xA9, 0xA9}, - dark_grey: {0xA9, 0xA9, 0xA9}, - dark_green: {0x00, 0x64, 0x00}, - dark_khaki: {0xBD, 0xB7, 0x6B}, - dark_magenta: {0x8B, 0x00, 0x8B}, - dark_olive_green: {0x55, 0x6B, 0x2F}, - dark_orange: {0xFF, 0x8C, 0x00}, - dark_orchid: {0x99, 0x32, 0xCC}, - dark_red: {0x8B, 0x00, 0x00}, - dark_salmon: {0xE9, 0x96, 0x7A}, - dark_sea_green: {0x8F, 0xBC, 0x8F}, - dark_slate_blue: {0x48, 0x3D, 0x8B}, - dark_slate_gray: {0x2F, 0x4F, 0x4F}, - dark_slate_grey: {0x2F, 0x4F, 0x4F}, - dark_turquoise: {0x00, 0xCE, 0xD1}, - dark_violet: {0x94, 0x00, 0xD3}, - deep_pink: {0xFF, 0x14, 0x93}, - deep_sky_blue: {0x00, 0xBF, 0xFF}, - dim_gray: {0x69, 0x69, 0x69}, - dim_grey: {0x69, 0x69, 0x69}, - dodger_blue: {0x1E, 0x90, 0xFF}, - fire_brick: {0xB2, 0x22, 0x22}, - floral_white: {0xFF, 0xFA, 0xF0}, - forest_green: {0x22, 0x8B, 0x22}, - fuchsia: {0xFF, 0x00, 0xFF}, - gainsboro: {0xDC, 0xDC, 0xDC}, - ghost_white: {0xF8, 0xF8, 0xFF}, - gold: {0xFF, 0xD7, 0x00}, - golden_rod: {0xDA, 0xA5, 0x20}, - gray: {0x80, 0x80, 0x80}, - grey: {0x80, 0x80, 0x80}, - green: {0x00, 0x80, 0x00}, - green_yellow: {0xAD, 0xFF, 0x2F}, - honey_dew: {0xF0, 0xFF, 0xF0}, - hot_pink: {0xFF, 0x69, 0xB4}, - indian_red: {0xCD, 0x5C, 0x5C}, - indigo: {0x4B, 0x00, 0x82}, - ivory: {0xFF, 0xFF, 0xF0}, - khaki: {0xF0, 0xE6, 0x8C}, - lavender: {0xE6, 0xE6, 0xFA}, - lavender_blush: {0xFF, 0xF0, 0xF5}, - lawn_green: {0x7C, 0xFC, 0x00}, - lemon_chiffon: {0xFF, 0xFA, 0xCD}, - light_blue: {0xAD, 0xD8, 0xE6}, - light_coral: {0xF0, 0x80, 0x80}, - light_cyan: {0xE0, 0xFF, 0xFF}, - light_golden_rod: {0xFA, 0xFA, 0xD2}, - light_golden_rod_yellow: {0xFA, 0xFA, 0xD2}, - light_gray: {0xD3, 0xD3, 0xD3}, - light_grey: {0xD3, 0xD3, 0xD3}, - light_green: {0x90, 0xEE, 0x90}, - light_pink: {0xFF, 0xB6, 0xC1}, - light_salmon: {0xFF, 0xA0, 0x7A}, - light_sea_green: {0x20, 0xB2, 0xAA}, - light_sky_blue: {0x87, 0xCE, 0xFA}, - light_slate_gray: {0x77, 0x88, 0x99}, - light_slate_grey: {0x77, 0x88, 0x99}, - light_steel_blue: {0xB0, 0xC4, 0xDE}, - light_yellow: {0xFF, 0xFF, 0xE0}, - lime: {0x00, 0xFF, 0x00}, - lime_green: {0x32, 0xCD, 0x32}, - linen: {0xFA, 0xF0, 0xE6}, - magenta: {0xFF, 0x00, 0xFF}, - maroon: {0x80, 0x00, 0x00}, - medium_aqua_marine: {0x66, 0xCD, 0xAA}, - medium_blue: {0x00, 0x00, 0xCD}, - medium_orchid: {0xBA, 0x55, 0xD3}, - medium_purple: {0x93, 0x70, 0xDB}, - medium_sea_green: {0x3C, 0xB3, 0x71}, - medium_slate_blue: {0x7B, 0x68, 0xEE}, - medium_spring_green: {0x00, 0xFA, 0x9A}, - medium_turquoise: {0x48, 0xD1, 0xCC}, - medium_violet_red: {0xC7, 0x15, 0x85}, - midnight_blue: {0x19, 0x19, 0x70}, - mint_cream: {0xF5, 0xFF, 0xFA}, - misty_rose: {0xFF, 0xE4, 0xE1}, - moccasin: {0xFF, 0xE4, 0xB5}, - navajo_white: {0xFF, 0xDE, 0xAD}, - navy: {0x00, 0x00, 0x80}, - old_lace: {0xFD, 0xF5, 0xE6}, - olive: {0x80, 0x80, 0x00}, - olive_drab: {0x6B, 0x8E, 0x23}, - orange: {0xFF, 0xA5, 0x00}, - orange_red: {0xFF, 0x45, 0x00}, - orchid: {0xDA, 0x70, 0xD6}, - pale_golden_rod: {0xEE, 0xE8, 0xAA}, - pale_green: {0x98, 0xFB, 0x98}, - pale_turquoise: {0xAF, 0xEE, 0xEE}, - pale_violet_red: {0xDB, 0x70, 0x93}, - papaya_whip: {0xFF, 0xEF, 0xD5}, - peach_puff: {0xFF, 0xDA, 0xB9}, - peru: {0xCD, 0x85, 0x3F}, - pink: {0xFF, 0xC0, 0xCB}, - plum: {0xDD, 0xA0, 0xDD}, - powder_blue: {0xB0, 0xE0, 0xE6}, - purple: {0x80, 0x00, 0x80}, - rebecca_purple: {0x66, 0x33, 0x99}, - red: {0xFF, 0x00, 0x00}, - rosy_brown: {0xBC, 0x8F, 0x8F}, - royal_blue: {0x41, 0x69, 0xE1}, - saddle_brown: {0x8B, 0x45, 0x13}, - salmon: {0xFA, 0x80, 0x72}, - sandy_brown: {0xF4, 0xA4, 0x60}, - sea_green: {0x2E, 0x8B, 0x57}, - sea_shell: {0xFF, 0xF5, 0xEE}, - sienna: {0xA0, 0x52, 0x2D}, - silver: {0xC0, 0xC0, 0xC0}, - sky_blue: {0x87, 0xCE, 0xEB}, - slate_blue: {0x6A, 0x5A, 0xCD}, - slate_gray: {0x70, 0x80, 0x90}, - slate_grey: {0x70, 0x80, 0x90}, - snow: {0xFF, 0xFA, 0xFA}, - spring_green: {0x00, 0xFF, 0x7F}, - steel_blue: {0x46, 0x82, 0xB4}, - tan: {0xD2, 0xB4, 0x8C}, - teal: {0x00, 0x80, 0x80}, - thistle: {0xD8, 0xBF, 0xD8}, - tomato: {0xFF, 0x63, 0x47}, - turquoise: {0x40, 0xE0, 0xD0}, - violet: {0xEE, 0x82, 0xEE}, - wheat: {0xF5, 0xDE, 0xB3}, - white: {0xFF, 0xFF, 0xFF}, - white_smoke: {0xF5, 0xF5, 0xF5}, - yellow: {0xFF, 0xFF, 0x00}, - yellow_green: {0x9A, 0xCD, 0x32} - } - @moduledoc """ APIs to create and work with colors. @@ -201,8 +49,6 @@ defmodule Scenic.Color do a list of all the color names. I'll eventually add a link to a page that shows them visually. - #{inspect(Enum.map(@named_colors, fn {k, _v} -> k end) |> Enum.sort(), limit: :infinity, pretty: true)} - ## Additional Named Colors @@ -615,6 +461,7 @@ defmodule Scenic.Color do end # -------------------------------------------------------- + @spec named :: map @doc """ Return map of all named colors and their values """ diff --git a/lib/scenic/primitive/style/theme.ex b/lib/scenic/primitive/style/theme.ex deleted file mode 100644 index ff636bc5..00000000 --- a/lib/scenic/primitive/style/theme.ex +++ /dev/null @@ -1,187 +0,0 @@ -# # -# # Created by Boyd Multerer on 2018-08-18. -# # Copyright © 2018 Kry10 Limited. All rights reserved. -# # - -# defmodule Scenic.Primitive.Style.Theme do -# @moduledoc """ -# Themes are a way to bundle up a set of colors that are intended to be used -# by components invoked by a scene. - -# There are a set of pre-defined themes. -# You can also pass in a map of color values. - -# Unlike other styles, The currently set theme is given to child components. -# Each component gets to pick, choose, or ignore any colors in a given style. - -# ### Predefined Themes -# * `:dark` - This is the default and most common. Use when the background is dark. -# * `:light` - Use when the background is light colored. - -# ### Specialty Themes - -# The remaining themes are designed to color the standard components and don't really -# make much sense when applied to the root of a graph. You could, but it would be... -# interesting. - -# The most obvious place to use them is with [`Button`](Scenic.Component.Button.html) -# components. - -# * `:primary` - Blue background. This is the primary button type indicator. -# * `:secondary` - Grey background. Not primary type indicator. -# * `:success` - Green background. -# * `:danger` - Red background. Use for irreversible or dangerous actions. -# * `:warning` - Orange background. -# * `:info` - Lightish blue background. -# * `:text` - Transparent background. -# """ - -# use Scenic.Primitive.Style -# alias Scenic.Primitive.Style.Paint.Color - -# @theme_light %{ -# text: :black, -# background: :white, -# border: :dark_grey, -# active: {215, 215, 215}, -# thumb: :cornflower_blue, -# focus: :blue, -# highlight: :saddle_brown -# } - -# @theme_dark %{ -# text: :white, -# background: :black, -# border: :light_grey, -# active: {40, 40, 40}, -# thumb: :cornflower_blue, -# focus: :cornflower_blue, -# highlight: :sandy_brown -# } - -# # specialty themes -# @primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}}) -# @secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}}) -# @success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}}) -# @danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}}) -# @warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}}) -# @info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}}) -# @text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear}) - -# @themes %{ -# light: @theme_light, -# dark: @theme_dark, -# primary: @primary, -# secondary: @secondary, -# success: @success, -# danger: @danger, -# warning: @warning, -# info: @info, -# text: @text -# } - -# # ============================================================================ -# # data verification and serialization -# @doc false -# def validate(theme) -# def validate(:light), do: {:ok, :light} -# def validate(:dark), do: {:ok, :dark} -# def validate(:primary), do: {:ok, :primary} -# def validate(:secondary), do: {:ok, :secondary} -# def validate(:success), do: {:ok, :success} -# def validate(:danger), do: {:ok, :danger} -# def validate(:warning), do: {:ok, :warning} -# def validate(:info), do: {:ok, :info} -# def validate(:text), do: {:ok, :text} - -# def validate( -# %{ -# text: _, -# background: _, -# border: _, -# active: _, -# thumb: _, -# focus: _ -# } = theme -# ) do -# # we know all the required colors are there. -# # now make sure they are all valid colors, including any custom added ones. -# theme -# |> Enum.reduce({:ok, theme}, fn -# _, {:error, msg} -> -# {:error, msg} - -# {key, color}, {:ok, _} = acc -> -# case Color.validate(color) do -# {:ok, _} -> acc -# {:error, msg} -> err_color(key, msg) -# end -# end) -# end - -# def validate(name) when is_atom(name) do -# { -# :error, -# """ -# #{IO.ANSI.red()}Invalid theme name -# Received: #{inspect(name)} -# #{IO.ANSI.yellow()} -# Named themes must be from the following list: -# :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text#{IO.ANSI.default_color()} -# """ -# } -# end - -# def validate(%{} = map) do -# { -# :error, -# """ -# #{IO.ANSI.red()}Invalid theme specification -# Received: #{inspect(map)} -# #{IO.ANSI.yellow()} -# You passed in a map, but it didn't include all the required color specifications. -# It must contain a valid color for each of the following entries. -# :text, :background, :border, :active, :thumb, :focus -# #{IO.ANSI.default_color()} -# """ -# } -# end - -# def validate(data) do -# { -# :error, -# """ -# #{IO.ANSI.red()}Invalid theme specification -# Received: #{inspect(data)} -# #{IO.ANSI.yellow()} -# Themes can be a name from this list: -# :light, :dark, :primary, :secondary, :success, :danger, :warning, :info, :text - -# Or it may also be a map defining colors for the values of -# :text, :background, :border, :active, :thumb, :focus - -# If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} -# """ -# } -# end - -# defp err_color(key, msg) do -# { -# :error, -# """ -# #{IO.ANSI.red()}Invalid color in map -# Map entry: #{inspect(key)} -# #{msg} -# """ -# } -# end - -# # -------------------------------------------------------- -# @doc false -# def normalize(theme) when is_atom(theme), do: Map.get(@themes, theme) -# def normalize(theme) when is_map(theme), do: theme - -# # -------------------------------------------------------- -# @doc false -# def preset(theme), do: Map.get(@themes, theme) -# end diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index 21e2e6cf..69e0d6a0 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -1,5 +1,6 @@ defmodule Scenic.Themes do alias Scenic.Palette + alias Scenic.Primitive.Style.Paint.Color @moduledoc """ Manages theme libraries by registering your map of themes to a library key. By registering themes in this way you can safely pull in themes from external libraries, @@ -33,11 +34,11 @@ defmodule Scenic.Themes do use Scenic.Themes, [ - {:scenic, Scenic.Themes"}, + {:scenic, Scenic.Themes}, {:my_app, load()} ] - def load(), do: {@themes, @schema} + def load(), do: [name: :my_library, themes: @themes, schema: @schema, palette: @palette] end ``` @@ -55,7 +56,6 @@ defmodule Scenic.Themes do defmacro __using__(sources \\ []) do quote do - alias Scenic.Primitive.Style.Paint.Color @behaviour Scenic.Themes @opts_schema [ name: [required: true, type: :atom], @@ -64,7 +64,7 @@ defmodule Scenic.Themes do palette: [required: false, type: :any] ] @default_schema [:text, :background, :border, :active, :thumb, :focus] - @palette %{} + @_palette %{} @library_themes Enum.reduce(unquote(sources), %{}, fn lib_opts, acc -> case NimbleOptions.validate(lib_opts, @opts_schema) do @@ -73,7 +73,7 @@ defmodule Scenic.Themes do themes = lib_opts[:themes] schema = lib_opts[:schema] || [] palette = lib_opts[:palette] || %{} - @palette Map.merge(@palette, palette) + @_palette Map.merge(@_palette, palette) case themes do themes when is_map(themes) -> # not a module so we can load in the settings directly @@ -84,8 +84,7 @@ defmodule Scenic.Themes do themes = lib_opts[:themes] schema = lib_opts[:schema] || [] palette = lib_opts[:palette] || %{} - IO.inspect palette - @palette Map.merge(@palette, palette) + @_palette Map.merge(@_palette, palette) Map.put_new(acc, name, {themes, List.flatten([@default_schema | schema])}) end {:error, error} -> @@ -94,235 +93,188 @@ defmodule Scenic.Themes do end) # validate the passed options - def validate(theme) - def validate({lib, theme_name} = lib_theme) when is_atom(theme_name) do - case Map.get(@library_themes, lib) do - {themes, schema} -> - # validate against the schema - case Map.get(themes, theme_name) do - nil -> - { - :error, - """ - #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(theme_name)} - #{IO.ANSI.yellow()} - The theme could not be found in library #{inspect(lib)}. - Ensure you got the name correct. - #{IO.ANSI.default_color()} - """ - } - theme -> - case validate(theme, schema) do - {:ok, _} -> {:ok, lib_theme} - error -> error - end - end + def library(), do: @library_themes + + def _get_palette(), do: @_palette + end + end + + @doc false + def module() do + with {:ok, config} <- Application.fetch_env(:scenic, :themes), + {:ok, module} <- Keyword.fetch(config, :module) do + module + else + _ -> + # No module configure return the default + __MODULE__ + end + end + + def validate(theme) + def validate({lib, theme_name} = lib_theme) when is_atom(theme_name) do + themes = module().library() + case Map.get(themes, lib) do + {themes, schema} -> + # validate against the schema + case Map.get(themes, theme_name) do nil -> { :error, """ #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(lib_theme)} + Received: #{inspect(theme_name)} #{IO.ANSI.yellow()} - You passed in a tuple representing a library theme, but it could not be found. - Please ensure you've imported the the library correctly in your Themes module. + The theme could not be found in library #{inspect(lib)}. + Ensure you got the name correct. #{IO.ANSI.default_color()} """ } - end - end - - def validate( - theme, - schema - ) do - # we have the schema so we can validate against it. - schema - |> Enum.reduce({:ok, theme}, fn - _, {:error, msg} -> - {:error, msg} - key, {:ok, _} = acc -> - case Map.has_key?(theme, key) do - true -> acc - false -> err_key(key, theme) + theme -> + case validate(theme, schema) do + {:ok, _} -> {:ok, lib_theme} + error -> error end - end) - end - - def validate( - %{ - text: _, - background: _, - border: _, - active: _, - thumb: _, - focus: _ - } = theme - ) do - # we dont have the schema so validate against the default, - # this is not ideal, but should be fine for now. - # we know all the required colors are there. - # now make sure they are all valid colors, including any custom added ones. - theme - |> Enum.reduce({:ok, theme}, fn - _, {:error, msg} -> - {:error, msg} - - {key, color}, {:ok, _} = acc -> - case Color.validate(color) do - {:ok, _} -> acc - {:error, msg} -> err_color(key, msg) - end - end) - end - - def validate(%{} = map) do - { - :error, - """ - #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(map)} - #{IO.ANSI.yellow()} - You passed in a map, but it didn't include all the required color specifications. - It must contain a valid color for each of the following entries. - :text, :background, :border, :active, :thumb, :focus - If you're using a custom theme please check the documentation for that specific theme. - #{IO.ANSI.default_color()} - """ - } - end - - def validate(data) do - { - :error, - """ - #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(data)} - #{IO.ANSI.yellow()} - Themes can be a tuple represent a theme for example: - {:scenic, :light}, {:scenic, :dark} - - Or it may also be a map defining colors for the values of - :text, :background, :border, :active, :thumb, :focus - - If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} - """ - } - end - - @doc false - def get_schema(lib) do - case Map.get(@library_themes, lib) do - {_, schema} -> schema - nil -> nil end - end - - @doc false - def get_palette() do - @palette - end - - @doc false - def normalize({lib, theme_name}) when is_atom(theme_name) do - case Map.get(@library_themes, lib) do - {themes, _schema} -> Map.get(themes, theme_name) - nil -> nil - end - end - - def normalize(theme) when is_map(theme), do: theme - - @doc false - def preset({lib, theme_name}) do - case Map.get(@library_themes, lib) do - {themes, schema} -> Map.get(themes, theme_name) - nil -> nil - end - end - - defp err_key(key, map) do + nil -> { :error, """ #{IO.ANSI.red()}Invalid theme specification - Received: #{inspect(map)} + Received: #{inspect(lib_theme)} #{IO.ANSI.yellow()} - Map entry: #{inspect(key)} + You passed in a tuple representing a library theme, but it could not be found. + Please ensure you've imported the the library correctly in your Themes module. #{IO.ANSI.default_color()} """ } - end - - defp err_color(key, msg) do - { - :error, - """ - #{IO.ANSI.red()}Invalid color in map - Map entry: #{inspect(key)} - #{msg} - """ - } - end end end - @doc false - def module() do - with {:ok, config} <- Application.fetch_env(:scenic, :themes), - {:ok, module} <- Keyword.fetch(config, :module) do - module - else - _ -> - raise """ - No Themes module is configured. - You need to create themes module in your application. - Then connect it to Scenic with some config. - - Example Themes module that includes an optional alias: + def validate( + %{ + text: _, + background: _, + border: _, + active: _, + thumb: _, + focus: _ + } = theme + ) do + # we dont have the schema so validate against the default, + # this is not ideal, but should be fine for now. + # we know all the required colors are there. + # now make sure they are all valid colors, including any custom added ones. + theme + |> Enum.reduce({:ok, theme}, fn + _, {:error, msg} -> + {:error, msg} + + {key, color}, {:ok, _} = acc -> + case Color.validate(color) do + {:ok, _} -> acc + {:error, msg} -> err_color(key, msg) + end + end) + end - defmodule MyApplication.Themes do - use Scenic.Themes, [ - [name: scenic: themes: Scenic.Themes], - ] - end + def validate(%{} = map) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(map)} + #{IO.ANSI.yellow()} + You passed in a map, but it didn't include all the required color specifications. + It must contain a valid color for each of the following entries. + :text, :background, :border, :active, :thumb, :focus + If you're using a custom theme please check the documentation for that specific theme. + #{IO.ANSI.default_color()} + """ + } + end - Example configuration script (this goes in your config.exs file): + def validate(data) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(data)} + #{IO.ANSI.yellow()} + Themes can be a tuple represent a theme for example: + {:scenic, :light}, {:scenic, :dark} + + Or it may also be a map defining colors for the values of + :text, :background, :border, :active, :thumb, :focus + + If you pass in a map, you may add your own colors in addition to the required ones.#{IO.ANSI.default_color()} + """ + } + end - config :scenic, :themes, - module: MyApplication.Themes - """ - end + def validate( + theme, + schema + ) do + # we have the schema so we can validate against it. + schema + |> Enum.reduce({:ok, theme}, fn + _, {:error, msg} -> + {:error, msg} + key, {:ok, _} = acc -> + case Map.has_key?(theme, key) do + true -> acc + false -> err_key(key, theme) + end + end) end - @spec validate({atom, atom} | {atom, map} | map) :: {:ok, term} | {:error, term} + @spec get_schema(atom) :: list @doc """ - Validate a theme + Retrieve a library's schema """ - def validate(theme), do: module().validate(theme) + def get_schema(lib) do + themes = module().library() + case Map.get(themes, lib) do + {_, schema} -> schema + nil -> nil + end + end - @spec get_schema(atom) :: list + @spec get_palette() :: map @doc """ - Retrieve a library's schema + Retrieve the color palette """ - def get_schema(lib), do: module().get_schema(lib) + def get_palette() do + module()._get_palette() + end - @spec normalize({atom, atom} | map) :: map | nil @doc false - def normalize(theme), do: module().normalize(theme) + def normalize({lib, theme_name}) when is_atom(theme_name) do + themes = module().library() + case Map.get(themes, lib) do + {themes, _schema} -> Map.get(themes, theme_name) + nil -> nil + end + end - @spec preset({atom, atom} | map) :: map | nil + @spec normalize({atom, atom} | map) :: map | nil @doc """ - Get a theme. + Converts a theme from it's tuple form to it's map form. """ - def preset(theme), do: module().preset(theme) + def normalize(theme) when is_map(theme), do: theme - @spec get_palette() :: map + @spec preset({atom, atom} | map) :: map | nil @doc """ - Get the palette of colors. + Get a theme. """ - def get_palette(), do: module().get_palette() + def preset({lib, theme_name}) do + themes = module().library() + case Map.get(themes, lib) do + {themes, _schema} -> Map.get(themes, theme_name) + nil -> nil + end + end @theme_light %{ text: :black, @@ -362,7 +314,37 @@ defmodule Scenic.Themes do info: @info, text: @text } + @default_schema [:text, :background, :border, :active, :thumb, :focus] + @palette Palette.get() + + def _get_palette(), do: @palette + + def library(), do: %{scenic: {@themes, @default_schema}} @doc false - def load(), do: [name: :scenic, themes: @themes, palette: Palette.get()] + def load(), do: [name: :scenic, themes: @themes, palette: @palette] + + defp err_key(key, map) do + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(map)} + #{IO.ANSI.yellow()} + Map entry: #{inspect(key)} + #{IO.ANSI.default_color()} + """ + } + end + + defp err_color(key, msg) do + { + :error, + """ + #{IO.ANSI.red()}Invalid color in map + Map entry: #{inspect(key)} + #{msg} + """ + } + end end diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs index dd9d00f5..8a96e444 100644 --- a/test/scenic/themes_test.exs +++ b/test/scenic/themes_test.exs @@ -152,7 +152,6 @@ defmodule Scenic.ThemesTest do end test "custom color can be retrieved" do - correct_color = {0xFF, 0xF6, 0x00} assert Color.to_rgb(:yellow_1) == {:color_rgb, {255, 246, 0}} end end From 103e0102d7afcf86994ccc5628ba085db4adbdb5 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Tue, 7 Dec 2021 20:04:28 -0800 Subject: [PATCH 07/12] remove old theme tests --- test/scenic/primitive/style/theme_test.exs | 68 ---------------------- 1 file changed, 68 deletions(-) delete mode 100644 test/scenic/primitive/style/theme_test.exs diff --git a/test/scenic/primitive/style/theme_test.exs b/test/scenic/primitive/style/theme_test.exs deleted file mode 100644 index 95479c92..00000000 --- a/test/scenic/primitive/style/theme_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# -# Created by Boyd Multerer on 2018-09-25. -# Copyright © 2018-2021 Kry10 Limited. All rights reserved. -# - -# defmodule Scenic.Primitive.Style.ThemeTest do -# use ExUnit.Case, async: true -# doctest Scenic.Primitive.Style.Theme - -# alias Scenic.Primitive.Style.Theme - -# test "validate accepts the named themes" do -# assert Theme.validate(:dark) == {:ok, :dark} -# assert Theme.validate(:light) == {:ok, :light} -# assert Theme.validate(:primary) == {:ok, :primary} -# assert Theme.validate(:secondary) == {:ok, :secondary} -# assert Theme.validate(:success) == {:ok, :success} -# assert Theme.validate(:danger) == {:ok, :danger} -# assert Theme.validate(:warning) == {:ok, :warning} -# assert Theme.validate(:info) == {:ok, :info} -# assert Theme.validate(:text) == {:ok, :text} -# end - -# test "validate rejects invalid theme names" do -# {:error, msg} = Theme.validate(:invalid) -# assert msg =~ "Named themes must be from the following list" -# end - -# test "validate accepts maps of colors" do -# color_map = %{ -# text: :red, -# background: :green, -# border: :blue, -# active: :magenta, -# thumb: :cyan, -# focus: :yellow, -# my_color: :black -# } - -# assert Theme.validate(color_map) == {:ok, color_map} -# end - -# test "validate rejects maps with invalid colors" do -# color_map = %{ -# text: :red, -# background: :green, -# border: :invalid, -# active: :magenta, -# thumb: :cyan, -# focus: :yellow, -# my_color: :black -# } - -# {:error, msg} = Theme.validate(color_map) -# assert msg =~ "Map entry: :border" -# assert msg =~ "Invalid Color specification: :invalid" -# end - -# test "verify rejects maps without the standard colors" do -# color_map = %{some_name: :red} -# {:error, msg} = Theme.validate(color_map) -# assert msg =~ "didn't include all the required color" -# end - -# test "verify rejects invalid values" do -# {:error, _msg} = Theme.validate("totally wrong") -# end -# end From ba40753b138faf134af0ce5935270bd35715c04f Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Tue, 7 Dec 2021 20:40:21 -0800 Subject: [PATCH 08/12] move normalize to correct place in module --- lib/scenic/themes.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index 69e0d6a0..86df4dcf 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -249,7 +249,10 @@ defmodule Scenic.Themes do module()._get_palette() end - @doc false + @spec normalize({atom, atom} | map) :: map | nil + @doc """ + Converts a theme from it's tuple form to it's map form. + """ def normalize({lib, theme_name}) when is_atom(theme_name) do themes = module().library() case Map.get(themes, lib) do @@ -258,12 +261,6 @@ defmodule Scenic.Themes do end end - @spec normalize({atom, atom} | map) :: map | nil - @doc """ - Converts a theme from it's tuple form to it's map form. - """ - def normalize(theme) when is_map(theme), do: theme - @spec preset({atom, atom} | map) :: map | nil @doc """ Get a theme. @@ -276,6 +273,8 @@ defmodule Scenic.Themes do end end + def preset(theme) when is_map(theme), do: theme + @theme_light %{ text: :black, background: :white, From 1d06de73ac6e3631ba10b349f62f0b1ea3a4a381 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Thu, 9 Dec 2021 17:05:07 -0800 Subject: [PATCH 09/12] normalize returns themes when it's a map. --- lib/scenic/themes.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index 86df4dcf..f858c368 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -261,6 +261,8 @@ defmodule Scenic.Themes do end end + def normalize(theme) when is_map(theme), do: theme + @spec preset({atom, atom} | map) :: map | nil @doc """ Get a theme. From ecd7bcd645f9557703bb9d6f1cdece646275fe16 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Mon, 13 Dec 2021 16:30:03 -0800 Subject: [PATCH 10/12] search in :scenic by default for single atom themes --- lib/scenic/themes.ex | 49 ++++++++++++++++++++++++++++++++++--- test/scenic/themes_test.exs | 10 +++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index f858c368..87c6e073 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -151,6 +151,30 @@ defmodule Scenic.Themes do end end + def validate(theme_name) when is_atom(theme_name) do + lib = module().library() + {themes, schema} = Map.get(lib, :scenic) + case Map.get(themes, theme_name) do + nil -> + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(theme_name)} + #{IO.ANSI.yellow()} + The theme could not be found in library #{inspect(:scenic)}. + Ensure you got the name correct. + #{IO.ANSI.default_color()} + """ + } + theme -> + case validate(theme, schema) do + {:ok, _} -> {:ok, theme_name} + error -> error + end + end + end + def validate( %{ text: _, @@ -201,9 +225,12 @@ defmodule Scenic.Themes do #{IO.ANSI.red()}Invalid theme specification Received: #{inspect(data)} #{IO.ANSI.yellow()} - Themes can be a tuple represent a theme for example: + Themes can be a tuple representing a theme for example: {:scenic, :light}, {:scenic, :dark} + Or an atom representing one of scenics default themes: + :primary, :secondary + Or it may also be a map defining colors for the values of :text, :background, :border, :active, :thumb, :focus @@ -249,7 +276,7 @@ defmodule Scenic.Themes do module()._get_palette() end - @spec normalize({atom, atom} | map) :: map | nil + @spec normalize({atom, atom} | map | atom) :: map | nil @doc """ Converts a theme from it's tuple form to it's map form. """ @@ -261,9 +288,17 @@ defmodule Scenic.Themes do end end + def normalize(theme_name) when is_atom(theme_name) do + themes = module().library() + case Map.get(themes, :scenic) do + {themes, _schema} -> Map.get(themes, theme_name) + nil -> nil + end + end + def normalize(theme) when is_map(theme), do: theme - @spec preset({atom, atom} | map) :: map | nil + @spec preset({atom, atom} | map | atom) :: map | nil @doc """ Get a theme. """ @@ -275,6 +310,14 @@ defmodule Scenic.Themes do end end + def preset(theme_name) when is_atom(theme_name) do + themes = module().library() + case Map.get(themes, :scenic) do + {themes, _schema} -> Map.get(themes, theme_name) + nil -> nil + end + end + def preset(theme) when is_map(theme), do: theme @theme_light %{ diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs index 8a96e444..6c131b52 100644 --- a/test/scenic/themes_test.exs +++ b/test/scenic/themes_test.exs @@ -70,6 +70,10 @@ defmodule Scenic.ThemesTest do assert Themes.normalize({:scenic, :dark}) == @theme_dark end + test "normalize returns default scenic theme when an atom is passed" do + assert Themes.normalize(:dark) == @theme_dark + end + test "custom validate method accepts custom named themes" do assert Themes.validate({:custom_scenic, :custom_dark}) == {:ok, {:custom_scenic, :custom_dark}} assert Themes.validate({:custom_scenic, :custom_light}) == {:ok, {:custom_scenic, :custom_light}} @@ -102,7 +106,11 @@ defmodule Scenic.ThemesTest do test "validate rejects invalid theme names" do {:error, msg} = Themes.validate(:invalid) - assert msg =~ "Themes can be a tuple represent a theme for example:" + assert msg =~ "The theme could not be found in library" + end + + test "validate defaults to the scenic library when an atom is passed" do + assert Themes.validate(:primary) == {:ok, :primary} end test "validate accepts maps of colors" do From 51c79d2371f5c2a2c752bf3a20b520494f476497 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Sun, 19 Dec 2021 11:59:28 -0800 Subject: [PATCH 11/12] add method to validate tuple theme against custom schema --- lib/scenic/themes.ex | 48 +++++++++++++++++++++++++++++++++++++ test/scenic/themes_test.exs | 10 ++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index 87c6e073..fc894df2 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -239,6 +239,54 @@ defmodule Scenic.Themes do } end + def validate( + {lib, theme_name} = lib_theme, + schema + ) do + # we have the schema so we can validate against it. + themes = module().library() + case Map.get(themes, lib) do + {themes, _schema} -> + case Map.get(themes, theme_name) do + nil -> + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(lib_theme)} + #{IO.ANSI.yellow()} + The theme could not be found in library #{inspect(:scenic)}. + Ensure you got the name correct. + #{IO.ANSI.default_color()} + """ + } + theme -> + schema + |> Enum.reduce({:ok, theme}, fn + _, {:error, msg} -> + {:error, msg} + key, {:ok, _} = acc -> + case Map.has_key?(theme, key) do + true -> acc + false -> err_key(key, theme) + end + end) + end + nil -> + { + :error, + """ + #{IO.ANSI.red()}Invalid theme specification + Received: #{inspect(lib_theme)} + #{IO.ANSI.yellow()} + You passed in a tuple representing a library theme, but it could not be found. + Please ensure you've imported the the library correctly in your Themes module. + #{IO.ANSI.default_color()} + """ + } + end + end + def validate( theme, schema diff --git a/test/scenic/themes_test.exs b/test/scenic/themes_test.exs index 6c131b52..49814bc6 100644 --- a/test/scenic/themes_test.exs +++ b/test/scenic/themes_test.exs @@ -50,6 +50,8 @@ defmodule Scenic.ThemesTest do text: @text } + @schema [:background, :text, :thumb, :focus, :highlight] + @properly_configured_module [ name: :scenic, themes: @themes, @@ -143,13 +145,17 @@ defmodule Scenic.ThemesTest do assert msg =~ "Invalid Color specification: :invalid" end - test "verify rejects maps without the standard colors" do + test "validate accepts a theme against a schema passed in" do + assert Themes.validate({:scenic, :primary}, @schema) + end + + test "validate rejects maps without the standard colors" do color_map = %{some_name: :red} {:error, msg} = Themes.validate(color_map) assert msg =~ "didn't include all the required color" end - test "verify rejects invalid values" do + test "validate rejects invalid values" do {:error, _msg} = Themes.validate("totally wrong") end From 4ccba595789ed221b4f03377857ecf3e3f1ef718 Mon Sep 17 00:00:00 2001 From: Vacarsu Date: Fri, 17 Mar 2023 03:10:53 -0700 Subject: [PATCH 12/12] Only attempt to run _get_palette if it's exported --- lib/scenic/themes.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/scenic/themes.ex b/lib/scenic/themes.ex index fc894df2..4580db42 100644 --- a/lib/scenic/themes.ex +++ b/lib/scenic/themes.ex @@ -321,7 +321,8 @@ defmodule Scenic.Themes do Retrieve the color palette """ def get_palette() do - module()._get_palette() + module = module() + if function_exported?(module, :_get_palette, 0), do: apply(module, :_get_palette, []), else: _get_palette() end @spec normalize({atom, atom} | map | atom) :: map | nil