servo-script 0.1.2

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::cell::Ref;
use std::ops::{Add, Div};

use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix, QualName, local_name, ns};
use js::context::JSContext;
use js::rust::HandleObject;
use stylo_dom::ElementState;

use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HTMLMeterElementBinding::HTMLMeterElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document;
use crate::dom::element::{AttributeMutation, Element};
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeTraits};
use crate::dom::nodelist::NodeList;
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;

#[dom_struct]
pub(crate) struct HTMLMeterElement {
    htmlelement: HTMLElement,
    labels_node_list: MutNullableDom<NodeList>,
    shadow_tree: DomRefCell<Option<ShadowTree>>,
}

/// Holds handles to all slots in the UA shadow tree
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct ShadowTree {
    meter_value: Dom<Element>,
}

/// <https://html.spec.whatwg.org/multipage/#the-meter-element>
impl HTMLMeterElement {
    fn new_inherited(
        local_name: LocalName,
        prefix: Option<Prefix>,
        document: &Document,
    ) -> HTMLMeterElement {
        HTMLMeterElement {
            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
            labels_node_list: MutNullableDom::new(None),
            shadow_tree: Default::default(),
        }
    }

    pub(crate) fn new(
        local_name: LocalName,
        prefix: Option<Prefix>,
        document: &Document,
        proto: Option<HandleObject>,
        can_gc: CanGc,
    ) -> DomRoot<HTMLMeterElement> {
        Node::reflect_node_with_proto(
            Box::new(HTMLMeterElement::new_inherited(
                local_name, prefix, document,
            )),
            document,
            proto,
            can_gc,
        )
    }

    fn create_shadow_tree(&self, cx: &mut JSContext) {
        let document = self.owner_document();
        let root = self.upcast::<Element>().attach_ua_shadow_root(cx, true);

        let meter_value = Element::create(
            cx,
            QualName::new(None, ns!(html), local_name!("div")),
            None,
            &document,
            crate::dom::element::ElementCreator::ScriptCreated,
            crate::dom::element::CustomElementCreationMode::Asynchronous,
            None,
        );
        root.upcast::<Node>()
            .AppendChild(cx, meter_value.upcast::<Node>())
            .unwrap();

        let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
            meter_value: meter_value.as_traced(),
        });
        self.upcast::<Node>()
            .dirty(crate::dom::node::NodeDamage::Other);
    }

    fn shadow_tree(&self, cx: &mut JSContext) -> Ref<'_, ShadowTree> {
        if !self.upcast::<Element>().is_shadow_host() {
            self.create_shadow_tree(cx);
        }

        Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref)
            .ok()
            .expect("UA shadow tree was not created")
    }

    fn update_state(&self, cx: &mut JSContext) {
        let value = *self.Value();
        let low = *self.Low();
        let high = *self.High();
        let min = *self.Min();
        let max = *self.Max();
        let optimum = *self.Optimum();

        // If the optimum point is less than the low boundary, then the region between the minimum value and
        // the low boundary must be treated as the optimum region, the region from the low boundary up to the
        // high boundary must be treated as a suboptimal region, and the remaining region must be treated as
        // an even less good region
        let element_state = if optimum < low {
            if value < low {
                ElementState::OPTIMUM
            } else if value <= high {
                ElementState::SUB_OPTIMUM
            } else {
                ElementState::SUB_SUB_OPTIMUM
            }
        }
        // If the optimum point is higher than the high boundary, then the situation is reversed; the region between
        // the high boundary and the maximum value must be treated as the optimum region, the region from the high
        // boundary down to the low boundary must be treated as a suboptimal region, and the remaining region must
        // be treated as an even less good region.
        else if optimum > high {
            if value > high {
                ElementState::OPTIMUM
            } else if value >= low {
                ElementState::SUB_OPTIMUM
            } else {
                ElementState::SUB_SUB_OPTIMUM
            }
        }
        // If the optimum point is equal to the low boundary or the high boundary, or anywhere in between them,
        // then the region between the low and high boundaries of the gauge must be treated as the optimum region,
        // and the low and high parts, if any, must be treated as suboptimal.
        else if (low..=high).contains(&value) {
            ElementState::OPTIMUM
        } else {
            ElementState::SUB_OPTIMUM
        };

        // Set the correct pseudo class
        self.upcast::<Element>()
            .set_state(ElementState::METER_OPTIMUM_STATES, false);
        self.upcast::<Element>().set_state(element_state, true);

        // Update the visual width of the meter
        let shadow_tree = self.shadow_tree(cx);
        let position = (value - min) / (max - min) * 100.0;
        let style = format!("width: {position}%");
        shadow_tree.meter_value.set_string_attribute(
            &local_name!("style"),
            style.into(),
            CanGc::from_cx(cx),
        );
    }
}

