diff --git a/include/bar.hpp b/include/bar.hpp index 2dc513799..8f0fc04b7 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -76,6 +76,7 @@ class Bar : public sigc::trackable { void handleSignal(int); util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr2Action(); + int getAvailableWidthForLeftModule(const AModule* module) const; void toggleSuspend(bool suspend); diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 8b902f03e..45889af9f 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include +#include #include "AModule.hpp" #include "bar.hpp" @@ -41,6 +43,10 @@ class Workspaces : public AModule, public sigc::trackable { void updateWindows(const Json::Value&, std::string&); Gtk::Button& addButton(const Json::Value&); void onButtonReady(const Json::Value&, Gtk::Button&); + void onBarSizeAllocate(Gtk::Allocation& /*allocation*/); + void updateOverflow(); + void rebuildOverflowMenu(); + void switchToWorkspace(const Json::Value&); std::string getIcon(const std::string&, const Json::Value&); std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; @@ -53,12 +59,15 @@ class Workspaces : public AModule, public sigc::trackable { std::vector high_priority_named_; std::vector workspaces_order_; Gtk::Box box_; + Gtk::Button overflow_button_{"…"}; + Gtk::Menu overflow_menu_; std::string m_formatWindowSeparator; std::vector m_ignoreWorkspaces; util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; std::unordered_map custom_sort_priorities_; + std::unordered_set overflowed_; std::mutex mutex_; Ipc ipc_; }; diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 7a1bd4d5d..bb2eec649 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -135,6 +135,12 @@ warp-on-scroll: ++ Keys are output names and values are class names like *${output}: {output-class}*. Assignment in config is used to keep the stylesheet independent of the available outputs. +*overflow*: ++ + typeof: bool ++ + default: true ++ + Hides workspace buttons that would displace modules on the right side of the + bar. Hidden workspaces are available from an ellipsis button. The focused + workspace is kept visible. # FORMAT REPLACEMENTS diff --git a/src/bar.cpp b/src/bar.cpp index ec3189c6b..8285f8806 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -567,6 +567,28 @@ void waybar::Bar::handleSignal(int signal) { waybar::util::KillSignalAction waybar::Bar::getOnSigusr1Action() { return this->onSigusr1; } waybar::util::KillSignalAction waybar::Bar::getOnSigusr2Action() { return this->onSigusr2; } +int waybar::Bar::getAvailableWidthForLeftModule(const AModule* target) const { + Gdk::Rectangle geometry; + output->monitor->get_geometry(geometry); + + int minimum = 0; + int right_natural = 0; + right_.get_preferred_width(minimum, right_natural); + + int sibling_width = 0; + for (const auto& module : modules_left_) { + if (module.get() == target) { + continue; + } + int natural = 0; + static_cast(*module).get_preferred_width(minimum, natural); + sibling_width += natural; + } + + const int gaps = std::max(0, static_cast(modules_left_.size()) - 1) * left_.get_spacing(); + return std::max(1, geometry.get_width() - right_natural - sibling_width - gaps); +} + void waybar::Bar::getModules(const Factory& factory, const std::string& pos, waybar::Group* group = nullptr) { auto module_list = group != nullptr ? config[pos]["modules"] : config[pos]; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 86902ef59..a92529dbe 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -71,6 +71,25 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& } box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); + overflow_button_.set_name("sway-workspaces-overflow"); + overflow_button_.set_relief(Gtk::RELIEF_NONE); + overflow_button_.get_style_context()->add_class("overflow"); + overflow_button_.signal_clicked().connect([this] { + { + std::lock_guard lock(mutex_); + rebuildOverflowMenu(); + } + const auto widget_anchor = + bar_.position == Gtk::POS_BOTTOM ? GDK_GRAVITY_NORTH_EAST : GDK_GRAVITY_SOUTH_EAST; + const auto menu_anchor = + bar_.position == Gtk::POS_BOTTOM ? GDK_GRAVITY_SOUTH_EAST : GDK_GRAVITY_NORTH_EAST; + gtk_menu_popup_at_widget(overflow_menu_.gobj(), GTK_WIDGET(overflow_button_.gobj()), + widget_anchor, menu_anchor, nullptr); + }); + box_.pack_end(overflow_button_, false, false, 0); + overflow_button_.hide(); + const_cast(bar_).window.signal_size_allocate().connect( + sigc::mem_fun(*this, &Workspaces::onBarSizeAllocate)); if (config_["format-window-separator"].isString()) { m_formatWindowSeparator = config_["format-window-separator"].asString(); } else { @@ -447,6 +466,7 @@ auto Workspaces::update() -> void { } onButtonReady(*it, button); } + updateOverflow(); // Call parent update AModule::update(); } @@ -458,37 +478,131 @@ Gtk::Button& Workspaces::addButton(const Json::Value& node) { button.set_name("sway-workspace-" + node["name"].asString()); button.set_relief(Gtk::RELIEF_NONE); if (!config_["disable-click"].asBool()) { - button.signal_pressed().connect([this, node] { - try { - if (node["target_output"].isString()) { - ipc_.sendCmd(IPC_COMMAND, - fmt::format(persistent_workspace_switch_cmd_, "--no-auto-back-and-forth", - node["name"].asString(), node["target_output"].asString(), - "--no-auto-back-and-forth", node["name"].asString())); - } else { - std::string flag = - config_["disable-auto-back-and-forth"].asBool() ? "--no-auto-back-and-forth" : ""; - if (config_["no-switch-output"].asBool()) { - ipc_.sendCmd(IPC_COMMAND, - fmt::format("[workspace=\"^{}$\"] move workspace to output current; " - "workspace number {} \"{}\"", - node["name"].asString(), flag, node["name"].asString())); - } else if (node["num"].asInt() >= 0) { - ipc_.sendCmd(IPC_COMMAND, - fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt())); - } else { - ipc_.sendCmd(IPC_COMMAND, - fmt::format(workspace_switch_cmd_, flag, node["name"].asString())); - } - } - } catch (const std::exception& e) { - spdlog::error("Workspaces: {}", e.what()); - } - }); + button.signal_pressed().connect([this, node] { switchToWorkspace(node); }); } return button; } +void Workspaces::switchToWorkspace(const Json::Value& node) { + try { + if (node["target_output"].isString()) { + ipc_.sendCmd(IPC_COMMAND, + fmt::format(persistent_workspace_switch_cmd_, "--no-auto-back-and-forth", + node["name"].asString(), node["target_output"].asString(), + "--no-auto-back-and-forth", node["name"].asString())); + } else { + std::string flag = + config_["disable-auto-back-and-forth"].asBool() ? "--no-auto-back-and-forth" : ""; + if (config_["no-switch-output"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, + fmt::format("[workspace=\"^{}$\"] move workspace to output current; " + "workspace number {} \"{}\"", + node["name"].asString(), flag, node["name"].asString())); + } else if (node["num"].asInt() >= 0) { + ipc_.sendCmd(IPC_COMMAND, + fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt())); + } else { + ipc_.sendCmd(IPC_COMMAND, + fmt::format(workspace_switch_cmd_, flag, node["name"].asString())); + } + } + } catch (const std::exception& e) { + spdlog::error("Workspaces: {}", e.what()); + } +} + +void Workspaces::rebuildOverflowMenu() { + for (auto* child : overflow_menu_.get_children()) { + overflow_menu_.remove(*child); + } + + for (const auto& workspace : workspaces_) { + if (overflowed_.count(workspace["name"].asString()) == 0) { + continue; + } + auto* item = Gtk::manage(new Gtk::MenuItem(workspace["name"].asString())); + item->signal_activate().connect([this, workspace] { switchToWorkspace(workspace); }); + overflow_menu_.append(*item); + } + overflow_menu_.show_all(); +} + +void Workspaces::onBarSizeAllocate(Gtk::Allocation& /*allocation*/) { + std::lock_guard lock(mutex_); + updateOverflow(); +} + +void Workspaces::updateOverflow() { + for (const auto& name : overflowed_) { + auto button = buttons_.find(name); + if (button != buttons_.end()) { + button->second.show(); + } + } + overflowed_.clear(); + + if (config_["overflow"].isBool() && !config_["overflow"].asBool()) { + overflow_button_.hide(); + return; + } + + const int max_width = bar_.getAvailableWidthForLeftModule(this); + + int overflow_min = 0; + int overflow_natural = 0; + overflow_button_.get_preferred_width(overflow_min, overflow_natural); + + int used = 0; + int total = 0; + std::vector> shown; + for (const auto& workspace : workspaces_) { + auto it = buttons_.find(workspace["name"].asString()); + if (it == buttons_.end()) { + continue; + } + auto& button = it->second; + if (!button.get_visible()) { + continue; + } + int minimum = 0; + int natural = 0; + button.get_preferred_width(minimum, natural); + total += natural; + } + + if (total <= max_width) { + overflow_button_.hide(); + return; + } + + for (const auto& workspace : workspaces_) { + auto it = buttons_.find(workspace["name"].asString()); + if (it == buttons_.end() || !it->second.get_visible()) { + continue; + } + auto& button = it->second; + int minimum = 0; + int natural = 0; + button.get_preferred_width(minimum, natural); + const bool focused = hasFlag(workspace, "focused"); + while (focused && used + natural + overflow_natural > max_width && !shown.empty()) { + const auto& [name, width] = shown.back(); + buttons_.at(name).hide(); + overflowed_.insert(name); + used -= width; + shown.pop_back(); + } + if (!focused && used + natural + overflow_natural > max_width) { + button.hide(); + overflowed_.insert(workspace["name"].asString()); + } else { + used += natural; + shown.emplace_back(workspace["name"].asString(), natural); + } + } + overflow_button_.show(); +} + std::string Workspaces::getIcon(const std::string& name, const Json::Value& node) { std::vector keys = {"high-priority-named", "urgent", "focused", name, "default"}; for (auto const& key : keys) {