Newer
Older
minerva / Userland / Libraries / LibGfx / PlasticWindowTheme.cpp
@minerva minerva on 13 Jul 9 KB Initial commit
/*
 * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Array.h>
#include <LibGfx/CharacterBitmap.h>
#include <LibGfx/Gradients.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Palette.h>
#include <LibGfx/PlasticWindowTheme.h>
#include <LibGfx/StylePainter.h>

namespace Gfx {

constexpr float inactive_tint_amount = 0.3f;

static constexpr Color tint_color(Color base, Color tint, float amount)
{
    return base.mixed_with(tint, amount).with_alpha(base.alpha());
}

template<size_t Size>
Array<ColorStop, Size> tint_color_stops(Array<ColorStop, Size> const& base_color_stops, Color tint, float amount)
{
    if (amount == 0.0f)
        return base_color_stops;
    Array<ColorStop, Size> tinted_color_stops;
    for (size_t i = 0; i < Size; i++) {
        tinted_color_stops[i] = base_color_stops[i];
        tinted_color_stops[i].color = tint_color(tinted_color_stops[i].color, tint, amount);
    }
    return tinted_color_stops;
}

// TODO: Somehow allow colors to be configured in the theme .ini file.
static Array const s_title_gradient {
    ColorStop { Color(9, 151, 255), 0.00f },
    ColorStop { Color(0, 83, 238), 0.14f },
    ColorStop { Color(0, 80, 238), 0.40f },
    ColorStop { Color(0, 102, 255), 0.88f },
    ColorStop { Color(0, 102, 255), 0.93f },
    ColorStop { Color(0, 91, 255), 0.95f },
    ColorStop { Color(0, 61, 215), 0.96f },
    ColorStop { Color(0, 61, 215), 1.00f }
};

static Array const s_inactive_title_gradient = tint_color_stops(s_title_gradient, Color::White, inactive_tint_amount);

static Array const s_button_gradient_base {
    ColorStop { Color(72, 146, 247), 0.0f },
    ColorStop { Color(57, 128, 244), 0.05f },
    ColorStop { Color(57, 128, 244), 1.0f }
};

static Array const s_button_gradient_overlay {
    ColorStop { Color(109, 164, 246), 0.0f },
    ColorStop { Color::Transparent, 0.05f },
    ColorStop { Color::Transparent, 1.0f }
};

static constexpr struct FrameColors {
    Color base { 22, 39, 213 };
    Color middle_shade { 22, 80, 217 };
    Color light_shade { 32, 102, 234 };
    Color close_button { 246, 63, 0 };
} s_frame_colors;

static FrameColors const s_inactive_frame_colors {
    .base = tint_color(s_frame_colors.base, Color::White, inactive_tint_amount),
    .middle_shade = tint_color(s_frame_colors.middle_shade, Color::White, inactive_tint_amount),
    .light_shade = tint_color(s_frame_colors.light_shade, Color::White, inactive_tint_amount),
    .close_button = tint_color(s_frame_colors.close_button, Color::White, inactive_tint_amount)
};

static constexpr struct {
    Color border { 38, 83, 174 };
} s_button_colors;

static constexpr Gfx::CharacterBitmap s_window_border_radius_mask {
    "#####"
    "###  "
    "##   "
    "#    "
    "#    "sv,
    5, 5
};

static constexpr Gfx::CharacterBitmap s_window_border_radius_accent {
    "     "
    "   ##"
    "  #  "
    " #   "
    " #   "sv,
    5, 5
};

IntRect PlasticWindowTheme::titlebar_rect(WindowType window_type, WindowMode window_mode, IntRect const& window_rect, Palette const& palette) const
{
    auto window_titlebar_height = titlebar_height(window_type, window_mode, palette);
    // FIXME: Theme notifications.
    if (window_type == WindowType::Notification)
        return ClassicWindowTheme::titlebar_rect(window_type, window_mode, window_rect, palette);
    return { 0, 0, window_rect.width() + palette.window_border_thickness() * 2, window_titlebar_height };
}

static void paint_window_frame(IntRect rect, Painter& painter, Palette const& palette, FrameColors const& frame_colors)
{
    int border_thickness = palette.window_border_thickness();
    auto border_rect = rect.shrunken(border_thickness, border_thickness);
    painter.draw_rect_with_thickness(border_rect, frame_colors.base, border_thickness);
    painter.draw_line(rect.top_left().translated(0, 1), rect.bottom_left(), frame_colors.base);
    painter.draw_line(rect.top_left().translated(1, 1), rect.top_right().translated(-1, 1), frame_colors.light_shade);
    painter.draw_line(rect.top_left().translated(1, 1), rect.bottom_left().translated(1, -1), frame_colors.light_shade);
    painter.draw_line(rect.top_right(), rect.bottom_right(), frame_colors.base);
    painter.draw_line(rect.top_right().translated(-1, 1), rect.bottom_right().translated(-1, -1), frame_colors.middle_shade);
    painter.draw_line(rect.bottom_left(), rect.bottom_right(), frame_colors.base);
    painter.draw_line(rect.bottom_left().translated(1, -1), rect.bottom_right().translated(-1, -1), frame_colors.middle_shade);
}

void PlasticWindowTheme::paint_normal_frame(Painter& painter, WindowState window_state, WindowMode window_mode, IntRect const& window_rect, StringView window_title, Bitmap const& icon, Palette const& palette, IntRect const& leftmost_button_rect, int menu_row_count, bool window_modified) const
{
    // FIXME: Handle these cases.
    (void)icon;
    (void)window_modified;

    bool is_inactive = window_state == WindowState::Inactive;
    auto const& frame_colors = is_inactive ? s_inactive_frame_colors : s_frame_colors;
    auto const& title_gradient = is_inactive ? s_inactive_title_gradient : s_title_gradient;

    auto frame_rect = frame_rect_for_window(WindowType::Normal, window_mode, window_rect, palette, menu_row_count);
    frame_rect.set_location({ 0, 0 });
    paint_window_frame(frame_rect, painter, palette, frame_colors);

    // Draw frame title.
    auto titlebar_rect = this->titlebar_rect(WindowType::Normal, window_mode, window_rect, palette);
    titlebar_rect.set_height(titlebar_rect.height() + palette.window_border_thickness() + 1);
    painter.fill_rect_with_linear_gradient(titlebar_rect, title_gradient, 180);

    auto& title_font = FontDatabase::window_title_font();
    auto clipped_title_rect = titlebar_rect.translated(7, 0);
    clipped_title_rect.set_width(leftmost_button_rect.left() - clipped_title_rect.x());
    if (!clipped_title_rect.is_empty()) {
        auto title_alignment = palette.title_alignment();
        painter.draw_text(clipped_title_rect.translated(1, 2), window_title, title_font, title_alignment, Color(15, 16, 137), Gfx::TextElision::Right);
        // FIXME: The translated(0, 1) wouldn't be necessary if we could center text based on its baseline.
        painter.draw_text(clipped_title_rect.translated(0, 1), window_title, title_font, title_alignment, Color::White, Gfx::TextElision::Right);
    }

    // Paint/clip border radii.
    Gfx::IntRect point(0, 0, 1, 1);
    auto border_radius = s_window_border_radius_mask.width();
    auto left_border_radius_pos = titlebar_rect.location();
    auto right_border_radius_pos = titlebar_rect.location().translated(titlebar_rect.width() - border_radius, 0);
    painter.draw_rect(titlebar_rect, frame_colors.base);
    for (unsigned y = 0; y < s_window_border_radius_mask.height(); y++) {
        for (unsigned x = 0; x < s_window_border_radius_mask.width(); x++) {
            if (s_window_border_radius_mask.bit_at(x, y)) {
                painter.clear_rect(point.translated(left_border_radius_pos).translated(x, y), Color::Transparent);
                painter.clear_rect(point.translated(right_border_radius_pos).translated(border_radius - x, y), Color::Transparent);
            }
            if (s_window_border_radius_accent.bit_at(x, y)) {
                painter.clear_rect(point.translated(left_border_radius_pos).translated(x, y), frame_colors.base);
                painter.clear_rect(point.translated(right_border_radius_pos).translated(border_radius - x, y), frame_colors.base);
            }
        }
    }
}

void PlasticWindowTheme::paint_notification_frame(Painter& painter, WindowMode window_mode, IntRect const& window_rect, Palette const& palette, IntRect const& close_button_rect) const
{
    (void)close_button_rect;

    auto frame_rect = frame_rect_for_window(WindowType::Notification, window_mode, window_rect, palette, 0);
    frame_rect.set_location({ 0, 0 });

    paint_window_frame(frame_rect, painter, palette, s_frame_colors);

    auto titlebar_rect = this->titlebar_rect(WindowType::Notification, window_mode, window_rect, palette);
    painter.fill_rect_with_linear_gradient(titlebar_rect, s_title_gradient, 270);
}

Vector<IntRect> PlasticWindowTheme::layout_buttons(WindowType window_type, WindowMode window_mode, IntRect const& window_rect, Palette const& palette, size_t buttons, bool is_maximized) const
{
    auto button_rects = ClassicWindowTheme::layout_buttons(window_type, window_mode, window_rect, palette, buttons, is_maximized);
    if (window_type != WindowType::Notification) {
        IntPoint offset(-s_window_border_radius_mask.width(), 2);
        for (auto& rect : button_rects)
            rect.translate_by(offset);
    }
    return button_rects;
}

void PlasticWindowTheme::paint_taskbar(Painter& painter, IntRect const& taskbar_rect, Palette const&) const
{
    painter.fill_rect_with_linear_gradient(taskbar_rect, s_title_gradient, 180);
}

void PlasticWindowTheme::paint_button(Painter& painter, IntRect const& rect, Palette const&, ButtonStyle button_style, bool pressed, bool hovered, bool checked, bool enabled, bool focused, bool default_button) const
{
    // FIXME: Handle these cases.
    (void)button_style;
    (void)pressed;
    (void)hovered;
    (void)checked;
    (void)enabled;
    (void)focused;
    (void)default_button;

    if (focused)
        return;

    painter.fill_rect_with_linear_gradient(rect, s_button_gradient_base, 180);
    painter.fill_rect_with_linear_gradient(rect, s_button_gradient_overlay, 160);
    painter.draw_rect(rect, s_button_colors.border);
}

}