impl HTMLMeterElementMethods<crate::DomTypeHolder> for HTMLMeterElement {
    // https://html.spec.whatwg.org/multipage/#dom-lfe-labels
    make_labels_getter!(Labels, labels_node_list);

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-actual>
    fn Value(&self) -> Finite<f64> {
        let min = *self.Min();
        let max = *self.Max();

        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("value"))
                .parse_floating_point_number()
                .map_or(0.0, |candidate_actual_value| {
                    candidate_actual_value.clamp(min, max)
                }),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#dom-meter-value>
    fn SetValue(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>()
            .set_string_attribute(&local_name!("value"), string_value, can_gc);
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-minimum>
    fn Min(&self) -> Finite<f64> {
        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("min"))
                .parse_floating_point_number()
                .unwrap_or(0.0),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#dom-meter-min>
    fn SetMin(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>()
            .set_string_attribute(&local_name!("min"), string_value, can_gc);
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-maximum>
    fn Max(&self) -> Finite<f64> {
        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("max"))
                .parse_floating_point_number()
                .unwrap_or(1.0)
                .max(*self.Min()),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-maximum>
    fn SetMax(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>()
            .set_string_attribute(&local_name!("max"), string_value, can_gc);
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-low>
    fn Low(&self) -> Finite<f64> {
        let min = *self.Min();
        let max = *self.Max();

        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("low"))
                .parse_floating_point_number()
                .map_or(min, |candidate_low_boundary| {
                    candidate_low_boundary.clamp(min, max)
                }),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#dom-meter-low>
    fn SetLow(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>()
            .set_string_attribute(&local_name!("low"), string_value, can_gc);
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-high>
    fn High(&self) -> Finite<f64> {
        let max: f64 = *self.Max();
        let low: f64 = *self.Low();

        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("high"))
                .parse_floating_point_number()
                .map_or(max, |candidate_high_boundary| {
                    if candidate_high_boundary < low {
                        return low;
                    }

                    candidate_high_boundary.clamp(*self.Min(), max)
                }),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#dom-meter-high>
    fn SetHigh(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>()
            .set_string_attribute(&local_name!("high"), string_value, can_gc);
    }

    /// <https://html.spec.whatwg.org/multipage/#concept-meter-optimum>
    fn Optimum(&self) -> Finite<f64> {
        let max = *self.Max();
        let min = *self.Min();

        Finite::wrap(
            self.upcast::<Element>()
                .get_string_attribute(&local_name!("optimum"))
                .parse_floating_point_number()
                .map_or(max.add(min).div(2.0), |candidate_optimum_point| {
                    candidate_optimum_point.clamp(min, max)
                }),
        )
    }

    /// <https://html.spec.whatwg.org/multipage/#dom-meter-optimum>
    fn SetOptimum(&self, value: Finite<f64>, can_gc: CanGc) {
        let mut string_value = DOMString::from((*value).to_string());
        string_value.set_best_representation_of_the_floating_point_number();
        self.upcast::<Element>().set_string_attribute(
            &local_name!("optimum"),
            string_value,
            can_gc,
        );
    }
}

impl VirtualMethods for HTMLMeterElement {
    fn super_type(&self) -> Option<&dyn VirtualMethods> {
        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
    }

    fn attribute_mutated(
        &self,
        cx: &mut js::context::JSContext,
        attr: &Attr,
        mutation: AttributeMutation,
    ) {
        self.super_type()
            .unwrap()
            .attribute_mutated(cx, attr, mutation);

        let is_important_attribute = matches!(
            attr.local_name(),
            &local_name!("high") |
                &local_name!("low") |
                &local_name!("min") |
                &local_name!("max") |
                &local_name!("optimum") |
                &local_name!("value")
        );
        if is_important_attribute {
            self.update_state(cx);
        }
    }

    fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
        self.super_type().unwrap().children_changed(cx, mutation);

        self.update_state(cx);
    }

    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
        self.super_type().unwrap().bind_to_tree(cx, context);

        self.update_state(cx);
    }
}