Browse Source

LibWeb: Add basic input range rendering

Bastiaan van der Plaat 1 year ago
parent
commit
0f37e0ee89

+ 68 - 0
Base/res/html/misc/input-range.html

@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Input range showcase</title>
+    <style>
+        input[type=range] {
+            width: 80%;
+        }
+
+        .fancy {
+            -webkit-appearance: none;
+            margin: 18px 0;
+        }
+
+        .fancy:focus {
+            outline: none;
+        }
+
+        .fancy::-webkit-slider-runnable-track {
+            width: 100%;
+            height: 8.4px;
+            cursor: drag;
+            box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+            background: #3071a9;
+            border-radius: 1.3px;
+            border: 0.2px solid #010101;
+        }
+
+        .fancy::-webkit-slider-thumb {
+            box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+            border: 1px solid #000000;
+            height: 36px;
+            width: 16px;
+            border-radius: 3px;
+            background: #ffffff;
+            cursor: pointer;
+            -webkit-appearance: none;
+            margin-top: -14px;
+        }
+
+        .fancy:focus::-webkit-slider-runnable-track {
+            background: #367ebd;
+        }
+    </style>
+</head>
+<body>
+    <h1>Input range showcase</h1>
+    <p>
+        <input type="range" oninput="document.getElementById('a-value').textContent = this.value">
+        Value: <span id="a-value">?</span>
+    </p>
+    <p>
+        <input type="range" value="0" class="fancy" oninput="document.getElementById('b-value').textContent = this.value">
+        Value: <span id="b-value">?</span>
+    </p>
+    <p>
+        <input type="range" min="10" value="11" max="15" oninput="document.getElementById('c-value').textContent = this.value">
+        Value: <span id="c-value">?</span>
+    </p>
+    <p>
+        <input type="range" class="fancy" min="50" max="200" step="5"
+            oninput="document.getElementById('d-value').textContent = this.value">
+        Value: <span id="d-value">?</span>
+    </p>
+</body>
+</html>

+ 1 - 1
Base/res/html/misc/input.html

@@ -29,7 +29,7 @@
     <input type="time" id="time" value="time" /><br />
     <input type="time" id="time" value="time" /><br />
     <input type="datetime-local" id="datetime-local" value="datetime-local" /><br />
     <input type="datetime-local" id="datetime-local" value="datetime-local" /><br />
     <input type="number" id="number" value="number" /><br />
     <input type="number" id="number" value="number" /><br />
-    <input type="range" id="range" value="range" /><br />
+    <input type="range" id="range" value="25" /><br />
     <input type="color" id="color" value="color" /><br />
     <input type="color" id="color" value="color" /><br />
     <input type="checkbox" id="checkbox" value="checkbox" /><br />
     <input type="checkbox" id="checkbox" value="checkbox" /><br />
     <input type="radio" id="radio-a" value="a" name="test-radio" /><br />
     <input type="radio" id="radio-a" value="a" name="test-radio" /><br />

+ 25 - 1
Userland/Libraries/LibWeb/CSS/Default.css

@@ -26,7 +26,7 @@ label {
 }
 }
 
 
 /* FIXME: This is a temporary hack until we can render a native-looking frame for these. */
 /* FIXME: This is a temporary hack until we can render a native-looking frame for these. */
