mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-19 01:02:22 +02:00
153 lines
5.4 KiB
C++
153 lines
5.4 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "ui/painter.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include "ui/format.h"
|
|
#include "ui/theme.h"
|
|
#include "util/i18n.h"
|
|
|
|
namespace ui {
|
|
|
|
void Painter::border(const Rect& r, int t, Color c) {
|
|
fill(rect(r.x - t, r.y - t, r.w + 2 * t, t), c);
|
|
fill(rect(r.x - t, r.y + r.h, r.w + 2 * t, t), c);
|
|
fill(rect(r.x - t, r.y, t, r.h), c);
|
|
fill(rect(r.x + r.w, r.y, t, r.h), c);
|
|
}
|
|
|
|
int Painter::label(const std::string& s, int x, int y, Weight w, int px, Color c) {
|
|
return text_->draw(s, x, y, w, px, c);
|
|
}
|
|
|
|
void Painter::label_in(const std::string& s, const Rect& r, Weight w, int px, Color c,
|
|
Align align) {
|
|
const std::string fitted = text_->ellipsize(s, w, px, r.w);
|
|
const int width = text_->measure(fitted, w, px);
|
|
int x = r.x;
|
|
if (align == Align::Center) {
|
|
x = r.x + (r.w - width) / 2;
|
|
} else if (align == Align::Right) {
|
|
x = r.x + r.w - width;
|
|
}
|
|
const int y = r.y + (r.h - text_->line_height(w, px)) / 2;
|
|
text_->draw(fitted, x, y, w, px, c);
|
|
}
|
|
|
|
int Painter::paragraph(const std::string& s, const Rect& r, Weight w, int px, Color c,
|
|
int max_lines) {
|
|
const int line_h = text_->line_height(w, px);
|
|
int y = r.y;
|
|
for (const std::string& line : text_->wrap(s, w, px, r.w, max_lines)) {
|
|
text_->draw(line, r.x, y, w, px, c);
|
|
y += line_h;
|
|
}
|
|
return y - r.y;
|
|
}
|
|
|
|
void Painter::progress(const Rect& r, double fraction, Color fg, Color bg) {
|
|
if (fraction < 0) fraction = 0;
|
|
if (fraction > 1) fraction = 1;
|
|
fill(r, bg);
|
|
fill(rect(r.x, r.y, static_cast<int>(r.w * fraction + 0.5), r.h), fg);
|
|
}
|
|
|
|
void Painter::spinner(int cx, int cy, int radius, Color c) {
|
|
const int dots = 8;
|
|
const int dot = radius / 3 > 4 ? radius / 3 : 4;
|
|
const int head = static_cast<int>(now_ms_ / 110) % dots;
|
|
for (int i = 0; i < dots; ++i) {
|
|
const double a = 2.0 * 3.14159265358979 * i / dots - 3.14159265358979 / 2;
|
|
const int x = cx + static_cast<int>(std::lround(std::cos(a) * radius)) - dot / 2;
|
|
const int y = cy + static_cast<int>(std::lround(std::sin(a) * radius)) - dot / 2;
|
|
const int age = (head - i + dots) % dots; // 0 = most recent point
|
|
Color dc = c;
|
|
dc.a = static_cast<uint8_t>(255 - age * 26);
|
|
fill(rect(x, y, dot, dot), dc);
|
|
}
|
|
}
|
|
|
|
void Painter::play_icon(const Rect& r, Color c) {
|
|
// Triangle pointing right, drawn line by line.
|
|
for (int y = 0; y < r.h; ++y) {
|
|
const int half = r.h / 2;
|
|
const int dist = y < half ? y : r.h - 1 - y;
|
|
const int w = static_cast<int>(static_cast<int64_t>(r.w) * (2 * dist + 1) / r.h);
|
|
fill(rect(r.x, r.y + y, w, 1), c);
|
|
}
|
|
}
|
|
|
|
void Painter::pause_icon(const Rect& r, Color c) {
|
|
const int bar = r.w * 2 / 5;
|
|
fill(rect(r.x, r.y, bar, r.h), c);
|
|
fill(rect(r.x + r.w - bar, r.y, bar, r.h), c);
|
|
}
|
|
|
|
bool Painter::poster_image(const PosterKey& key, const Rect& r) {
|
|
const Poster* p = posters_->get(key);
|
|
if (!p) {
|
|
return false;
|
|
}
|
|
// Centered crop to fill r without distortion.
|
|
Rect src = rect(0, 0, p->w, p->h);
|
|
if (static_cast<int64_t>(p->w) * r.h > static_cast<int64_t>(p->h) * r.w) {
|
|
src.w = static_cast<int>(static_cast<int64_t>(p->h) * r.w / r.h);
|
|
src.x = (p->w - src.w) / 2;
|
|
} else {
|
|
src.h = static_cast<int>(static_cast<int64_t>(p->w) * r.h / r.w);
|
|
src.y = (p->h - src.h) / 2;
|
|
}
|
|
canvas_->draw_texture(p->texture, src, r, rgba(255, 255, 255));
|
|
return true;
|
|
}
|
|
|
|
void Painter::poster(const api::MediaItem& item, const Rect& r, bool focused, int fetch_width) {
|
|
if (!poster_image(poster_key_for(item, fetch_width), r)) {
|
|
// Replacement: plain background and title (also while loading).
|
|
fill(r, focused ? theme::surface_focus() : theme::surface());
|
|
paragraph(display_title(item), rect(r.x + 14, r.y + r.h / 3, r.w - 28, r.h / 2),
|
|
Weight::Bold, theme::kSmall, theme::text_dim(), 4);
|
|
}
|
|
const double done = progress_fraction(item);
|
|
if (done > 0) {
|
|
progress(rect(r.x, r.y + r.h - 8, r.w, 8), done, theme::accent(), rgba(0, 0, 0, 160));
|
|
}
|
|
if (item.played) {
|
|
const Rect badge = rect(r.x + r.w - 62, r.y + 10, 52, 34);
|
|
fill(badge, theme::accent());
|
|
label_in(util::tr(util::Str::Watched), badge, Weight::Bold, 22, theme::text(), Align::Center);
|
|
}
|
|
if (focused) {
|
|
border(r, theme::kFocusBorder, theme::focus_ring());
|
|
}
|
|
}
|
|
|
|
void Painter::button(const Rect& r, const std::string& s, bool focused, bool with_icon) {
|
|
const Color fg = focused ? theme::background() : theme::text();
|
|
fill(r, focused ? theme::text() : theme::surface());
|
|
const int icon = with_icon ? 40 : 0; // room reserved for the triangle
|
|
if (with_icon) {
|
|
play_icon(rect(r.x + 30, r.y + (r.h - 26) / 2, 22, 26), fg);
|
|
}
|
|
label_in(s, rect(r.x + 24 + icon, r.y, r.w - 48 - icon, r.h), Weight::Bold, theme::kBody, fg,
|
|
Align::Center);
|
|
}
|
|
|
|
void Painter::hints(const std::string& s) {
|
|
label_in(s, rect(theme::kMarginX, theme::kScreenH - theme::kMarginY - 34, theme::kContentW, 34),
|
|
Weight::Regular, theme::kSmall, theme::text_dim(), Align::Right);
|
|
}
|
|
|
|
void Painter::status(const Rect& r, const std::string& s, bool waiting, bool error) {
|
|
const int line = text_->line_height(Weight::Regular, theme::kBody);
|
|
int y = r.y + (r.h - line) / 2;
|
|
if (waiting) {
|
|
spinner(r.x + r.w / 2, y - 50, 26, theme::text());
|
|
}
|
|
label_in(s, rect(r.x, y, r.w, line), Weight::Regular, theme::kBody,
|
|
error ? theme::danger() : theme::text_dim(), Align::Center);
|
|
}
|
|
|
|
} // namespace ui
|