use super::event_listener::{EventListenerGuard, EventTargetExt};
use crate::{AccentColor, Srgba};
use std::ops::Deref;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{css, Comment, HtmlElement, TransitionEvent, Window};
type JsResult<T> = Result<T, JsValue>;
pub(crate) struct AccentColorObserver {
_element: OffscreenElement,
_guard: EventListenerGuard,
}
impl AccentColorObserver {
pub(crate) fn new(
window: &Window,
callback: impl FnMut(AccentColor) + 'static,
) -> Option<(Self, AccentColor)> {
if !supports_accent_color() {
return None;
}
let element = create_element(window)?;
let guard = add_accent_color_listener(window.clone(), element.0.clone(), callback);
let initial_value = get_accent_color_from_computed_style(window, &element.0);
let observer = Self {
_element: element,
_guard: guard?,
};
Some((observer, initial_value))
}
}
fn supports_accent_color() -> bool {
css::supports("color: AccentColor").unwrap_or_default()
}
fn add_accent_color_listener(
window: Window,
element: HtmlElement,
mut callback: impl FnMut(AccentColor) + 'static,
) -> Option<EventListenerGuard> {
add_color_change_listener(&element.clone(), move || {
callback(get_accent_color_from_computed_style(&window, &element))
})
.ok()
}
fn create_element(window: &Window) -> Option<OffscreenElement> {
const COMMENT: &str = concat!(
"this element is used by the '",
env!("CARGO_PKG_NAME"),
"' crate to detect changes to the system accent color"
);
let element = OffscreenElement::new(window, COMMENT)?;
element.style().set_property("color", "AccentColor").ok()?;
Some(element)
}
fn add_color_change_listener(
element: &HtmlElement,
mut f: impl FnMut() + 'static,
) -> JsResult<EventListenerGuard> {
let style = element.style();
style.set_property("transition", "color 0.001ms step-start")?;
element.add_event_listener("transitionstart", move |event: TransitionEvent| {
if event.property_name() == "color" {
f();
}
})
}
fn get_accent_color_from_computed_style(window: &Window, element: &HtmlElement) -> AccentColor {
AccentColor(get_color_from_computed_style(window, element))
}
fn get_color_from_computed_style(window: &Window, element: &HtmlElement) -> Option<Srgba> {
let style = window.get_computed_style(element).ok().flatten()?;
let value = style.get_property_value("color").ok()?;
parse_css_color_value(&value)
}
#[derive(Clone)]
struct OffscreenElement(HtmlElement);
impl OffscreenElement {
fn new(window: &Window, description: &str) -> Option<Self> {
let document = window.document()?;
let body = document.body()?;
let element: HtmlElement = document.create_element("div").ok()?.unchecked_into();
let comment: Comment = document.create_comment(description);
_ = element.append_child(&comment);
element.set_attribute("aria-hidden", "true").ok()?;
_ = element.set_attribute("inert", "");
set_offscreen(&element);
body.append_child(&element).ok()?;
Some(Self(element))
}
}
impl Deref for OffscreenElement {
type Target = HtmlElement;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn set_offscreen(element: &HtmlElement) {
let style = element.style();
_ = style.set_property("position", "fixed");
_ = style.set_property("left", "-100%");
_ = style.set_property("top", "-100%");
}
impl Drop for OffscreenElement {
fn drop(&mut self) {
self.0.remove();
}
}
fn parse_css_color_value(s: &str) -> Option<Srgba> {
const SEPARATOR: &str = ", ";
if let Some(parts) = rgb(s) {
let mut parts = parts.splitn(3, SEPARATOR);
Some(Srgba::from_f64_array([
component(&mut parts)? / 255.,
component(&mut parts)? / 255.,
component(&mut parts)? / 255.,
1.,
]))
} else if let Some(parts) = rgba(s) {
let mut parts = parts.splitn(4, SEPARATOR);
Some(Srgba::from_f64_array([
component(&mut parts)? / 255.,
component(&mut parts)? / 255.,
component(&mut parts)? / 255.,
component(&mut parts)?,
]))
} else {
None
}
}
fn rgb(s: &str) -> Option<&str> {
s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(")"))
}
fn rgba(s: &str) -> Option<&str> {
s.strip_prefix("rgba(").and_then(|s| s.strip_suffix(")"))
}
fn component<'a>(parts: &mut impl Iterator<Item = &'a str>) -> Option<f64> {
let value = parts.next()?;
value.parse().ok()
}