Compare commits

..
3 Commits
Author SHA1 Message Date
justmeandClaude Sonnet 5.5 99fe8f589f Fetch libmonet in build.sh and document monet colours
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 00:23:12 +08:00
justmeandClaude Sonnet 5.5 5bea01e224 Colour the candidate window from the desktop accent via libmonet
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 00:23:12 +08:00
justmeandClaude Sonnet 5.5 064ca6b4d4 Map a libmonet scheme onto candidate window colours
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 00:23:12 +08:00
10 changed files with 215 additions and 4 deletions

No files matched your search

+7 -1
View File
@@ -47,7 +47,7 @@ sudo dnf install fcitx5-devel extra-cmake-modules cairo-devel pango-devel \
然后:
```
scripts/build.sh # 下载 Fcitx5 和 yoga 源码,叠加 src/,构建 m3ui
scripts/build.sh # 下载 Fcitx5、yoga 和 libmonet 源码,叠加 src/,构建 m3ui
scripts/install.sh # 安装到 ~/.local,不改动 $HOME 以外的任何东西
```
@@ -56,6 +56,12 @@ scripts/install.sh # 安装到 ~/.local,不改动 $HOME 以外的任何东西
没有固定提交的版本会被拒绝,除非你自己用 `FCITX_COMMIT=<sha>` 指定。这个插件跑在输入法进程里,
看得到你打的所有字,所以构建来源值得认真对待。
## 莫奈取色
候选窗的颜色由 [libmonet](https://git.oeo.one/justme/libmonet) 从桌面强调色生成(`UseMonet=True`,默认开),
深浅色跟随系统。输入法进程内不会启动任何子进程:直接复用 Fcitx5 自己监听的 XDG portal
(KDE Plasma 6、GNOME 47+ 都有),取不到时回退到默认的粉色。`UseMonet=False` 回到主题文件里的固定颜色。
## 使用
手动试用:
+1
View File
@@ -10,6 +10,7 @@ UseAccentColor=False
PerScreenDPI=True
ForceWaylandDPI=0
EnableFractionalScale=True
UseMonet=True
Animations=True
Bounce=True
SpatialStiffness=2000
+5
View File
@@ -27,6 +27,8 @@ commit=${FCITX_COMMIT:-${known_commits[$version]:-}}
[ -n "$commit" ] || die "no pinned commit for Fcitx5 $version. Look up the commit of its tag and pass it as FCITX_COMMIT=<sha>"
# Commit of the yoga submodule pinned by Fcitx5 (third_party/yoga).
yoga_commit=${YOGA_COMMIT:-042f5013152eb81c1552dec945b88f7b95ca350f}
# libmonet (header-only) supplies the colours derived from the desktop accent.
monet_commit=${MONET_COMMIT:-b97a55ac75ae82435628dc585cccadb45e4d2feb}
# fetch_commit <url> <dir> <commit>, leaves <dir> checked out at exactly <commit>
fetch_commit() {
@@ -48,6 +50,8 @@ echo ">> fetching Fcitx5 $version ($commit)"
fetch_commit https://github.com/fcitx/fcitx5 "$srcdir" "$commit"
echo ">> fetching yoga ($yoga_commit)"
fetch_commit https://github.com/facebook/yoga "$srcdir/third_party/yoga" "$yoga_commit"
echo ">> fetching libmonet ($monet_commit)"
fetch_commit https://git.oeo.one/justme/libmonet.git "$work/libmonet" "$monet_commit"
[ -z "${FETCH_ONLY:-}" ] || { echo ">> sources are in place"; exit 0; }
echo ">> overlaying m3ui"
@@ -61,6 +65,7 @@ fi
echo ">> configuring"
cmake -S "$srcdir" -B "$work/cmake" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DMONET_INCLUDE_DIR="$work/libmonet/include" \
-DCMAKE_INSTALL_PREFIX="${PREFIX:-$HOME/.local/m3ui}" \
-DENABLE_X11=On -DENABLE_WAYLAND=On \
-DENABLE_TEST=Off -DENABLE_TESTING_ADDONS=Off -DENABLE_ENCHANT=Off \
+8
View File
@@ -29,6 +29,12 @@ if (ENABLE_DBUS)
set(CLASSICUI_LIBS ${CLASSICUI_LIBS} Fcitx5::Module::DBus)
endif()
# libmonet, the shared colour code, header only. build.sh fetches it at a pinned
# commit and passes the directory.
if (NOT MONET_INCLUDE_DIR OR NOT EXISTS "${MONET_INCLUDE_DIR}/monet/monet.h")
message(FATAL_ERROR "MONET_INCLUDE_DIR must point at the include directory of libmonet")
endif()
add_fcitx5_addon(m3ui
classicui.cpp window.cpp theme.cpp inputwindow.cpp plasmathemewatchdog.cpp colorhelper.cpp ${CLASSICUI_SRCS}
)
@@ -37,6 +43,8 @@ if (ENABLE_X11)
target_compile_definitions(m3ui PRIVATE -DFCITX_XCB_EWMH)
endif()
target_include_directories(m3ui PRIVATE "${MONET_INCLUDE_DIR}")
target_link_libraries(m3ui
Fcitx5::Core
PkgConfig::Cairo PkgConfig::Pango
+53 -3
View File
@@ -7,6 +7,7 @@
#include "classicui.h"
#include <fcntl.h>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
@@ -180,6 +181,7 @@ void M3UI::reloadTheme() {
auto color = variant.dataAs<uint32_t>();
auto oldIsDark = isDark_;
isDark_ = (color == 1);
portalSchemeKnown_ = true;
if (oldIsDark != isDark_) {
CLASSICUI_DEBUG() << "XDG Portal AppearanceChanged "
"isDark"
@@ -189,8 +191,10 @@ void M3UI::reloadTheme() {
}
};
// The portal is also how the colours for UseMonet are learned, over the
// connection Fcitx5 already has, so nothing has to be started for it.
if (dbus()) {
if (*config_.useDarkTheme) {
if (*config_.useDarkTheme || *config_.useMonet) {
if (!darkModeEntry_ && settingMonitor_) {
darkModeEntry_ = settingMonitor_->watch(
"org.freedesktop.appearance", "color-scheme", parseMessage);
@@ -253,7 +257,7 @@ void M3UI::reloadTheme() {
};
if (dbus()) {
if (*config_.useAccentColor) {
if (*config_.useAccentColor || *config_.useMonet) {
if (!accentColorEntry_ && settingMonitor_) {
accentColorEntry_ =
settingMonitor_->watch("org.freedesktop.appearance",
@@ -266,7 +270,53 @@ void M3UI::reloadTheme() {
}
#endif
theme_.populateColor(accentColor_);
if (*config_.useMonet) {
theme_.applyMonet(mapScheme(monetScheme()));
}
// The accent colour is only watched for UseMonet, it must not recolour the
// fields of a classic theme unless the user asked for that.
theme_.populateColor(*config_.useAccentColor ? accentColor_
: std::nullopt);
}
monet::Scheme M3UI::monetScheme() const {
// Nothing is ever started from here: a slow program would stall the input
// method. The portal is read through the connection Fcitx5 has, the other
// sources only read files.
monet::Context ctx = monet::Context::system();
ctx.run = [](const std::vector<std::string> &, int) {
return std::optional<std::string>();
};
monet::Source portal{
"fcitx-portal", [this](const monet::Context &) {
monet::Appearance a;
if (accentColor_) {
auto q = [](float c) {
return static_cast<std::uint8_t>(std::lround(c * 255.0f));
};
a.accent = monet::Rgb{q(accentColor_->redF()),
q(accentColor_->greenF()),
q(accentColor_->blueF())};
}
if (portalSchemeKnown_) {
a.mode = isDark_ ? monet::Mode::Dark : monet::Mode::Light;
}
return a;
}};
// MONET_SEED and MONET_MODE first, then what the portal said, then the rest.
std::vector<monet::Source> sources;
for (auto &source : monet::defaultSources(ctx)) {
sources.push_back(std::move(source));
if (sources.back().name == "override") {
sources.push_back(portal);
}
}
const monet::Appearance a = monet::detect(ctx, sources);
CLASSICUI_DEBUG() << "Monet seed " << monet::hex(*a.accent) << " from "
<< a.accentSource << ", "
<< (*a.mode == monet::Mode::Dark ? "dark" : "light")
<< " from " << a.modeSource;
return monet::generate(*a.accent, *a.mode);
}
void M3UI::suspend() {
+9
View File
@@ -201,6 +201,11 @@ FCITX_CONFIGURATION(
"font DPI. If the value is 0, it means this option is "
"disabled.")}};
Option<bool> useMonet{
this, "UseMonet",
_("Take the colours from the desktop: its accent colour and its light "
"or dark setting"),
true};
Option<bool> animations{this, "Animations", _("Enable animations"), true};
Option<bool> bounce{this, "Bounce",
_("Bouncy overshoot (off: critically damped)"), true};
@@ -244,6 +249,8 @@ public:
FCITX_ADDON_DEPENDENCY_LOADER(dbus, instance_->addonManager());
Instance *instance() const { return instance_; }
const Configuration *getConfig() const override;
// The scheme for the current desktop colours, see classicui.cpp.
monet::Scheme monetScheme() const;
void setConfig(const RawConfig &config) override {
config_.load(config, true);
safeSaveAsIni(config_, "conf/m3ui.conf");
@@ -330,6 +337,8 @@ private:
mutable Theme subconfigTheme_;
bool suspended_ = true;
bool isDark_ = false;
// The portal has answered, so isDark_ is a value and not just the default.
bool portalSchemeKnown_ = false;
std::optional<Color> accentColor_ = std::nullopt;
std::unique_ptr<EventSource> deferedEnableTray_;
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <monet/monet.h>
#include "fcitx-utils/color.h"
namespace fcitx::m3ui {
// The colours of the candidate window that follow the desktop, taken from a
// libmonet scheme. Only colours are decided here, the radii and the spacing stay
// with the theme file.
struct MonetColors {
Color surface; // the panel
Color outline; // its border
Color highlightFill; // the pill behind the current candidate
Color normal; // candidate text and the typed pinyin
Color highlight; // the part of the typed pinyin being converted
Color highlightBackground;
Color highlightCandidate; // text on the pill
Color candidateLabel; // the number in front of a candidate
Color highlightCandidateLabel;
Color candidateComment;
Color highlightCandidateComment;
};
inline Color toColor(monet::Rgb c, int alpha = 255) {
return Color(c.r, c.g, c.b, static_cast<unsigned short>(alpha));
}
inline MonetColors mapScheme(const monet::Scheme &s) {
MonetColors c;
c.surface = toColor(s.surfaceContainer, 245);
c.outline = toColor(s.outlineVariant);
c.highlightFill = toColor(s.primaryContainer);
c.normal = toColor(s.onSurface);
c.highlight = toColor(s.primary);
c.highlightBackground = toColor(s.primaryContainer);
c.highlightCandidate = toColor(s.onPrimaryContainer);
c.candidateLabel = toColor(s.onSurfaceVariant);
c.highlightCandidateLabel = toColor(s.onPrimaryContainer);
c.candidateComment = toColor(s.onSurfaceVariant);
c.highlightCandidateComment = toColor(s.onPrimaryContainer);
return c;
}
} // namespace fcitx::m3ui
+19
View File
@@ -1180,6 +1180,25 @@ std::vector<Rect> Theme::mask(const BackgroundImageConfig &cfg, int width,
return result;
}
void Theme::applyMonet(const MonetColors &c) {
// A classic theme draws its panel from images, there is nothing to recolour.
if (inputPanel->surfaceColor->alpha() == 0) {
return;
}
auto *panel = inputPanel.mutableValue();
panel->surfaceColor.setValue(c.surface);
panel->outlineColor.setValue(c.outline);
panel->highlightFillColor.setValue(c.highlightFill);
panel->normalColor.setValue(c.normal);
panel->highlightColor.setValue(c.highlight);
panel->highlightBackgroundColor.setValue(c.highlightBackground);
panel->highlightCandidateColor.setValue(c.highlightCandidate);
panel->candidateLabelColor.setValue(c.candidateLabel);
panel->highlightCandidateLabelColor.setValue(c.highlightCandidateLabel);
panel->candidateCommentColor.setValue(c.candidateComment);
panel->highlightCandidateCommentColor.setValue(c.highlightCandidateComment);
}
void Theme::populateColor(std::optional<Color> accent) {
inputPanelBackground_ = *inputPanel->background->color;
inputPanelBorder_ = *inputPanel->background->borderColor;
+5
View File
@@ -30,6 +30,7 @@
#include "fcitx-utils/rect.h"
#include "fcitx/icontheme.h"
#include "common.h"
#include "monettheme.h"
namespace fcitx::m3ui {
enum class Gravity {
@@ -364,6 +365,10 @@ public:
const auto &accentColorFields() const { return accentColorFields_; }
void populateColor(std::optional<Color> accent);
// Replaces the colours of a Material style theme (one with a SurfaceColor)
// with colours that follow the desktop. Other themes are left as they are.
// Call it after load() and before populateColor().
void applyMonet(const MonetColors &colors);
const auto &inputPanelBackground() const { return inputPanelBackground_; }
const auto &inputPanelBorder() const { return inputPanelBorder_; }
+62
View File
@@ -0,0 +1,62 @@
// The candidate window colours taken from a libmonet scheme must stay readable
// for any seed, in both modes.
// g++ -std=c++20 -Isrc -I<libmonet>/include tests/monettheme_test.cpp
// (add the flags from: pkg-config --cflags --libs Fcitx5Utils)
#include <cmath>
#include <cstdio>
#include "monettheme.h"
using namespace fcitx::m3ui;
static int failures = 0;
#define CHECK_MSG(cond, ...) \
do { \
if (!(cond)) { \
std::printf("FAIL line %d: %s ", __LINE__, #cond); \
std::printf(__VA_ARGS__); \
std::printf("\n"); \
failures++; \
} \
} while (0)
static monet::Rgb rgb(const fcitx::Color &c) {
return {static_cast<std::uint8_t>(std::lround(c.redF() * 255)), static_cast<std::uint8_t>(std::lround(c.greenF() * 255)),
static_cast<std::uint8_t>(std::lround(c.blueF() * 255))};
}
int main() {
unsigned state = 4242;
auto next = [&] { state = state * 1664525u + 1013904223u; return static_cast<std::uint8_t>(state >> 24); };
std::vector<monet::Rgb> seeds = {{61, 174, 233}, {255, 176, 201}, {0, 0, 0}, {255, 255, 255}, {128, 128, 128},
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {0x67, 0x50, 0xA4}};
for (int i = 0; i < 300; i++) seeds.push_back({next(), next(), next()});
for (auto seed : seeds) {
for (auto mode : {monet::Mode::Light, monet::Mode::Dark}) {
const MonetColors c = mapScheme(monet::generate(seed, mode));
const char *m = mode == monet::Mode::Dark ? "dark" : "light";
const auto surface = rgb(c.surface), fill = rgb(c.highlightFill);
auto pair = [&](const char *name, monet::Rgb bg, const fcitx::Color &fg, double min) {
CHECK_MSG(monet::contrast(bg, rgb(fg)) >= min, "%s %s, seed %s: %.2f < %.1f", m, name, monet::hex(seed).c_str(),
monet::contrast(bg, rgb(fg)), min);
};
pair("candidate text", surface, c.normal, 7.0);
pair("candidate number", surface, c.candidateLabel, 4.5);
pair("candidate comment", surface, c.candidateComment, 4.5);
pair("typed pinyin being converted", surface, c.highlight, 4.5);
pair("text on the current candidate", fill, c.highlightCandidate, 7.0);
pair("number on the current candidate", fill, c.highlightCandidateLabel, 7.0);
pair("comment on the current candidate", fill, c.highlightCandidateComment, 7.0);
// the panel border can be seen against the panel
CHECK_MSG(monet::contrast(surface, rgb(c.outline)) >= 1.1, "%s border of seed %s", m, monet::hex(seed).c_str());
// the pill can be told apart from the panel, by tone since a container is only a few tones off the surface
CHECK_MSG(std::fabs(monet::toneOf(fill) - monet::toneOf(surface)) >= 3.0, "%s pill of seed %s: tones %.1f and %.1f", m,
monet::hex(seed).c_str(), monet::toneOf(fill), monet::toneOf(surface));
CHECK_MSG(c.surface.alpha() == 245 && c.normal.alpha() == 255 && c.highlightFill.alpha() == 255, "%s alpha of seed %s", m, monet::hex(seed).c_str());
// dark panel in dark mode, light panel in light mode
CHECK_MSG((monet::toneOf(surface) < 50) == (mode == monet::Mode::Dark), "%s panel tone of seed %s", m, monet::hex(seed).c_str());
}
}
std::printf(failures ? "%d check(s) failed\n" : "monettheme: all checks passed\n", failures);
return failures ? 1 : 0;
}