use crate::types::{ValidFor, props};
use silex_core::traits::{IntoRx, RxGet, RxValue};
use silex_dom::attribute::{ApplyTarget, ApplyToDom, IntoStorable};
use std::borrow::Cow;
use std::fmt::{Display, Write};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use wasm_bindgen::JsCast;
pub(crate) type DynamicValue = Rc<dyn Fn() -> String>;
pub(crate) type StaticRule = (&'static str, Cow<'static, str>);
pub(crate) type DynamicRule = (&'static str, DynamicValue);
#[derive(Clone)]
enum StyleValue {
Static(Cow<'static, str>),
Dynamic(DynamicValue),
}
impl StyleValue {
fn from_rx<V>(value: V) -> Self
where
V: IntoRx + RxValue + 'static,
V::Value: Display + Clone + Sized + 'static,
V::RxType: RxGet<Value = V::Value> + 'static,
{
if value.is_constant() {
let signal = value.into_rx();
Self::Static(Cow::Owned(format!("{}", signal.get())))
} else {
let signal = value.into_rx();
Self::Dynamic(Rc::new(move || format!("{}", signal.get())))
}
}
}
#[derive(Clone)]
pub(crate) enum NestedRule {
Media(&'static str, Style),
Selector(&'static str, Style),
}
#[derive(Clone)]
pub struct Style {
pub(crate) static_rules: Vec<StaticRule>,
pub(crate) dynamic_rules: Vec<DynamicRule>,
pub(crate) nested_rules: Vec<NestedRule>,
}
impl Default for Style {
fn default() -> Self {
Self::new()
}
}
impl Style {
pub fn new() -> Self {
Self {
static_rules: Vec::new(),
dynamic_rules: Vec::new(),
nested_rules: Vec::new(),
}
}
pub fn media<F>(mut self, query: &'static str, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nested_rules
.push(NestedRule::Media(query, f(Style::new())));
self
}
pub fn nest<F>(mut self, selector: &'static str, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nested_rules
.push(NestedRule::Selector(selector, f(Style::new())));
self
}
pub fn on_hover<F>(self, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nest(":hover", f)
}
pub fn on_active<F>(self, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nest(":active", f)
}
pub fn on_focus<F>(self, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nest(":focus", f)
}
pub fn margin_x<V>(self, value: V) -> Self
where
V: IntoRx + RxValue + Clone + 'static,
V::Value: ValidFor<props::MarginLeft>
+ ValidFor<props::MarginRight>
+ Display
+ Clone
+ Sized
+ 'static,
V::RxType: RxGet<Value = V::Value> + Clone + 'static,
{
self.margin_left(value.clone()).margin_right(value)
}
pub fn margin_y<V>(self, value: V) -> Self
where
V: IntoRx + RxValue + Clone + 'static,
V::Value: ValidFor<props::MarginTop>
+ ValidFor<props::MarginBottom>
+ Display
+ Clone
+ Sized
+ 'static,
V::RxType: RxGet<Value = V::Value> + Clone + 'static,
{
self.margin_top(value.clone()).margin_bottom(value)
}
pub fn padding_x<V>(self, value: V) -> Self
where
V: IntoRx + RxValue + Clone + 'static,
V::Value: ValidFor<props::PaddingLeft>
+ ValidFor<props::PaddingRight>
+ Display
+ Clone
+ Sized
+ 'static,
V::RxType: RxGet<Value = V::Value> + Clone + 'static,
{
self.padding_left(value.clone()).padding_right(value)
}
pub fn padding_y<V>(self, value: V) -> Self
where
V: IntoRx + RxValue + Clone + 'static,
V::Value: ValidFor<props::PaddingTop>
+ ValidFor<props::PaddingBottom>
+ Display
+ Clone
+ Sized
+ 'static,
V::RxType: RxGet<Value = V::Value> + Clone + 'static,
{
self.padding_top(value.clone()).padding_bottom(value)
}
pub fn pseudo<F>(self, selector: &'static str, f: F) -> Self
where
F: FnOnce(Style) -> Style,
{
self.nest(selector, f)
}
fn add_rule(mut self, prop: &'static str, value: StyleValue) -> Self {
match value {
StyleValue::Static(val_str) => {
self.static_rules.push((prop, val_str));
}
StyleValue::Dynamic(getter) => {
self.dynamic_rules.push((prop, getter));
}
}
self
}
}
pub fn sty() -> Style {
Style::new()
}
macro_rules! generate_builder_methods {
($( ($snake:ident, $kebab:expr, $pascal:ident, $group:ident) ),*) => {
impl Style {
$(
pub fn $snake<V>(self, value: V) -> Self
where
V: IntoRx + RxValue + 'static,
V::Value: ValidFor<props::$pascal> + Display + Clone + Sized + 'static,
V::RxType: RxGet<Value = V::Value> + Clone + 'static,
{
self.add_rule($kebab, StyleValue::from_rx(value))
}
)*
}
};
}
crate::for_all_properties!(generate_builder_methods);
impl ApplyToDom for Style {
fn apply(&self, el: &web_sys::Element, _target: ApplyTarget) {
self.apply_to_element(el);
}
}
impl Style {
pub fn apply_to_element(&self, el: &web_sys::Element) -> String {
let mut hasher = silex_hash::css::CssHasher::new();
hash_recursive(self, &mut hasher);
let hash_val = hasher.finish();
let mut hash_buf = [0u8; 13];
let hash_str = silex_hash::css::encode_base36(hash_val, &mut hash_buf);
let class_base = format!("slx-{}", hash_str);
let mut css_str = String::new();
let mut dyn_bindings = Vec::new();
let base_sel = format!(".{}", class_base);
generate_css_recursive(self, &base_sel, hash_str, &mut css_str, &mut dyn_bindings);
crate::inject_style(&class_base, &css_str);
let _ = el.class_list().add_1(&class_base);
for (var_name, getter) in dyn_bindings {
let el_clone = el.clone();
silex_core::reactivity::Effect::new(move |prev: Option<String>| {
let current = getter();
if prev.as_ref() != Some(¤t)
&& let Some(style) = el_clone
.dyn_ref::<web_sys::HtmlElement>()
.map(|e| e.style())
.or_else(|| el_clone.dyn_ref::<web_sys::SvgElement>().map(|e| e.style()))
{
let _ = style.set_property(&var_name, ¤t);
}
current
});
}
class_base
}
}
fn hash_recursive(style: &Style, hasher: &mut silex_hash::css::CssHasher) {
for (k, v) in &style.static_rules {
silex_hash::css::Normalized(k).hash(hasher);
silex_hash::css::Normalized(v).hash(hasher);
}
for (prop, _) in &style.dynamic_rules {
silex_hash::css::Normalized(prop).hash(hasher);
"dyn-val".hash(hasher); }
for rule in &style.nested_rules {
match rule {
NestedRule::Media(query, sub) => {
"media".hash(hasher);
silex_hash::css::Normalized(query).hash(hasher);
hash_recursive(sub, hasher);
}
NestedRule::Selector(selector, sub) => {
"selector".hash(hasher);
silex_hash::css::Normalized(selector).hash(hasher);
hash_recursive(sub, hasher);
}
}
}
}
fn generate_css_recursive(
style: &Style,
base_selector: &str,
hash_str: &str,
css_out: &mut String,
dyn_bindings: &mut Vec<(String, DynamicValue)>,
) {
if !style.static_rules.is_empty() || !style.dynamic_rules.is_empty() {
css_out.push_str(base_selector);
css_out.push_str(" {\n");
for (k, v) in &style.static_rules {
let _ = writeln!(css_out, " {}: {};", k, v);
}
for (prop, getter) in &style.dynamic_rules {
let var_name = format!("--sb-{}-{}", hash_str, dyn_bindings.len());
let _ = writeln!(css_out, " {}: var({});", prop, var_name);
dyn_bindings.push((var_name, getter.clone()));
}
css_out.push_str("}\n");
}
for rule in &style.nested_rules {
match rule {
NestedRule::Media(query, sub) => {
css_out.push_str(query);
css_out.push_str(" {\n");
generate_css_recursive(sub, base_selector, hash_str, css_out, dyn_bindings);
css_out.push_str("}\n");
}
NestedRule::Selector(selector, sub) => {
let full_selector = if selector.contains('&') {
selector.replace('&', base_selector)
} else {
format!("{}{}", base_selector, selector)
};
generate_css_recursive(sub, &full_selector, hash_str, css_out, dyn_bindings);
}
}
}
}
impl silex_dom::attribute::ReactiveApply for Style {
fn apply_to_dom(
rx: silex_core::Rx<Self, silex_core::RxValueKind>,
el: web_sys::Element,
_target: silex_dom::attribute::OwnedApplyTarget,
) {
let el = el.clone();
silex_core::reactivity::Effect::new(move |prev_class: Option<String>| {
if let Some(c) = &prev_class {
let _ = el.class_list().remove_1(c);
}
use silex_core::traits::RxGet;
let style = rx.get();
style.apply_to_element(&el)
});
}
}
impl From<Option<Style>> for Style {
fn from(opt: Option<Style>) -> Self {
opt.unwrap_or_default()
}
}
impl IntoStorable for Style {
type Stored = Self;
fn into_storable(self) -> Self::Stored {
self
}
}