LibWeb: Add basic HTML meter element support

This commit is contained in:
Bastiaan van der Plaat 2023-11-25 14:32:40 +01:00 committed by Sam Atkins
parent 761d824b72
commit 2107ab823d
13 changed files with 442 additions and 14 deletions

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meter showcase</title>
<style>
meter {
width: 50%;
}
</style>
</head>
<body>
<p>
<button onclick="randomize()">Randomize meters</button>
</p>
<p>Basic meters:</p>
<p>
<meter value="0.5">0.5%<br>50%<br>that is a half</meter>
</p>
<p>
<meter value="4" max="10">4/10</meter>
</p>
<p>
<meter min="1" value="3" max="10">grade 3</meter>
</p>
<p>Meters with values outside <code>low</code> and <code>high</code></p>
<p>
<meter low="4" value="1" max="10"></meter>
</p>
<p>
<meter low="4" high="6" value="9" max="10"></meter>
</p>
<p>Meters with values outside <code>optimum</code></p>
<p>
<meter low="4" high="6" value="9" max="10" optimum="1"></meter>
</p>
<p>Meters with values outside <code>min</code> and <code>max</code></p>
<p>
<meter low="4" value="1" high="5" min="3" max="10" optimum="10"></meter>
</p>
<p>
<meter low="4" value="3" high="5" min="1" max="2" optimum="10"></meter>
</p>
<script>
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomize() {
for (const meter of Array.from(document.querySelectorAll('meter'))) {
meter.value = rand(meter.min * 10, meter.max * 10) / 10;
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,17 @@
<link rel="match" href="reference/meter-ref.html" />
<style>
* {
margin: 0;
}
body {
background-color: white;
}
</style>
<meter value="0.5">50%</meter>
<meter value="4" max="10">4/10</meter>
<meter min="1" value="3" max="10">grade 3</meter>
<meter low="4" value="1" max="10"></meter>
<meter low="4" high="6" value="9" max="10"></meter>
<meter low="4" high="6" value="9" max="10" optimum="1"></meter>
<meter low="4" value="1" high="5" min="3" max="10" optimum="10"></meter>
<meter low="4" value="3" high="5" min="1" max="2" optimum="10"></meter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,15 @@
<style>
* {
margin: 0;
}
body {
background-color: white;
}
</style>
<!-- To rebase:
1. Open meter.html in Ladybird
2. Resize the window to 800x600
3. Right click > "Take Full Screenshot"
4. Update the image below:
-->
<img src="./images/meter-ref.png">

View file

@ -66,6 +66,30 @@ option {
display: none;
}
/* Custom <meter> styles */
meter {
display: inline-block;
width: 300px;
height: 12px;
}
meter::-webkit-meter-bar, meter::-webkit-meter-optimum-value, meter::-webkit-meter-suboptimum-value, meter::-webkit-meter-even-less-good-value {
display: block;
height: 100%;
}
meter::-webkit-meter-bar {
background-color: hsl(0, 0%, 96%);
border: 1px solid rgba(0, 0, 0, 0.5);
}
meter::-webkit-meter-optimum-value {
background-color: hsl(141, 53%, 53%);
}
meter::-webkit-meter-suboptimum-value {
background-color: hsl(48, 100%, 67%);
}
meter::-webkit-meter-even-less-good-value {
background-color: hsl(348, 100%, 61%);
}
/* 15.3.1 Hidden elements
* https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements
*/

View file

@ -373,6 +373,14 @@ Optional<Selector::PseudoElement> pseudo_element_from_string(StringView name)
return Selector::PseudoElement::FirstLine;
} else if (name.equals_ignoring_ascii_case("marker"sv)) {
return Selector::PseudoElement::Marker;
} else if (name.equals_ignoring_ascii_case("-webkit-meter-bar"sv)) {
return Selector::PseudoElement::MeterBar;
} else if (name.equals_ignoring_ascii_case("-webkit-meter-even-less-good-value"sv)) {
return Selector::PseudoElement::MeterEvenLessGoodValue;
} else if (name.equals_ignoring_ascii_case("-webkit-meter-optimum-value"sv)) {
return Selector::PseudoElement::MeterOptimumValue;
} else if (name.equals_ignoring_ascii_case("-webkit-meter-suboptimum-value"sv)) {
return Selector::PseudoElement::MeterSuboptimumValue;
} else if (name.equals_ignoring_ascii_case("-webkit-progress-bar"sv)) {
return Selector::PseudoElement::ProgressBar;
} else if (name.equals_ignoring_ascii_case("-webkit-progress-value"sv)) {

View file

@ -27,6 +27,10 @@ public:
FirstLine,
FirstLetter,
Marker,
MeterBar,
MeterEvenLessGoodValue,
MeterOptimumValue,
MeterSuboptimumValue,
ProgressValue,
ProgressBar,
Placeholder,
@ -217,6 +221,14 @@ constexpr StringView pseudo_element_name(Selector::PseudoElement pseudo_element)
return "first-letter"sv;
case Selector::PseudoElement::Marker:
return "marker"sv;
case Selector::PseudoElement::MeterBar:
return "-webkit-meter-bar"sv;
case Selector::PseudoElement::MeterEvenLessGoodValue:
return "-webkit-meter-even-less-good-value"sv;
case Selector::PseudoElement::MeterOptimumValue:
return "-webkit-meter-optimum-value"sv;
case Selector::PseudoElement::MeterSuboptimumValue:
return "-webkit-meter-suboptimum-value"sv;
case Selector::PseudoElement::ProgressBar:
return "-webkit-progress-bar"sv;
case Selector::PseudoElement::ProgressValue:

View file

@ -559,6 +559,18 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector)
case CSS::Selector::PseudoElement::Marker:
pseudo_element_description = "marker";
break;
case CSS::Selector::PseudoElement::MeterBar:
pseudo_element_description = "-webkit-meter-bar";
break;
case CSS::Selector::PseudoElement::MeterEvenLessGoodValue:
pseudo_element_description = "-webkit-meter-even-less-good-value";
break;
case CSS::Selector::PseudoElement::MeterOptimumValue:
pseudo_element_description = "-webkit-meter-optimum-value";
break;
case CSS::Selector::PseudoElement::MeterSuboptimumValue:
pseudo_element_description = "-webkit-meter-suboptimum-value";
break;
case CSS::Selector::PseudoElement::ProgressBar:
pseudo_element_description = "-webkit-progress-bar";
break;

View file

@ -78,6 +78,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(headers) \
__ENUMERATE_HTML_ATTRIBUTE(height) \
__ENUMERATE_HTML_ATTRIBUTE(hidden) \
__ENUMERATE_HTML_ATTRIBUTE(high) \
__ENUMERATE_HTML_ATTRIBUTE(href) \
__ENUMERATE_HTML_ATTRIBUTE(hreflang) \
__ENUMERATE_HTML_ATTRIBUTE(hspace) \
@ -98,6 +99,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(loading) \
__ENUMERATE_HTML_ATTRIBUTE(longdesc) \
__ENUMERATE_HTML_ATTRIBUTE(loop) \
__ENUMERATE_HTML_ATTRIBUTE(low) \
__ENUMERATE_HTML_ATTRIBUTE(marginheight) \
__ENUMERATE_HTML_ATTRIBUTE(marginwidth) \
__ENUMERATE_HTML_ATTRIBUTE(max) \
@ -195,6 +197,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(onwebkittransitionend) \
__ENUMERATE_HTML_ATTRIBUTE(onwheel) \
__ENUMERATE_HTML_ATTRIBUTE(open) \
__ENUMERATE_HTML_ATTRIBUTE(optimum) \
__ENUMERATE_HTML_ATTRIBUTE(pattern) \
__ENUMERATE_HTML_ATTRIBUTE(ping) \
__ENUMERATE_HTML_ATTRIBUTE(placeholder) \

View file

@ -1,11 +1,15 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/HTMLMeterElement.h>
#include <LibWeb/HTML/Numbers.h>
namespace Web::HTML {
@ -24,4 +28,219 @@ void HTMLMeterElement::initialize(JS::Realm& realm)
set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLMeterElementPrototype>(realm, "HTMLMeterElement"_fly_string));
}
void HTMLMeterElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_meter_value_element);
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-actual
double HTMLMeterElement::value() const
{
// If the value attribute is specified and a value could be parsed out of it, then that value is the candidate actual value. Otherwise, the candidate actual value is zero.
double candidate_value = 0.0;
auto maybe_value_string = get_attribute(HTML::AttributeNames::value);
if (maybe_value_string.has_value()) {
auto maybe_value = parse_floating_point_number(maybe_value_string.value());
if (maybe_value.has_value())
candidate_value = maybe_value.value();
}
// If the candidate actual value is less than the minimum value, then the actual value is the minimum value.
// Otherwise, if the candidate actual value is greater than the maximum value, then the actual value is the maximum value.
// Otherwise, the actual value is the candidate actual value.
return clamp(candidate_value, min(), max());
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_value(double value)
{
TRY(set_attribute(HTML::AttributeNames::value, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-minimum
double HTMLMeterElement::min() const
{
// If the min attribute is specified and a value could be parsed out of it, then the minimum value is that value. Otherwise, the minimum value is zero.
auto maybe_min_string = get_attribute(HTML::AttributeNames::min);
if (maybe_min_string.has_value()) {
auto maybe_min = parse_floating_point_number(maybe_min_string.value());
if (maybe_min.has_value())
return maybe_min.value();
}
return 0;
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_min(double value)
{
TRY(set_attribute(HTML::AttributeNames::min, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-maximum
double HTMLMeterElement::max() const
{
// If the max attribute is specified and a value could be parsed out of it, then the candidate maximum value is that value. Otherwise, the candidate maximum value is 1.0.
double candidate_max = 1.0;
auto maybe_max_string = get_attribute(HTML::AttributeNames::max);
if (maybe_max_string.has_value()) {
auto maybe_max = parse_floating_point_number(maybe_max_string.value());
if (maybe_max.has_value())
candidate_max = maybe_max.value();
}
// If the candidate maximum value is greater than or equal to the minimum value, then the maximum value is the candidate maximum value. Otherwise, the maximum value is the same as the minimum value.
return AK::max(candidate_max, min());
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_max(double value)
{
TRY(set_attribute(HTML::AttributeNames::max, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-low
double HTMLMeterElement::low() const
{
// If the low attribute is specified and a value could be parsed out of it, then the candidate low boundary is that value. Otherwise, the candidate low boundary is the same as the minimum value.
double candidate_low = min();
auto maybe_low_string = get_attribute(HTML::AttributeNames::low);
if (maybe_low_string.has_value()) {
auto maybe_low = parse_floating_point_number(maybe_low_string.value());
if (maybe_low.has_value())
candidate_low = maybe_low.value();
}
// If the candidate low boundary is less than the minimum value, then the low boundary is the minimum value.
// Otherwise, if the candidate low boundary is greater than the maximum value, then the low boundary is the maximum value.
// Otherwise, the low boundary is the candidate low boundary.
return clamp(candidate_low, min(), max());
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_low(double value)
{
TRY(set_attribute(HTML::AttributeNames::low, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-high
double HTMLMeterElement::high() const
{
// If the high attribute is specified and a value could be parsed out of it, then the candidate high boundary is that value. Otherwise, the candidate high boundary is the same as the maximum value.
double candidate_high = max();
auto maybe_high_string = get_attribute(HTML::AttributeNames::high);
if (maybe_high_string.has_value()) {
auto maybe_high = parse_floating_point_number(maybe_high_string.value());
if (maybe_high.has_value())
candidate_high = maybe_high.value();
}
// If the candidate high boundary is less than the low boundary, then the high boundary is the low boundary.
// Otherwise, if the candidate high boundary is greater than the maximum value, then the high boundary is the maximum value.
// Otherwise, the high boundary is the candidate high boundary.
return clamp(candidate_high, low(), max());
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_high(double value)
{
TRY(set_attribute(HTML::AttributeNames::high, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-meter-optimum
double HTMLMeterElement::optimum() const
{
// If the optimum attribute is specified and a value could be parsed out of it, then the candidate optimum point is that value. Otherwise, the candidate optimum point is the midpoint between the minimum value and the maximum value.
double candidate_optimum = (max() + min()) / 2;
auto maybe_optimum_string = get_attribute(HTML::AttributeNames::optimum);
if (maybe_optimum_string.has_value()) {
auto maybe_optimum = parse_floating_point_number(maybe_optimum_string.value());
if (maybe_optimum.has_value())
candidate_optimum = maybe_optimum.value();
}
// If the candidate optimum point is less than the minimum value, then the optimum point is the minimum value.
// Otherwise, if the candidate optimum point is greater than the maximum value, then the optimum point is the maximum value.
// Otherwise, the optimum point is the candidate optimum point.
return clamp(candidate_optimum, min(), max());
}
WebIDL::ExceptionOr<void> HTMLMeterElement::set_optimum(double value)
{
TRY(set_attribute(HTML::AttributeNames::optimum, MUST(String::number(value))));
update_meter_value_element();
document().invalidate_layout();
return {};
}
void HTMLMeterElement::inserted()
{
create_shadow_tree_if_needed();
}
void HTMLMeterElement::create_shadow_tree_if_needed()
{
if (shadow_root_internal())
return;
auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed);
set_shadow_root(shadow_root);
auto meter_bar_element = heap().allocate<MeterBarElement>(realm(), document());
MUST(shadow_root->append_child(*meter_bar_element));
m_meter_value_element = heap().allocate<MeterValueElement>(realm(), document());
MUST(meter_bar_element->append_child(*m_meter_value_element));
update_meter_value_element();
}
void HTMLMeterElement::update_meter_value_element()
{
if (!m_meter_value_element)
return;
// UA requirements for regions of the gauge:
double value = this->value();
double min = this->min();
double max = this->max();
double low = this->low();
double high = this->high();
double optimum = this->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.
if (optimum >= low && optimum <= high) {
if (value >= low && value <= high)
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterOptimumValue);
else
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterSuboptimumValue);
}
// Otherwise, 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.
else if (optimum < low) {
if (value >= low && value <= high)
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterSuboptimumValue);
else
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterEvenLessGoodValue);
}
// Finally, 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 (value >= low && value <= high)
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterSuboptimumValue);
else
m_meter_value_element->set_pseudo_element(CSS::Selector::PseudoElement::MeterEvenLessGoodValue);
}
double position = (value - min) / (max - min) * 100;
MUST(m_meter_value_element->set_attribute(HTML::AttributeNames::style, MUST(String::formatted("width: {}%;", position))));
}
}

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -8,10 +9,38 @@
#pragma once
#include <LibWeb/ARIA/Roles.h>
#include <LibWeb/HTML/HTMLDivElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
class MeterBarElement final : public HTMLDivElement {
JS_CELL(MeterBarElement, HTMLDivElement);
public:
MeterBarElement(DOM::Document& document)
: HTMLDivElement(document, DOM::QualifiedName { HTML::TagNames::div, ""_fly_string, Namespace::HTML })
{
}
virtual Optional<CSS::Selector::PseudoElement> pseudo_element() const override { return CSS::Selector::PseudoElement::MeterBar; }
};
class MeterValueElement final : public HTMLDivElement {
JS_CELL(MeterValueElement, HTMLDivElement);
public:
MeterValueElement(DOM::Document& document)
: HTMLDivElement(document, DOM::QualifiedName { HTML::TagNames::div, ""_fly_string, Namespace::HTML })
{
}
virtual Optional<CSS::Selector::PseudoElement> pseudo_element() const override { return m_pseudo_element; }
void set_pseudo_element(CSS::Selector::PseudoElement pseudo_element) { m_pseudo_element = pseudo_element; }
private:
CSS::Selector::PseudoElement m_pseudo_element;
};
class HTMLMeterElement final : public HTMLElement {
WEB_PLATFORM_OBJECT(HTMLMeterElement, HTMLElement);
JS_DECLARE_ALLOCATOR(HTMLMeterElement);
@ -19,7 +48,22 @@ class HTMLMeterElement final : public HTMLElement {
public:
virtual ~HTMLMeterElement() override;
double value() const;
WebIDL::ExceptionOr<void> set_value(double);
double min() const;
WebIDL::ExceptionOr<void> set_min(double value);
double max() const;
WebIDL::ExceptionOr<void> set_max(double value);
double low() const;
WebIDL::ExceptionOr<void> set_low(double value);
double high() const;
WebIDL::ExceptionOr<void> set_high(double value);
double optimum() const;
WebIDL::ExceptionOr<void> set_optimum(double value);
// ^HTMLElement
virtual void inserted() override;
// https://html.spec.whatwg.org/multipage/forms.html#category-label
virtual bool is_labelable() const override { return true; }
@ -30,6 +74,13 @@ private:
HTMLMeterElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
void create_shadow_tree_if_needed();
void update_meter_value_element();
JS::GCPtr<MeterValueElement> m_meter_value_element;
};
}

View file

@ -5,11 +5,11 @@
interface HTMLMeterElement : HTMLElement {
[HTMLConstructor] constructor();
// FIXME: [CEReactions] attribute double value;
// FIXME: [CEReactions] attribute double min;
// FIXME: [CEReactions] attribute double max;
// FIXME: [CEReactions] attribute double low;
// FIXME: [CEReactions] attribute double high;
// FIXME: [CEReactions] attribute double optimum;
[CEReactions] attribute double value;
[CEReactions] attribute double min;
[CEReactions] attribute double max;
[CEReactions] attribute double low;
[CEReactions] attribute double high;
[CEReactions] attribute double optimum;
// FIXME: readonly attribute NodeList labels;
};

View file

@ -309,15 +309,21 @@ ErrorOr<void> TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::
if (is<DOM::Element>(dom_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
// Special path for ::placeholder, which corresponds to a synthetic DOM element inside the <input> UA shadow root.
// Special path for elements that use pseudo selectors.
// FIXME: This is very hackish. Find a better way to architect this.
if (element.pseudo_element() == CSS::Selector::PseudoElement::Placeholder) {
auto& input_element = verify_cast<HTML::HTMLInputElement>(*element.root().parent_or_shadow_host());
style = TRY(style_computer.compute_style(input_element, CSS::Selector::PseudoElement::Placeholder));
if (input_element.placeholder_value().has_value())
display = style->display();
else
display = CSS::Display::from_short(CSS::Display::Short::None);
if (element.pseudo_element() == CSS::Selector::PseudoElement::Placeholder || element.pseudo_element() == CSS::Selector::PseudoElement::MeterBar || element.pseudo_element() == CSS::Selector::PseudoElement::MeterOptimumValue || element.pseudo_element() == CSS::Selector::PseudoElement::MeterSuboptimumValue || element.pseudo_element() == CSS::Selector::PseudoElement::MeterEvenLessGoodValue) {
auto& parent_element = verify_cast<HTML::HTMLElement>(*element.root().parent_or_shadow_host());
style = TRY(style_computer.compute_style(parent_element, element.pseudo_element()));
display = style->display();
if (element.pseudo_element() == CSS::Selector::PseudoElement::Placeholder) {
auto& input_element = verify_cast<HTML::HTMLInputElement>(parent_element);
if (!input_element.placeholder_value().has_value())
display = CSS::Display::from_short(CSS::Display::Short::None);
}
if (element.pseudo_element() == CSS::Selector::PseudoElement::MeterOptimumValue || element.pseudo_element() == CSS::Selector::PseudoElement::MeterSuboptimumValue || element.pseudo_element() == CSS::Selector::PseudoElement::MeterEvenLessGoodValue) {
auto computed_style = element.computed_css_values();
style->set_property(CSS::PropertyID::Width, computed_style->property(CSS::PropertyID::Width));
}
}
// Common path: this is a regular DOM element. Style should be present already, thanks to Document::update_style().
else {