-input:not([type=submit], input[type=button], input[type=reset], input[type=color], input[type=checkbox], input[type=radio]), textarea {
+input:not([type=submit], input[type=button], input[type=reset], input[type=color], input[type=checkbox], input[type=radio], input[type=range]), textarea {
     border: 1px solid ButtonBorder;
     border: 1px solid ButtonBorder;
     min-height: 16px;
     min-height: 16px;
     width: attr(size ch, 20ch);
     width: attr(size ch, 20ch);
@@ -70,6 +70,30 @@ option {
     display: none;
     display: none;
 }
 }
 
 
+/* Custom <input type="range"> styles */
+input[type=range] {
+    display: inline-block;
+    width: 20ch;
+    height: 16px;
+}
+input[type=range]::-webkit-slider-runnable-track, input[type=range]::-webkit-slider-thumb {
+    display: block;
+}
+input[type=range]::-webkit-slider-runnable-track {
+    height: 4px;
+    margin-top: 6px;
+    background-color:  hsl(217, 71%, 53%);
+    border: 1px solid rgba(0, 0, 0, 0.5);
+}
+input[type=range]::-webkit-slider-thumb {
+    margin-top: -6px;
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    background-color: hsl(0, 0%, 96%);
+    outline: 1px solid rgba(0, 0, 0, 0.5);
+}
+
 /* Custom <meter> styles */
 /* Custom <meter> styles */
 meter {
 meter {
     display: inline-block;
     display: inline-block;

+ 8 - 0
Userland/Libraries/LibWeb/CSS/Selector.cpp

@@ -390,6 +390,10 @@ StringView Selector::PseudoElement::name(Selector::PseudoElement::Type pseudo_el
         return "placeholder"sv;
         return "placeholder"sv;
     case Selector::PseudoElement::Type::Selection:
     case Selector::PseudoElement::Type::Selection:
         return "selection"sv;
         return "selection"sv;
+    case Selector::PseudoElement::Type::SliderRunnableTrack:
+        return "-webkit-slider-runnable-track"sv;
+    case Selector::PseudoElement::Type::SliderThumb:
+        return "-webkit-slider-thumb"sv;
     case Selector::PseudoElement::Type::KnownPseudoElementCount:
     case Selector::PseudoElement::Type::KnownPseudoElementCount:
         break;
         break;
     case Selector::PseudoElement::Type::UnknownWebKit:
     case Selector::PseudoElement::Type::UnknownWebKit:
@@ -426,6 +430,10 @@ Optional<Selector::PseudoElement> Selector::PseudoElement::from_string(FlyString
         return Selector::PseudoElement { Selector::PseudoElement::Type::Placeholder };
         return Selector::PseudoElement { Selector::PseudoElement::Type::Placeholder };
     } else if (name.equals_ignoring_ascii_case("selection"sv)) {
     } else if (name.equals_ignoring_ascii_case("selection"sv)) {
         return Selector::PseudoElement { Selector::PseudoElement::Type::Selection };
         return Selector::PseudoElement { Selector::PseudoElement::Type::Selection };
+    } else if (name.equals_ignoring_ascii_case("-webkit-slider-runnable-track"sv)) {
+        return Selector::PseudoElement { Selector::PseudoElement::Type::SliderRunnableTrack };
+    } else if (name.equals_ignoring_ascii_case("-webkit-slider-thumb"sv)) {
+        return Selector::PseudoElement { Selector::PseudoElement::Type::SliderThumb };
     }
     }
     return {};
     return {};
 }
 }

+ 2 - 0
Userland/Libraries/LibWeb/CSS/Selector.h

@@ -37,6 +37,8 @@ public:
             ProgressBar,
             ProgressBar,
             Placeholder,
             Placeholder,
             Selection,
             Selection,
+            SliderRunnableTrack,
+            SliderThumb,
 
 
             // Keep this last.
             // Keep this last.
             KnownPseudoElementCount,
             KnownPseudoElementCount,

+ 60 - 1
Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp

@@ -3,6 +3,7 @@
  * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
  * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
  * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
  * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
  * Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
  * Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
+ * Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
  *
  *
  * SPDX-License-Identifier: BSD-2-Clause
  * SPDX-License-Identifier: BSD-2-Clause
  */
  */
@@ -62,6 +63,7 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor)
     visitor.visit(m_color_well_element);
     visitor.visit(m_color_well_element);
     visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group);
     visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group);
     visitor.visit(m_selected_files);
     visitor.visit(m_selected_files);
+    visitor.visit(m_slider_thumb);
 }
 }
 
 
 JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
 JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
@@ -547,6 +549,9 @@ void HTMLInputElement::create_shadow_tree_if_needed()
     case TypeAttributeState::Color:
     case TypeAttributeState::Color:
         create_color_input_shadow_tree();
         create_color_input_shadow_tree();
         break;
         break;
+    case TypeAttributeState::Range:
+        create_range_input_shadow_tree();
+        break;
     // FIXME: This could be better factored. Everything except the above types becomes a text input.
     // FIXME: This could be better factored. Everything except the above types becomes a text input.
     default:
     default:
         create_text_input_shadow_tree();
         create_text_input_shadow_tree();
@@ -674,6 +679,38 @@ void HTMLInputElement::create_color_input_shadow_tree()
     set_shadow_root(shadow_root);
     set_shadow_root(shadow_root);
 }
 }
 
 
