mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(widgets): add '\n' support for clock
This commit is contained in:
@@ -270,3 +270,7 @@ battery_low_percent_threshold = 0 # set to e.g. 15 to enable battery_under_thr
|
||||
|
||||
# [widget.notifications]
|
||||
# hide_when_no_unread = true
|
||||
|
||||
# [widget.clock]
|
||||
# format = "{:%H:%M}\n{:%d/%m}"
|
||||
# vertical_format = "{:%H\n%M}"
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr float kStackedPrimaryScale = 0.72f;
|
||||
constexpr float kStackedSecondaryScale = 0.58f;
|
||||
|
||||
std::pair<std::string_view, std::string_view> splitFirstLine(std::string_view text) {
|
||||
const std::size_t newline = text.find('\n');
|
||||
if (newline == std::string_view::npos) {
|
||||
return {text, {}};
|
||||
}
|
||||
return {text.substr(0, newline), text.substr(newline + 1)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ClockWidget::ClockWidget(wl_output* output, std::string format, std::string verticalFormat)
|
||||
: m_output(output), m_format(std::move(format)), m_verticalFormat(std::move(verticalFormat)) {}
|
||||
|
||||
@@ -60,36 +76,97 @@ void ClockWidget::create() {
|
||||
// a stable ink envelope instead of the current text's ink.
|
||||
m_label = label.get();
|
||||
area->addChild(std::move(label));
|
||||
|
||||
auto secondaryLabel = std::make_unique<Label>();
|
||||
secondaryLabel->setBold(false);
|
||||
secondaryLabel->setTextAlign(TextAlign::Center);
|
||||
secondaryLabel->setFontSize(Style::fontSizeBody * m_contentScale * kStackedSecondaryScale);
|
||||
secondaryLabel->setVisible(false);
|
||||
m_secondaryLabel = secondaryLabel.get();
|
||||
area->addChild(std::move(secondaryLabel));
|
||||
|
||||
setRoot(std::move(area));
|
||||
}
|
||||
|
||||
void ClockWidget::doLayout(Renderer& renderer, float containerWidth, float containerHeight) {
|
||||
auto* rootNode = root();
|
||||
if (m_label == nullptr || rootNode == nullptr) {
|
||||
if (m_label == nullptr || m_secondaryLabel == nullptr || rootNode == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_isVertical = containerHeight > containerWidth;
|
||||
update(renderer);
|
||||
m_label->setColor(widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface)));
|
||||
// Horizontal clocks should use single-line metrics so capsule height matches sibling widgets.
|
||||
const ColorSpec foreground = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface));
|
||||
m_label->setColor(foreground);
|
||||
m_secondaryLabel->setColor(foreground);
|
||||
|
||||
const bool showSecondary = !m_isVertical && !m_lastSecondaryText.empty();
|
||||
float primaryFontSize = Style::fontSizeBody * m_contentScale * (showSecondary ? kStackedPrimaryScale : 1.0f);
|
||||
float secondaryFontSize = Style::fontSizeBody * m_contentScale * kStackedSecondaryScale;
|
||||
|
||||
// Horizontal clocks use single-line metrics unless the configured format
|
||||
// explicitly contains line breaks.
|
||||
m_label->setFontSize(primaryFontSize);
|
||||
m_secondaryLabel->setFontSize(secondaryFontSize);
|
||||
m_label->setMaxLines(m_isVertical ? 0 : 1);
|
||||
m_label->setMinWidth(0.0f);
|
||||
m_label->setMaxWidth(m_isVertical ? containerWidth : 0.0f);
|
||||
m_label->measure(renderer);
|
||||
m_label->setPosition(0.0f, 0.0f);
|
||||
rootNode->setSize(m_label->width(), m_label->height());
|
||||
|
||||
m_secondaryLabel->setVisible(showSecondary);
|
||||
m_secondaryLabel->setMaxLines(0);
|
||||
m_secondaryLabel->setMinWidth(0.0f);
|
||||
m_secondaryLabel->setMaxWidth(0.0f);
|
||||
if (showSecondary) {
|
||||
m_secondaryLabel->measure(renderer);
|
||||
}
|
||||
|
||||
float width = showSecondary ? std::max(m_label->width(), m_secondaryLabel->width()) : m_label->width();
|
||||
float height = showSecondary ? m_label->height() + m_secondaryLabel->height() : m_label->height();
|
||||
if (!m_isVertical && showSecondary && containerHeight > 0.0f && height > containerHeight) {
|
||||
const float fitScale = std::min(containerHeight / height, 1.0f);
|
||||
primaryFontSize *= fitScale;
|
||||
secondaryFontSize *= fitScale;
|
||||
m_label->setFontSize(primaryFontSize);
|
||||
m_secondaryLabel->setFontSize(secondaryFontSize);
|
||||
m_label->measure(renderer);
|
||||
m_secondaryLabel->measure(renderer);
|
||||
width = std::max(m_label->width(), m_secondaryLabel->width());
|
||||
height = m_label->height() + m_secondaryLabel->height();
|
||||
}
|
||||
|
||||
m_label->setPosition(showSecondary ? std::round((width - m_label->width()) * 0.5f) : 0.0f, 0.0f);
|
||||
if (showSecondary) {
|
||||
m_secondaryLabel->setPosition(std::round((width - m_secondaryLabel->width()) * 0.5f), m_label->height());
|
||||
}
|
||||
rootNode->setSize(width, height);
|
||||
}
|
||||
|
||||
void ClockWidget::doUpdate(Renderer& renderer) {
|
||||
if (m_label == nullptr) {
|
||||
if (m_label == nullptr || m_secondaryLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = formatTimeText();
|
||||
|
||||
if (text != m_lastText) {
|
||||
m_lastText = std::move(text);
|
||||
m_label->setText(m_lastText);
|
||||
}
|
||||
|
||||
std::string primaryText = m_lastText;
|
||||
std::string secondaryText;
|
||||
if (!m_isVertical) {
|
||||
const auto [primary, secondary] = splitFirstLine(m_lastText);
|
||||
primaryText = std::string(primary);
|
||||
secondaryText = std::string(secondary);
|
||||
}
|
||||
|
||||
if (primaryText != m_lastPrimaryText) {
|
||||
m_lastPrimaryText = std::move(primaryText);
|
||||
m_label->setText(m_lastPrimaryText);
|
||||
m_label->measure(renderer);
|
||||
}
|
||||
if (secondaryText != m_lastSecondaryText) {
|
||||
m_lastSecondaryText = std::move(secondaryText);
|
||||
m_secondaryLabel->setText(m_lastSecondaryText);
|
||||
m_secondaryLabel->measure(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,8 @@ private:
|
||||
std::string m_verticalFormat;
|
||||
bool m_isVertical = false;
|
||||
Label* m_label = nullptr;
|
||||
Label* m_secondaryLabel = nullptr;
|
||||
std::string m_lastText;
|
||||
std::string m_lastPrimaryText;
|
||||
std::string m_lastSecondaryText;
|
||||
};
|
||||
|
||||
@@ -51,9 +51,24 @@ namespace {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string normalizeFormatEscapes(std::string_view fmt) {
|
||||
std::string out;
|
||||
out.reserve(fmt.size());
|
||||
for (std::size_t i = 0; i < fmt.size(); ++i) {
|
||||
if (fmt[i] == '\\' && i + 1 < fmt.size() && fmt[i + 1] == 'n') {
|
||||
out.push_back('\n');
|
||||
++i;
|
||||
} else {
|
||||
out.push_back(fmt[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool shouldUseStrftimeCompat(std::string_view fmt) {
|
||||
return fmt.find("%-") != std::string_view::npos ||
|
||||
(fmt.find('{') == std::string_view::npos && fmt.find('%') != std::string_view::npos);
|
||||
(fmt.find('%') != std::string_view::npos &&
|
||||
(fmt.find('{') == std::string_view::npos || fmt.find("{:") != std::string_view::npos));
|
||||
}
|
||||
|
||||
std::string strftimeSpec(std::string_view spec, const std::tm& local) {
|
||||
@@ -129,19 +144,20 @@ namespace {
|
||||
|
||||
std::string formatLocalTime(const char* fmt) {
|
||||
using namespace std::chrono;
|
||||
const std::string normalizedFmt = normalizeFormatEscapes(fmt);
|
||||
const auto now = floor<seconds>(system_clock::now());
|
||||
const std::time_t raw = system_clock::to_time_t(now);
|
||||
std::tm localTm{};
|
||||
localtime_r(&raw, &localTm);
|
||||
if (auto compat = formatStrftimeCompat(fmt, localTm)) {
|
||||
if (auto compat = formatStrftimeCompat(normalizedFmt, localTm)) {
|
||||
return *compat;
|
||||
}
|
||||
|
||||
const auto local = current_zone()->to_local(now);
|
||||
try {
|
||||
return std::vformat(std::locale(""), fmt, std::make_format_args(local));
|
||||
return std::vformat(std::locale(""), normalizedFmt, std::make_format_args(local));
|
||||
} catch (...) {
|
||||
return fmt;
|
||||
return normalizedFmt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user