/* * Copyright (c) 2022, Frhun * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once #include #include #include #include #include namespace GUI { // The constants used for special values // Their order here, also defines their order among each other for min, max; operations, excluding Regular enum class SpecialDimension : int { Regular = 0, // only really useful for is_one_of Grow = -1, OpportunisticGrow = -2, Fit = -3, Shrink = -4, }; class UIDimension { friend constexpr auto AK::max(GUI::UIDimension const&, GUI::UIDimension const&) -> GUI::UIDimension; friend constexpr auto AK::min(GUI::UIDimension const&, GUI::UIDimension const&) -> GUI::UIDimension; public: UIDimension() = delete; UIDimension(int value) : m_value(value) { VERIFY(value >= 0); } UIDimension(SpecialDimension special) : m_value(to_underlying(special)) { } [[nodiscard]] inline bool is_special_value() const { return m_value < 0; } [[nodiscard]] inline bool is_int() const { return m_value >= 0; } [[nodiscard]] inline bool is_shrink() const { return m_value == to_underlying(SpecialDimension::Shrink); } [[nodiscard]] inline bool is_grow() const { return m_value == to_underlying(SpecialDimension::Grow); } [[nodiscard]] inline bool is_opportunistic_grow() const { return m_value == to_underlying(SpecialDimension::OpportunisticGrow); } [[nodiscard]] inline bool is_fit() const { return m_value == to_underlying(SpecialDimension::Fit); } [[nodiscard]] inline bool is_one_of(std::initializer_list valid_values) const { for (SpecialDimension v : valid_values) { if (m_value == to_underlying(v) || (v == SpecialDimension::Regular && is_int())) return true; } return false; } [[nodiscard]] ALWAYS_INLINE constexpr bool is(SpecialDimension special_value) const { return m_value == to_underlying(special_value) || (special_value == SpecialDimension::Regular && is_int()); } template [[nodiscard]] bool is_one_of(Ts... valid_values) const { return (... || (is(forward(valid_values)))); } [[nodiscard]] inline bool operator==(UIDimension other) const { return m_value == other.m_value; } [[nodiscard]] inline UIDimension must_sum_with(UIDimension other) const { VERIFY(is_int() && other.is_int()); return UIDimension { m_value + other.m_value }; } inline void must_add(int to_add) { VERIFY(is_int()); VERIFY(m_value >= -to_add); m_value += to_add; } inline void add_if_int(int to_add) { if (is_int()) { m_value += to_add; } } [[nodiscard]] inline ErrorOr shrink_value() const { if (m_value >= 0) return m_value; if (m_value == to_underlying(SpecialDimension::Shrink)) return 0; return Error::from_string_literal("value is neither shrink nor an integer ≥0"); } [[nodiscard]] inline int as_int() const { VERIFY(is_int()); return m_value; } [[nodiscard]] AK::JsonValue as_json_value() const { if (is_int()) return m_value; if (is_shrink()) return "shrink"; if (is_grow()) return "grow"; if (is_opportunistic_grow()) return "opportunistic_grow"; if (is_fit()) return "fit"; VERIFY_NOT_REACHED(); } [[nodiscard]] static Optional construct_from_json_value(AK::JsonValue const value) { if (value.is_string()) { String value_literal = value.as_string(); if (value_literal == "shrink") return UIDimension { SpecialDimension::Shrink }; else if (value_literal == "grow") return UIDimension { SpecialDimension::Grow }; else if (value_literal == "opportunistic_grow") return UIDimension { SpecialDimension::OpportunisticGrow }; else if (value_literal == "fit") return UIDimension { SpecialDimension::Fit }; else return {}; } else { int value_int = value.to_i32(); if (value_int < 0) return {}; return UIDimension(value_int); } } private: int m_value; }; class UISize : public Gfx::Size { public: UISize() = delete; UISize(int in_width, int in_height) : Gfx::Size(in_width, in_height) { } UISize(Gfx::IntSize size) : UISize(size.width(), size.height()) { } UISize(SpecialDimension special) : Gfx::Size(UIDimension { special }, UIDimension { special }) { } UISize(UIDimension width, UIDimension height) : Gfx::Size(width, height) { } inline UISize replace_component_if_matching_with(UIDimension to_match, UISize replacement) { if (width() == to_match) set_width(replacement.width()); if (height() == to_match) set_height(replacement.height()); return *this; } [[nodiscard]] inline bool has_only_int_values() const { return width().is_int() && height().is_int(); } [[nodiscard]] inline bool either_is(UIDimension to_match) const { return (width() == to_match || height() == to_match); } explicit operator Gfx::IntSize() const { return Gfx::IntSize(width().as_int(), height().as_int()); } }; } namespace AK { template<> inline auto max(GUI::UIDimension const& a, GUI::UIDimension const& b) -> GUI::UIDimension { if ((a.is_int() && b.is_int()) || (a.is_special_value() && b.is_special_value())) return a.m_value > b.m_value ? a : b; if (a.is_grow() || b.is_grow()) return GUI::SpecialDimension::Grow; if (a.is_opportunistic_grow() || b.is_opportunistic_grow()) return GUI::SpecialDimension::OpportunisticGrow; if (a.is_fit() || b.is_fit()) return GUI::SpecialDimension::Fit; if (a.is_shrink()) return b; if (b.is_shrink()) return a; VERIFY_NOT_REACHED(); } template<> inline auto min(GUI::UIDimension const& a, GUI::UIDimension const& b) -> GUI::UIDimension { if ((a.is_int() && b.is_int()) || (a.is_special_value() && b.is_special_value())) return a.m_value < b.m_value ? a : b; if (a.is_shrink() || b.is_shrink()) return GUI::SpecialDimension::Shrink; if (a.is_int()) return a; if (b.is_int()) return b; if (a.is_fit() || b.is_fit()) return GUI::SpecialDimension::Fit; if (a.is_opportunistic_grow() || b.is_opportunistic_grow()) return GUI::SpecialDimension::OpportunisticGrow; VERIFY_NOT_REACHED(); } template<> inline auto clamp(GUI::UIDimension const& input, GUI::UIDimension const& lower_bound, GUI::UIDimension const& upper_bound) -> GUI::UIDimension { return min(max(input, lower_bound), upper_bound); } } #define REGISTER_UI_DIMENSION_PROPERTY(property_name, getter, setter) \ register_property( \ property_name, \ [this] { \ return this->getter().as_json_value(); \ }, \ [this](auto& value) { \ auto result = GUI::UIDimension::construct_from_json_value(value); \ if (result.has_value()) \ this->setter(result.value()); \ return result.has_value(); \ }); #define REGISTER_READONLY_UI_DIMENSION_PROPERTY(property_name, getter) \ register_property( \ property_name, \ [this] { \ return this->getter().as_json_value(); \ }); #define REGISTER_UI_SIZE_PROPERTY(property_name, getter, setter) \ register_property( \ property_name, \ [this] { \ auto size = this->getter(); \ JsonObject size_object; \ size_object.set("width"sv, size.width().as_json_value()); \ size_object.set("height"sv, size.height().as_json_value()); \ return size_object; \ }, \ [this](auto& value) { \ if (!value.is_object()) \ return false; \ auto result_width = GUI::UIDimension::construct_from_json_value( \ value.as_object().get("width"sv)); \ auto result_height = GUI::UIDimension::construct_from_json_value( \ value.as_object().get("height"sv)); \ if (result_width.has_value() && result_height.has_value()) { \ GUI::UISize size(result_width.value(), result_height.value()); \ setter(size); \ return true; \ } \ return false; \ }); #define REGISTER_READONLY_UI_SIZE_PROPERTY(property_name, getter) \ register_property( \ property_name, \ [this] { \ auto size = this->getter(); \ JsonObject size_object; \ size_object.set("width", size.width().as_json_value()); \ size_object.set("height", size.height().as_json_value()); \ return size_object; \ });