+void HTMLInputElement::create_range_input_shadow_tree()
+{
+    auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed);
+    set_shadow_root(shadow_root);
+
+    auto slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
+    slider_runnable_track->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderRunnableTrack);
+    MUST(shadow_root->append_child(slider_runnable_track));
+
+    m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
+    m_slider_thumb->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderThumb);
+    MUST(slider_runnable_track->append_child(*m_slider_thumb));
+    update_slider_thumb_element();
+}
+
+void HTMLInputElement::update_slider_thumb_element()
+{
+    double minimum = *min();
+    double maximum = *max();
+
+    double default_value = minimum + (maximum - minimum) / 2;
+    if (maximum < minimum)
+        default_value = minimum;
+
+    double value = MUST(value_as_number());
+    if (!isfinite(value))
+        value = default_value;
+
+    double position = (value - minimum) / (maximum - minimum) * 100;
+    MUST(m_slider_thumb->style_for_bindings()->set_property(CSS::PropertyID::MarginLeft, MUST(String::formatted("{}%", position))));
+}
+
 void HTMLInputElement::did_receive_focus()
 void HTMLInputElement::did_receive_focus()
 {
 {
     auto* browsing_context = document().browsing_context();
     auto* browsing_context = document().browsing_context();
@@ -714,6 +751,9 @@ void HTMLInputElement::attribute_changed(FlyString const& name, Optional<String>
 
 
                 if (type_state() == TypeAttributeState::Color && m_color_well_element)
                 if (type_state() == TypeAttributeState::Color && m_color_well_element)
                     MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
                     MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
+
+                if (type_state() == TypeAttributeState::Range && m_slider_thumb)
+                    update_slider_thumb_element();
             }
             }
         } else {
         } else {
             if (!m_dirty_value) {
             if (!m_dirty_value) {
@@ -722,6 +762,9 @@ void HTMLInputElement::attribute_changed(FlyString const& name, Optional<String>
 
 
                 if (type_state() == TypeAttributeState::Color && m_color_well_element)
                 if (type_state() == TypeAttributeState::Color && m_color_well_element)
                     MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
                     MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
+
+                if (type_state() == TypeAttributeState::Range && m_slider_thumb)
+                    update_slider_thumb_element();
             }
             }
         }
         }
     } else if (name == HTML::AttributeNames::placeholder) {
     } else if (name == HTML::AttributeNames::placeholder) {
@@ -1051,6 +1094,10 @@ Optional<double> HTMLInputElement::convert_string_to_number(StringView input) co
     if (type_state() == TypeAttributeState::Number)
     if (type_state() == TypeAttributeState::Number)
         return parse_floating_point_number(input);
         return parse_floating_point_number(input);
 
 
+    // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-string-number
+    if (type_state() == TypeAttributeState::Range)
+        return parse_floating_point_number(input);
+
     dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type());
     dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type());
     return {};
     return {};
 }
 }
@@ -1062,6 +1109,10 @@ String HTMLInputElement::covert_number_to_string(double input) const
     if (type_state() == TypeAttributeState::Number)
     if (type_state() == TypeAttributeState::Number)
         return MUST(String::number(input));
         return MUST(String::number(input));
 
 
+    // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-number-string
+    if (type_state() == TypeAttributeState::Range)
+        return MUST(String::number(input));
+
     dbgln("HTMLInputElement::covert_number_to_string() not implemented for input type {}", type());
     dbgln("HTMLInputElement::covert_number_to_string() not implemented for input type {}", type());
     return {};
     return {};
 }
 }
@@ -1109,6 +1160,10 @@ double HTMLInputElement::default_step() const
     if (type_state() == TypeAttributeState::Number)
     if (type_state() == TypeAttributeState::Number)
         return 1;
         return 1;
 
 
+    // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-default
+    if (type_state() == TypeAttributeState::Range)
+        return 1;
+
     dbgln("HTMLInputElement::default_step() not implemented for input type {}", type());
     dbgln("HTMLInputElement::default_step() not implemented for input type {}", type());
     return 0;
     return 0;
 }
 }
@@ -1116,10 +1171,14 @@ double HTMLInputElement::default_step() const
 // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale
 // https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale
 double HTMLInputElement::step_scale_factor() const
 double HTMLInputElement::step_scale_factor() const
 {
 {
-    // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-default
+    // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-scale
     if (type_state() == TypeAttributeState::Number)
     if (type_state() == TypeAttributeState::Number)
         return 1;
         return 1;
 
 
+    // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-scale
+    if (type_state() == TypeAttributeState::Range)
+        return 1;
+
     dbgln("HTMLInputElement::step_scale_factor() not implemented for input type {}", type());
     dbgln("HTMLInputElement::step_scale_factor() not implemented for input type {}", type());
     return 0;
     return 0;
 }
 }

+ 5 - 0
Userland/Libraries/LibWeb/HTML/HTMLInputElement.h

@@ -1,6 +1,7 @@
 /*
 /*
  * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
  * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
+ * Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
  *
  *
  * SPDX-License-Identifier: BSD-2-Clause
  * SPDX-License-Identifier: BSD-2-Clause
  */
  */
@@ -202,6 +203,7 @@ private:
     void create_shadow_tree_if_needed();
     void create_shadow_tree_if_needed();
     void create_text_input_shadow_tree();
     void create_text_input_shadow_tree();
     void create_color_input_shadow_tree();
     void create_color_input_shadow_tree();
+    void create_range_input_shadow_tree();
     WebIDL::ExceptionOr<void> run_input_activation_behavior();
     WebIDL::ExceptionOr<void> run_input_activation_behavior();
     void set_checked_within_group();
     void set_checked_within_group();
 
 
@@ -219,6 +221,9 @@ private:
     JS::GCPtr<DOM::Text> m_text_node;
     JS::GCPtr<DOM::Text> m_text_node;
     bool m_checked { false };
     bool m_checked { false };
 
 
+    void update_slider_thumb_element();
+    JS::GCPtr<DOM::Element> m_slider_thumb;
+
     // https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate
     // https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate
     bool m_indeterminate { false };
     bool m_indeterminate { false };