diff --git a/files.cmake b/files.cmake index 0a8df25f3b..efdafd1ca6 100644 --- a/files.cmake +++ b/files.cmake @@ -1484,6 +1484,8 @@ set(DUSK_FILES src/dusk/ui/theme.cpp src/dusk/ui/ui.hpp src/dusk/ui/ui.cpp + src/dusk/ui/window.hpp + src/dusk/ui/window.cpp src/dusk/achievements.cpp src/dusk/iso_validate.cpp src/dusk/livesplit.cpp diff --git a/src/dusk/ui/control_surface.cpp b/src/dusk/ui/control_surface.cpp index 9b65b4bca9..8521002e13 100644 --- a/src/dusk/ui/control_surface.cpp +++ b/src/dusk/ui/control_surface.cpp @@ -3,7 +3,6 @@ #include namespace dusk::ui { - ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone) { switch (tone) { case ControlSurfaceTone::Primary: @@ -22,6 +21,14 @@ ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone) { .activeBorderOpacity = 255, .activeBackgroundOpacity = 116, }; + case ControlSurfaceTone::Window: + return { + .accent = theme::WindowAccent, + .inactiveBorderOpacity = 0, + .inactiveBackgroundOpacity = 26, + .activeBorderOpacity = 200, + .activeBackgroundOpacity = 76, + }; case ControlSurfaceTone::Quiet: default: return { @@ -34,20 +41,12 @@ ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone) { } } -void apply_control_surface_style(Rml::Element* element, const ControlSurfaceStyle& style, - bool active) { +void apply_control_surface_style(Rml::Element* element, const ControlSurfaceStyle& style, bool active) { if (element == nullptr) { return; } - element->SetProperty("border-color", - active ? theme::rgba(style.accent, style.activeBorderOpacity) : - theme::rgba(theme::ElevatedBorder, - style.inactiveBorderOpacity)); - element->SetProperty("background-color", - theme::rgba(style.accent, - active ? style.activeBackgroundOpacity : - style.inactiveBackgroundOpacity)); + element->SetProperty("border-color", active ? theme::rgba(style.accent, style.activeBorderOpacity) : theme::rgba(theme::ElevatedBorder, style.inactiveBorderOpacity)); + element->SetProperty("background-color", theme::rgba(style.accent, active ? style.activeBackgroundOpacity : style.inactiveBackgroundOpacity)); } - } // namespace dusk::ui diff --git a/src/dusk/ui/control_surface.hpp b/src/dusk/ui/control_surface.hpp index 534e4bf665..04ef80c98b 100644 --- a/src/dusk/ui/control_surface.hpp +++ b/src/dusk/ui/control_surface.hpp @@ -7,11 +7,11 @@ class Element; } namespace dusk::ui { - enum class ControlSurfaceTone { Primary, Secondary, Quiet, + Window, }; struct ControlSurfaceStyle { @@ -23,7 +23,5 @@ struct ControlSurfaceStyle { }; ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone); -void apply_control_surface_style(Rml::Element* element, const ControlSurfaceStyle& style, - bool active); - +void apply_control_surface_style(Rml::Element* element, const ControlSurfaceStyle& style, bool active); } // namespace dusk::ui diff --git a/src/dusk/ui/theme.hpp b/src/dusk/ui/theme.hpp index 0b86bc5a85..8fa182232f 100644 --- a/src/dusk/ui/theme.hpp +++ b/src/dusk/ui/theme.hpp @@ -25,9 +25,18 @@ inline constexpr Color ElevatedBorder{160, 191, 182, 96}; inline constexpr Color Transparent{0, 0, 0, 0}; inline constexpr Color Danger{192, 30, 24, 255}; +inline constexpr Color WindowSurface{21, 22, 16, 178}; +inline constexpr Color WindowTitleOverlay{217, 217, 217, 26}; +inline constexpr Color WindowDivider{146, 135, 91, 255}; +inline constexpr Color WindowAccent{194, 164, 45, 255}; +inline constexpr Color WindowAccentSoft{204, 184, 119, 255}; +inline constexpr Color WindowItemSurface{17, 16, 10, 128}; +inline constexpr Color WindowGlyph{224, 219, 200, 255}; +inline constexpr float WindowTabBarHeight = 66.0f; + inline constexpr float BorderRadiusSmall = 8.0f; inline constexpr float BorderRadiusMedium = 12.0f; -inline constexpr float BorderWidth = 1.1f; +inline constexpr float BorderWidth = 2.0f; std::string rgba(Color color, int opacity = -1); std::string dp(float value); diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp new file mode 100644 index 0000000000..c85fc52dfb --- /dev/null +++ b/src/dusk/ui/window.cpp @@ -0,0 +1,399 @@ +#include "window.hpp" +#include "element.hpp" +#include "focus_border.hpp" +#include "label.hpp" +#include "theme.hpp" + +#include + +namespace dusk::ui { +WindowTab::WindowTab(Rml::Element* parent, std::string_view id, std::string_view label, std::function selectedCallback) : m_selectedCallback(std::move(selectedCallback)) { + using namespace theme; + + m_element = append(parent, "button", id); + set_props(m_element, { + {"display", "flex"}, + {"position", "relative"}, + {"flex-direction", "column"}, + {"align-items", "center"}, + {"justify-content", "center"}, + {"box-sizing", "border-box"}, + {"height", "100%"}, + {"padding-left", "20dp"}, + {"padding-right", "20dp"}, + {"background-color", rgba(Transparent)}, + {"border-width", "0"}, + {"cursor", "pointer"}, + {"tab-index", "auto"}, + {"nav-up", "auto"}, + {"nav-down", "auto"}, + {"nav-left", "auto"}, + {"nav-right", "auto"}, + {"font-family", "Inter"}, + }); + + add_focus_border(m_element, BorderRadiusSmall); + + m_label = append_text(m_element, "span", label); + apply_label_style(m_label, LabelStyle::Body); + set_props(m_label, { + {"pointer-events", "none"}, + {"font-size", "20dp"}, + {"letter-spacing", "1dp"}, + {"font-weight", "700"}, + {"text-align", "center"}, + }); + + m_indicator = append(m_element, "div"); + set_props(m_indicator, { + {"position", "absolute"}, + {"left", "0"}, + {"right", "0"}, + {"bottom", dp(-BorderWidth)}, + {"height", dp(2.0f)}, + {"background-color", rgba(WindowAccent, 0)}, + {"pointer-events", "none"}, + }); + + m_element->AddEventListener(Rml::EventId::Click, this); + m_element->AddEventListener(Rml::EventId::Focus, this); + m_element->AddEventListener(Rml::EventId::Blur, this); + m_element->AddEventListener(Rml::EventId::Mouseover, this); + m_element->AddEventListener(Rml::EventId::Mouseout, this); + apply_style(); +} + +WindowTab::~WindowTab() { + if (m_element == nullptr) { + return; + } + + m_element->RemoveEventListener(Rml::EventId::Click, this); + m_element->RemoveEventListener(Rml::EventId::Focus, this); + m_element->RemoveEventListener(Rml::EventId::Blur, this); + m_element->RemoveEventListener(Rml::EventId::Mouseover, this); + m_element->RemoveEventListener(Rml::EventId::Mouseout, this); + m_element = nullptr; +} + +void WindowTab::ProcessEvent(Rml::Event& event) { + switch (event.GetId()) { + case Rml::EventId::Click: + if (m_selectedCallback) { + m_selectedCallback(); + } + break; + case Rml::EventId::Focus: + m_focused = true; + apply_style(); + break; + case Rml::EventId::Blur: + m_focused = false; + apply_style(); + break; + case Rml::EventId::Mouseover: + m_hovered = true; + apply_style(); + break; + case Rml::EventId::Mouseout: + m_hovered = false; + apply_style(); + break; + default: + break; + } +} + +std::string WindowTab::id() const { + return m_element == nullptr ? std::string{} : m_element->GetId(); +} + +void WindowTab::set_selected(bool selected) { + if (m_selected == selected) { + return; + } + m_selected = selected; + apply_style(); +} + +void WindowTab::apply_style() { + using namespace theme; + + if (m_element == nullptr) { + return; + } + + const bool active = m_hovered || m_focused; + + int textOpacity; + if (m_selected) { + textOpacity = 255; + } else if (active) { + textOpacity = 200; + } else { + textOpacity = 110; + } + const Color textColor = m_selected ? TextActive : Text; + m_label->SetProperty("color", rgba(textColor, textOpacity)); + + if (m_indicator != nullptr) { + const int indicatorOpacity = m_selected ? 255 : (active ? 96 : 0); + m_indicator->SetProperty("background-color", rgba(WindowAccent, indicatorOpacity)); + } + + set_focus_border_visible(m_element, m_focused); +} + +Window::Window(Rml::Element* parent, std::string_view id, std::function closeCallback) : m_closeCallback(std::move(closeCallback)) { + using namespace theme; + + m_element = append(parent, "div", id); + set_props(m_element, { + {"display", "flex"}, + {"flex-direction", "column"}, + {"box-sizing", "border-box"}, + {"width", "100%"}, + {"max-width", "1088dp"}, + {"border-width", dp(BorderWidth)}, + {"border-radius", dp(BorderRadiusMedium)}, + {"border-color", rgba(ElevatedBorder)}, + {"background-color", rgba(WindowSurface)}, + {"overflow", "hidden"}, + }); + + m_tabBar = append(m_element, "div"); + set_props(m_tabBar, { + {"display", "flex"}, + {"position", "relative"}, + {"flex-direction", "row"}, + {"align-items", "center"}, + {"box-sizing", "border-box"}, + {"width", "100%"}, + {"height", dp(WindowTabBarHeight)}, + {"min-height", dp(WindowTabBarHeight)}, + {"padding-left", "12dp"}, + {"padding-right", "12dp"}, + {"gap", "4dp"}, + {"background-color", rgba(WindowTitleOverlay)}, + {"border-bottom-width", dp(BorderWidth * 1.5f)}, + {"border-bottom-color", rgba(WindowDivider)}, + }); + + m_tabStrip = append(m_tabBar, "div"); + set_props(m_tabStrip, { + {"display", "flex"}, + {"flex-direction", "row"}, + {"align-items", "stretch"}, + {"justify-content", "flex-start"}, + {"flex-grow", "1"}, + {"flex-shrink", "1"}, + {"min-width", "0"}, + {"height", "100%"}, + {"gap", "4dp"}, + }); + + const std::string closeId = id.empty() ? std::string{} : std::string(id) + "-close"; + m_closeButton = append(m_tabBar, "button", closeId); + set_props(m_closeButton, { + {"display", "flex"}, + {"position", "relative"}, + {"align-items", "center"}, + {"justify-content", "center"}, + {"box-sizing", "border-box"}, + {"width", "36dp"}, + {"height", "36dp"}, + {"flex-shrink", "0"}, + {"border-width", "0"}, + {"border-radius", dp(BorderRadiusSmall)}, + {"background-color", rgba(Transparent)}, + {"cursor", "pointer"}, + {"tab-index", "auto"}, + {"font-family", "Inter"}, + }); + add_focus_border(m_closeButton, BorderRadiusSmall); + + auto* closeGlyph = append_text(m_closeButton, "span", "\xc3\x97"); + set_props(closeGlyph, { + {"font-size", "22dp"}, + {"font-weight", "400"}, + {"color", rgba(WindowGlyph)}, + {"pointer-events", "none"}, + }); + + m_closeButton->AddEventListener(Rml::EventId::Click, this); + m_closeButton->AddEventListener(Rml::EventId::Focus, this); + m_closeButton->AddEventListener(Rml::EventId::Blur, this); + m_closeButton->AddEventListener(Rml::EventId::Mouseover, this); + m_closeButton->AddEventListener(Rml::EventId::Mouseout, this); + + m_contentRow = append(m_element, "div"); + set_props(m_contentRow, { + {"display", "flex"}, + {"flex-direction", "row"}, + {"align-items", "stretch"}, + {"box-sizing", "border-box"}, + {"width", "100%"}, + {"flex-grow", "1"}, + {"flex-shrink", "1"}, + {"min-height", "0"}, + {"min-width", "0"}, + {"gap", "20dp"}, + }); + + m_leftPane = append(m_contentRow, "div"); + set_props(m_leftPane, { + {"display", "flex"}, + {"flex-direction", "column"}, + {"box-sizing", "border-box"}, + {"min-width", "0"}, + {"min-height", "0"}, + {"padding", "24dp"}, + {"gap", "12dp"}, + }); + + m_rightPane = append(m_contentRow, "div"); + set_props(m_rightPane, { + {"display", "none"}, + {"flex-direction", "column"}, + {"box-sizing", "border-box"}, + {"min-width", "0"}, + {"min-height", "0"}, + {"padding-top", "24dp"}, + {"padding-bottom", "24dp"}, + {"padding-right", "24dp"}, + {"padding-left", "8dp"}, + {"align-items", "flex-start"}, + {"overflow-y", "auto"}, + }); + + apply_close_style(); +} + +Window::~Window() { + m_tabs.clear(); + if (m_closeButton != nullptr) { + m_closeButton->RemoveEventListener(Rml::EventId::Click, this); + m_closeButton->RemoveEventListener(Rml::EventId::Focus, this); + m_closeButton->RemoveEventListener(Rml::EventId::Blur, this); + m_closeButton->RemoveEventListener(Rml::EventId::Mouseover, this); + m_closeButton->RemoveEventListener(Rml::EventId::Mouseout, this); + m_closeButton = nullptr; + } + m_element = nullptr; +} + +void Window::ProcessEvent(Rml::Event& event) { + if (event.GetTargetElement() != m_closeButton) { + return; + } + + switch (event.GetId()) { + case Rml::EventId::Click: + if (m_closeCallback) { + m_closeCallback(); + } + break; + case Rml::EventId::Focus: + m_closeFocused = true; + apply_close_style(); + break; + case Rml::EventId::Blur: + m_closeFocused = false; + apply_close_style(); + break; + case Rml::EventId::Mouseover: + m_closeHovered = true; + apply_close_style(); + break; + case Rml::EventId::Mouseout: + m_closeHovered = false; + apply_close_style(); + break; + default: + break; + } +} + +WindowTab* Window::add_tab(std::string_view id, std::string_view label, std::function selectedCallback) { + if (m_tabStrip == nullptr) { + return nullptr; + } + + const std::string idString(id); + auto wrapped = [this, idString, cb = std::move(selectedCallback)]() { + set_selected_tab(idString); + if (cb) { + cb(); + } + }; + auto tab = std::make_unique(m_tabStrip, idString, label, std::move(wrapped)); + WindowTab* raw = tab.get(); + m_tabs.push_back(std::move(tab)); + return raw; +} + +void Window::set_selected_tab(std::string_view id) { + for (auto& tab : m_tabs) { + tab->set_selected(tab->id() == id); + } +} + +std::string Window::selected_tab_id() const { + for (const auto& tab : m_tabs) { + if (tab->is_selected()) { + return tab->id(); + } + } + return {}; +} + +void Window::set_right_pane_visible(bool visible) { + if (m_rightPaneVisible == visible) { + return; + } + m_rightPaneVisible = visible; + apply_pane_layout(); +} + +void Window::apply_pane_layout() { + using namespace theme; + + if (m_leftPane == nullptr || m_rightPane == nullptr) { + return; + } + + if (m_rightPaneVisible) { + set_props(m_leftPane, { + {"display", "flex"}, + {"flex", "1 1 0"}, + {"min-width", "0"}, + }); + set_props(m_rightPane, { + {"display", "flex"}, + {"flex", "0 0 40%"}, + {"min-width", "0"}, + }); + } else { + set_props(m_leftPane, { + {"display", "flex"}, + {"flex", "1 1 auto"}, + {"width", "100%"}, + }); + set_props(m_rightPane, { + {"display", "none"}, + }); + } +} + +void Window::apply_close_style() { + using namespace theme; + + if (m_closeButton == nullptr) { + return; + } + + const bool active = m_closeHovered || m_closeFocused; + m_closeButton->SetProperty("background-color", active ? rgba(WindowAccent, 56) : rgba(Transparent)); + set_focus_border_visible(m_closeButton, m_closeFocused); +} +} // namespace dusk::ui diff --git a/src/dusk/ui/window.hpp b/src/dusk/ui/window.hpp new file mode 100644 index 0000000000..ae8433fd91 --- /dev/null +++ b/src/dusk/ui/window.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace Rml { +class Element; +} + +namespace dusk::ui { +class WindowTab : public Rml::EventListener { +public: + WindowTab(Rml::Element* parent, std::string_view id, std::string_view label, std::function selectedCallback); + ~WindowTab() override; + + WindowTab(const WindowTab&) = delete; + WindowTab& operator=(const WindowTab&) = delete; + + void ProcessEvent(Rml::Event& event) override; + + Rml::Element* element() const { return m_element; } + std::string id() const; + + void set_selected(bool selected); + bool is_selected() const { return m_selected; } + +private: + Rml::Element* m_element = nullptr; + Rml::Element* m_label = nullptr; + Rml::Element* m_indicator = nullptr; + std::function m_selectedCallback; + bool m_hovered = false; + bool m_focused = false; + bool m_selected = false; + + void apply_style(); +}; + +class Window : public Rml::EventListener { +public: + Window(Rml::Element* parent, std::string_view id, std::function closeCallback = {}); + ~Window() override; + + Window(const Window&) = delete; + Window& operator=(const Window&) = delete; + + void ProcessEvent(Rml::Event& event) override; + + Rml::Element* element() const { return m_element; } + Rml::Element* body() const { return m_leftPane; } + Rml::Element* right_pane() const { return m_rightPane; } + void set_right_pane_visible(bool visible); + + Rml::Element* tab_strip() const { return m_tabStrip; } + + WindowTab* add_tab(std::string_view id, std::string_view label, std::function selectedCallback); + void set_selected_tab(std::string_view id); + std::string selected_tab_id() const; + +private: + Rml::Element* m_element = nullptr; + Rml::Element* m_tabBar = nullptr; + Rml::Element* m_tabStrip = nullptr; + Rml::Element* m_closeButton = nullptr; + Rml::Element* m_body = nullptr; + std::function m_closeCallback; + std::vector > m_tabs; + bool m_closeHovered = false; + bool m_closeFocused = false; + + void apply_close_style(); +}; +} // namespace dusk::ui