diff --git a/src/shell/bar/widget_factory.cpp b/src/shell/bar/widget_factory.cpp index 450bf4e086..8b0f5eb046 100644 --- a/src/shell/bar/widget_factory.cpp +++ b/src/shell/bar/widget_factory.cpp @@ -646,12 +646,45 @@ std::unique_ptr WidgetFactory::create( } else if (display == "none") { displayMode = WorkspacesWidget::DisplayMode::None; } + + const std::string colorModeValue = wc != nullptr ? wc->getString("color_mode", "state") : std::string("state"); + + WorkspacesWidget::ColorMode colorMode = WorkspacesWidget::ColorMode::State; + + if (colorModeValue == "cycle") { + colorMode = WorkspacesWidget::ColorMode::Cycle; + } else if (colorModeValue != "state") { + kLog.warn("invalid widget.{}.color_mode '{}'; expected state or cycle", name, colorModeValue); + } + + std::vector cycleColors; + + if (wc != nullptr) { + const auto configuredColors = wc->getStringList("cycle_colors"); + + cycleColors.reserve(configuredColors.size()); + + for (std::size_t i = 0; i < configuredColors.size(); ++i) { + cycleColors.push_back(colorSpecFromConfigString( + configuredColors[i], "widget." + name + ".cycle_colors[" + std::to_string(i) + "]" + )); + } + } + + if (colorMode == WorkspacesWidget::ColorMode::Cycle && cycleColors.empty()) { + kLog.warn("widget.{}.color_mode is cycle but cycle_colors is empty; falling back to state", name); + + colorMode = WorkspacesWidget::ColorMode::State; + } + std::size_t maxLabelChars = 1; // Default: truncate names to 1 char (v4 behavior) if (wc != nullptr && wc->hasSetting("max_label_chars")) { maxLabelChars = static_cast(wc->getInt("max_label_chars", 1)); } WorkspacesWidget::Options options{ .displayMode = displayMode, + .colorMode = colorMode, + .cycleColors = std::move(cycleColors), .focusedColor = focusedColor, .occupiedColor = occupiedColor, .emptyColor = emptyColor, @@ -664,7 +697,7 @@ std::unique_ptr WidgetFactory::create( .minimal = wc != nullptr ? wc->getBool("minimal", false) : false, .focusedOutputOnly = wc != nullptr ? wc->getBool("focused_output_only", false) : false, }; - auto widget = std::make_unique(m_platform, output, options); + auto widget = std::make_unique(m_platform, output, std::move(options)); widget->setContentScale(contentScale); return widget; } diff --git a/src/shell/bar/widgets/workspaces_widget.cpp b/src/shell/bar/widgets/workspaces_widget.cpp index 722ca0e077..c3c3f6a061 100644 --- a/src/shell/bar/widgets/workspaces_widget.cpp +++ b/src/shell/bar/widgets/workspaces_widget.cpp @@ -41,10 +41,10 @@ namespace { } // namespace WorkspacesWidget::WorkspacesWidget(CompositorPlatform& platform, wl_output* output, Options options) - : m_platform(platform), m_output(output), m_displayMode(options.displayMode), - m_maxLabelChars(options.maxLabelChars), m_labelsOnlyWhenOccupied(options.labelsOnlyWhenOccupied), - m_hideWhenEmpty(options.hideWhenEmpty), m_pillScale(options.pillScale), - m_activePillSize(std::clamp(options.activePillSize, 0.25f, 8.0f)), + : m_platform(platform), m_output(output), m_displayMode(options.displayMode), m_colorMode(options.colorMode), + m_cycleColors(std::move(options.cycleColors)), m_maxLabelChars(options.maxLabelChars), + m_labelsOnlyWhenOccupied(options.labelsOnlyWhenOccupied), m_hideWhenEmpty(options.hideWhenEmpty), + m_pillScale(options.pillScale), m_activePillSize(std::clamp(options.activePillSize, 0.25f, 8.0f)), m_inactivePillSize(std::clamp(options.inactivePillSize, 0.25f, 8.0f)), m_minimal(options.minimal), m_focusedOutputOnly(options.focusedOutputOnly), m_focusedColor(options.focusedColor), m_occupiedColor(options.occupiedColor), m_emptyColor(options.emptyColor) {} @@ -708,21 +708,48 @@ std::optional WorkspacesWidget::numericWorkspaceId(const Workspace& return std::nullopt; } +ColorSpec WorkspacesWidget::workspaceCycleColor(const Workspace& workspace) const { + if (m_cycleColors.empty()) { + return m_emptyColor; + } + + std::size_t workspaceNumber = workspace.index; + + if (workspaceNumber == 0) { + workspaceNumber = numericWorkspaceId(workspace).value_or(1); + } + + const std::size_t colorIndex = (workspaceNumber - 1) % m_cycleColors.size(); + return m_cycleColors[colorIndex]; +} + bool WorkspacesWidget::isFocusedOutput() const { return m_platform.preferredInteractiveOutput() == m_output; } ColorSpec WorkspacesWidget::workspaceFillColor(const Workspace& workspace) const { if (workspace.active) { + if (m_colorMode == ColorMode::Cycle && !m_cycleColors.empty()) { + return workspaceCycleColor(workspace); + } + if (m_activeUsesFocusedColor) { return m_focusedColor; } + return m_occupiedColor; } + if (workspace.urgent) { return colorSpecFromRole(ColorRole::Error); } + if (workspace.occupied) { + if (m_colorMode == ColorMode::Cycle && !m_cycleColors.empty()) { + return workspaceCycleColor(workspace); + } + return m_occupiedColor; } + ColorSpec color = m_emptyColor; color.alpha *= 0.55f; return color; @@ -732,18 +759,31 @@ ColorSpec WorkspacesWidget::workspaceTextColor(const Workspace& workspace) const if (workspace.urgent) { return m_minimal ? colorSpecFromRole(ColorRole::Error) : colorSpecFromRole(ColorRole::OnError); } + if (!m_minimal) { return readableColorForFill(workspaceFillColor(workspace)); } + if (workspace.active) { + if (m_colorMode == ColorMode::Cycle && !m_cycleColors.empty()) { + return workspaceCycleColor(workspace); + } + if (m_activeUsesFocusedColor) { return m_focusedColor; } + return m_occupiedColor; } + if (workspace.occupied) { + if (m_colorMode == ColorMode::Cycle && !m_cycleColors.empty()) { + return workspaceCycleColor(workspace); + } + return m_occupiedColor; } + ColorSpec color = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurfaceVariant)); color.alpha *= 0.55f; return color; diff --git a/src/shell/bar/widgets/workspaces_widget.h b/src/shell/bar/widgets/workspaces_widget.h index 555381d474..c6d714be86 100644 --- a/src/shell/bar/widgets/workspaces_widget.h +++ b/src/shell/bar/widgets/workspaces_widget.h @@ -23,8 +23,15 @@ class WorkspacesWidget : public Widget { Name, }; + enum class ColorMode : std::uint8_t { + State, + Cycle, + }; + struct Options { DisplayMode displayMode = DisplayMode::Id; + ColorMode colorMode = ColorMode::State; + std::vector cycleColors; ColorSpec focusedColor = colorSpecFromRole(ColorRole::Primary); ColorSpec occupiedColor = colorSpecFromRole(ColorRole::Secondary); ColorSpec emptyColor = colorSpecFromRole(ColorRole::Secondary); @@ -86,6 +93,7 @@ class WorkspacesWidget : public Widget { }; [[nodiscard]] ColorSpec workspaceFillColor(const Workspace& workspace) const; + [[nodiscard]] ColorSpec workspaceCycleColor(const Workspace& workspace) const; [[nodiscard]] ColorSpec workspaceTextColor(const Workspace& workspace) const; [[nodiscard]] static ColorRole onRoleForFill(ColorRole fill); [[nodiscard]] static ColorSpec readableColorForFill(const ColorSpec& fill); @@ -94,6 +102,8 @@ class WorkspacesWidget : public Widget { CompositorPlatform& m_platform; wl_output* m_output = nullptr; DisplayMode m_displayMode = DisplayMode::None; + ColorMode m_colorMode = ColorMode::State; + std::vector m_cycleColors; std::size_t m_maxLabelChars = 1; bool m_labelsOnlyWhenOccupied = false; bool m_hideWhenEmpty = false; diff --git a/src/shell/settings/widget_settings_registry.cpp b/src/shell/settings/widget_settings_registry.cpp index fd8253a4f6..2de7dd4b6b 100644 --- a/src/shell/settings/widget_settings_registry.cpp +++ b/src/shell/settings/widget_settings_registry.cpp @@ -948,6 +948,14 @@ namespace settings { add(std::move(focusedOutputOnly)); } add(segmentedSpec("display", "id", workspaceDisplay)); + { + auto colorMode = stringSpec("color_mode", "state", true); + add(std::move(colorMode)); + } + { + auto cycleColors = stringListSpec("cycle_colors", {}, true); + add(std::move(cycleColors)); + } { auto labelsOnlyWhenOccupied = boolSpec("labels_only_when_occupied", false); labelsOnlyWhenOccupied.descriptionKey =