diff --git a/src/tree.cc b/src/tree.cc index a2b1abe..349739d 100644 --- a/src/tree.cc +++ b/src/tree.cc @@ -41,6 +41,10 @@ vector Tree::GetLeaves() const { return root_node_->GetLeaves(); } +void Tree::Normalize() { + root_node_->Normalize(); +} + Tree::Node* Tree::root_node() const { return root_node_.get(); } @@ -169,7 +173,7 @@ void Tree::DfsSerializeHelper(Tree::Node* node, string& data) const { } Tree::Node::Node(unique_ptr client) - : client_(std::move(client)), tiling_direction_(TilingDirection::UNSPECIFIED) { + : parent_(), client_(std::move(client)), tiling_direction_(TilingDirection::UNSPECIFIED) { if (client_) { Tree::Node::mapper_[client_.get()] = this; } @@ -184,21 +188,81 @@ void Tree::Node::AddChild(unique_ptr child) { children_.push_back(std::move(child)); } -void Tree::Node::RemoveChild(Tree::Node* child) { +unique_ptr Tree::Node::RemoveChild(Tree::Node* child) { + unique_ptr removed_ptr = nullptr; + child->set_parent(nullptr); - children_.erase( - std::remove_if(children_.begin(), children_.end(), - [&](unique_ptr& node) { return node.get() == child; }), - children_.end()); + children_.erase(std::remove_if(children_.begin(), children_.end(), + [&](unique_ptr& node) { + return node.get() == child && + (removed_ptr = std::move(node), true); + }), + children_.end()); + return removed_ptr; } void Tree::Node::InsertChildAfter(unique_ptr child, Tree::Node* ref) { + InsertChildBeside(std::move(child), ref, TilingPosition::AFTER); +} + +void Tree::Node::InsertChildBeside(unique_ptr child, Tree::Node* ref, + TilingPosition tiling_position) { child->set_parent(this); ptrdiff_t ref_idx = std::find_if(children_.begin(), children_.end(), [&](unique_ptr& node) { return node.get() == ref; }) - children_.begin(); - children_.insert(children_.begin() + ref_idx + 1, std::move(child)); + ptrdiff_t position = tiling_position == TilingPosition::BEFORE ? 0 : 1; + children_.insert(children_.begin() + ref_idx + position, std::move(child)); +} + +// Insert a child node, but every current child of this node will be the child of the inserted +// node. +void Tree::Node::InsertChildAboveChildren(unique_ptr child) { + for (const auto current_child : children()) { + unique_ptr current_child_ptr = RemoveChild(current_child); + child->AddChild(std::move(current_child_ptr)); + } + + AddChild(std::move(child)); +} + +// Insert a node as a parent while lifting the current parent up to the grandparent. +void Tree::Node::InsertParent(std::unique_ptr parent) { + Tree::Node* parent_raw = parent.get(); + + parent_->InsertChildAfter(std::move(parent), this); + unique_ptr this_ptr = parent_->RemoveChild(this); + parent_raw->AddChild(std::move(this_ptr)); +} + +void Tree::Node::Swap(Tree::Node* destination) { + if (!leaf() || !destination->leaf() || !parent_ || !destination->parent_) { + return; + } + + unique_ptr& this_ptr = owning_pointer_(); + unique_ptr& dest_ptr = destination->owning_pointer_(); + + this_ptr.swap(dest_ptr); + std::swap(parent_, destination->parent_); +} + +// Delete redundant internal nodes below this node. +void Tree::Node::Normalize() { + for (auto& child : children()) { + if (child->children_.size() == 1) { + unique_ptr grandchild = child->RemoveChild(child->children_.front().get()); + Tree::Node* grandchild_raw = grandchild.get(); + + InsertChildAfter(std::move(grandchild), child); + RemoveChild(child); + + grandchild_raw->Normalize(); + } else { + child->Normalize(); + } + } } Tree::Node* Tree::Node::GetLeftSibling() const { @@ -303,4 +367,9 @@ bool Tree::Node::leaf() const { return children_.empty(); } +unique_ptr& Tree::Node::owning_pointer_() const { + return *std::find_if(parent_->children_.begin(), parent_->children_.end(), + [&](unique_ptr& node) { return node.get() == this; }); +} + } // namespace wmderland diff --git a/src/tree.h b/src/tree.h index dbeaa69..244bd52 100644 --- a/src/tree.h +++ b/src/tree.h @@ -16,6 +16,11 @@ enum class TilingDirection { VERTICAL, }; +enum class TilingPosition { + BEFORE, + AFTER, +}; + class Tree { public: Tree(); @@ -27,8 +32,15 @@ class Tree { virtual ~Node(); void AddChild(std::unique_ptr child); - void RemoveChild(Tree::Node* child); + std::unique_ptr RemoveChild(Tree::Node* child); void InsertChildAfter(std::unique_ptr child, Tree::Node* ref); + void InsertChildBeside(std::unique_ptr child, Tree::Node* ref, + TilingPosition tiling_position); + void InsertChildAboveChildren(std::unique_ptr child); + void InsertParent(std::unique_ptr parent); + void Swap(Tree::Node* destination); + + void Normalize(); Tree::Node* GetLeftSibling() const; Tree::Node* GetRightSibling() const; @@ -50,6 +62,8 @@ class Tree { static std::unordered_map mapper_; private: + std::unique_ptr& owning_pointer_() const; + std::vector> children_; Tree::Node* parent_; @@ -60,6 +74,8 @@ class Tree { Tree::Node* GetTreeNode(Client* client) const; std::vector GetLeaves() const; + void Normalize(); + Tree::Node* root_node() const; Tree::Node* current_node() const; void set_current_node(Tree::Node* node); diff --git a/src/window_manager.cc b/src/window_manager.cc index 35ae84e..952aa4f 100644 --- a/src/window_manager.cc +++ b/src/window_manager.cc @@ -9,6 +9,7 @@ extern "C" { #include #include #include +#include #include "client.h" #include "log.h" @@ -47,6 +48,8 @@ extern "C" { } while (0) using std::pair; +using std::tuple; +using std::vector; namespace wmderland { @@ -407,25 +410,40 @@ void WindowManager::OnButtonPress(const XButtonEvent& e) { c->workspace()->SetFocusedClient(c->window()); c->workspace()->RaiseAllFloatingClients(); - if (c->is_floating() && !c->is_fullscreen()) { - c->workspace()->DisableFocusFollowsMouse(); + if (c->is_fullscreen()) { + return; + } + + if (c->is_floating()) { c->Raise(); c->set_attr_cache(c->GetXWindowAttributes()); - - mouse_->btn_pressed_event_ = e; - mouse_->SetCursor(static_cast(e.button)); + } else if (e.button != Mouse::Button::LEFT) { + return; } + + c->workspace()->DisableFocusFollowsMouse(); + mouse_->btn_pressed_event_ = e; + mouse_->SetCursor(static_cast(e.button)); } -void WindowManager::OnButtonRelease(const XButtonEvent&) { +void WindowManager::OnButtonRelease(const XButtonEvent& e) { Client* c = nullptr; GET_CLIENT_OR_RETURN(mouse_->btn_pressed_event_.subwindow, c); - assert(c->is_floating() && !c->is_fullscreen()); + assert(!c->is_fullscreen()); c->workspace()->EnableFocusFollowsMouse(); - XWindowAttributes attr = wm_utils::GetXWindowAttributes(mouse_->btn_pressed_event_.subwindow); - cookie_.Put(c->window(), {attr.x, attr.y, attr.width, attr.height}); + if (c->is_floating()) { + XWindowAttributes attr = wm_utils::GetXWindowAttributes(mouse_->btn_pressed_event_.subwindow); + cookie_.Put(c->window(), {attr.x, attr.y, attr.width, attr.height}); + } else { + tuple drop_location = + GetDropLocation(e); + + MoveWindow(c->window(), std::get(drop_location), std::get(drop_location), + std::get(drop_location), + std::get(drop_location)); + } mouse_->btn_pressed_event_.subwindow = None; mouse_->SetCursor(Mouse::CursorType::NORMAL); @@ -435,6 +453,10 @@ void WindowManager::OnMotionNotify(const XButtonEvent& e) { Client* c = nullptr; GET_CLIENT_OR_RETURN(mouse_->btn_pressed_event_.subwindow, c); + if (!c->is_floating()) { + return; + } + const XWindowAttributes& attr = c->attr_cache(); const XButtonEvent& btn_pressed_event = mouse_->btn_pressed_event_; @@ -556,14 +578,17 @@ void WindowManager::Unmanage(Window window) { Client* c = nullptr; GET_CLIENT_OR_RETURN(window, c); + Workspace* workspace = c->workspace(); + // If the client being destroyed is in fullscreen mode, make sure to unset the // workspace's fullscreen state. if (c->is_fullscreen()) { - c->workspace()->set_fullscreen(false); + workspace->set_fullscreen(false); } // Remove the corresponding client from the client tree. - c->workspace()->Remove(window); + workspace->Remove(window); + workspace->Normalize(); UpdateClientList(); ArrangeWindows(); } @@ -680,6 +705,48 @@ void WindowManager::MoveWindowToWorkspace(Window window, int next) { ArrangeWindows(); } +void WindowManager::MoveWindow(Window window, Window ref, AreaType area_type, + TilingDirection tiling_direction, + TilingPosition tiling_position) { + Client* c = nullptr; + Client* ref_client = nullptr; + GET_CLIENT_OR_RETURN(window, c); + GET_CLIENT_OR_RETURN(ref, ref_client); + + // Suppress the warning for the unused variable 'c'. + (void)c; + + if (area_type == AreaType::CENTER || ref_client->is_floating()) { + SwapWindows(window, ref); + return; + } + + workspaces_[current_]->Move(window, ref, area_type, tiling_direction, tiling_position); + + ArrangeWindows(); +} + +void WindowManager::SwapWindows(Window window0, Window window1) { + Client* c0 = nullptr; + Client* c1 = nullptr; + GET_CLIENT_OR_RETURN(window0, c0); + GET_CLIENT_OR_RETURN(window1, c1); + + workspaces_[current_]->Swap(window0, window1); + + // Also swap the coordinates of the windows. (Apply it if the window is floating.) + XWindowAttributes attr0 = c0->GetXWindowAttributes(); + if (c0->is_floating()) { + XWindowAttributes attr = c1->GetXWindowAttributes(); + c0->MoveResize(attr.x, attr.y, attr.width, attr.height); + } + if (c1->is_floating()) { + c1->MoveResize(attr0.x, attr0.y, attr0.width, attr0.height); + } + + ArrangeWindows(); +} + void WindowManager::SetFloating(Window window, bool floating, bool use_default_size) { Client* c = nullptr; GET_CLIENT_OR_RETURN(window, c); @@ -855,6 +922,76 @@ Client::Area WindowManager::GetFloatingWindowArea(Window window, bool use_defaul return area; } +// Detect where the mouse button was released. +tuple WindowManager::GetDropLocation( + const XButtonEvent& e) const { + Client* c = nullptr; + auto it = Client::mapper_.find(e.subwindow); + + if (it != Client::mapper_.end()) { + c = it->second; + } else { // If the point dropped at is not on a window, try to find the window which area + // contains the point. + int pad_lt = 1 + config_->gap_width() / 2; + int pad_rb = pad_lt + 2 * config_->border_width(); + vector clients = workspaces_[current_]->GetClients(); + auto it = std::find_if(clients.begin(), clients.end(), [&](Client* client) { + if (client->is_floating()) { + return false; + } + + XWindowAttributes attr = client->GetXWindowAttributes(); + + return e.x >= attr.x - pad_lt && e.x <= attr.x + attr.width + pad_rb && + e.y >= attr.y - pad_lt && e.y <= attr.y + attr.height + pad_rb; + }); + + if (it != clients.end()) { + c = *it; + } else { + return std::make_tuple(None, AreaType::UNDEFINED, TilingDirection::UNSPECIFIED, + TilingPosition::AFTER); + } + } + + XWindowAttributes attr = c->GetXWindowAttributes(); + + int width = attr.width + 2 * config_->border_width(); + int height = attr.height + 2 * config_->border_width(); + int x = e.x - attr.x - 0.5 * width; + int y = e.y - attr.y - 0.5 * height; + double r = x != 0 ? (double)y / x : 0.; + double r_diagonal = width != 0 ? (double)height / width : 0.; + + TilingDirection tiling_direction; + // How the point dropped at differ from the center of the window. (Normalized in [-0.5, 0.5]) + double d; + // Value of `d` such that it is 35px away from the end of the window. + double d_edge; + if (x == 0 || r >= r_diagonal || r <= -r_diagonal) { // Dropped in the North or the South + tiling_direction = TilingDirection::VERTICAL; + d = (double)y / height; + d_edge = 0.5 - 35. / height; + } else { // Dropped in the East or the West + tiling_direction = TilingDirection::HORIZONTAL; + d = (double)x / width; + d_edge = 0.5 - 35. / width; + } + + TilingPosition tiling_position = d >= 0. ? TilingPosition::AFTER : TilingPosition::BEFORE; + + AreaType area_type; + if (d >= -0.1 && d <= 0.1) { + area_type = AreaType::CENTER; + } else if (d >= -d_edge && d <= d_edge) { + area_type = AreaType::MID; + } else { + area_type = AreaType::EDGE; + } + + return std::make_tuple(c->window(), area_type, tiling_direction, tiling_position); +} + void WindowManager::UpdateClientList() { XDeleteProperty(dpy_, root_window_, prop_->net[atom::NET_CLIENT_LIST]); diff --git a/src/window_manager.h b/src/window_manager.h index 998dd41..f925c0a 100644 --- a/src/window_manager.h +++ b/src/window_manager.h @@ -9,6 +9,7 @@ extern "C" { } #include #include +#include #include #include "action.h" @@ -65,7 +66,12 @@ class WindowManager { // Workspace manipulation void GotoWorkspace(int next); + + // Window replacement void MoveWindowToWorkspace(Window window, int next); + void MoveWindow(Window window, Window ref, AreaType area_type, + TilingDirection tiling_direction, TilingPosition tiling_position); + void SwapWindows(Window window0, Window window1); // Client manipulation void SetFloating(Window window, bool floating, bool use_default_size); @@ -81,6 +87,8 @@ class WindowManager { std::pair GetDisplayResolution() const; Client::Area GetTilingArea() const; Client::Area GetFloatingWindowArea(Window window, bool use_default_size); + std::tuple GetDropLocation( + const XButtonEvent& e) const; // Misc void UpdateClientList(); diff --git a/src/workspace.cc b/src/workspace.cc index 65a1ce4..f35760d 100644 --- a/src/workspace.cc +++ b/src/workspace.cc @@ -30,7 +30,7 @@ bool Workspace::Has(Window window) const { return GetClient(window) != nullptr; } -void Workspace::Add(Window window) { +void Workspace::Add(Window window, TilingPosition tiling_position) { unique_ptr client = std::make_unique(dpy_, window, this); unique_ptr new_node = std::make_unique(std::move(client)); @@ -42,7 +42,8 @@ void Workspace::Add(Window window) { client_tree_.root_node()->AddChild(std::move(new_node)); } else { Tree::Node* current_node = client_tree_.current_node(); - current_node->parent()->InsertChildAfter(std::move(new_node), current_node); + current_node->parent()->InsertChildBeside(std::move(new_node), current_node, + tiling_position); } if (!is_fullscreen_) { @@ -93,6 +94,14 @@ void Workspace::Remove(Window window) { client_tree_.set_current_node(nodes[idx]); } +// The tree with redundant internal nodes looks the same as the tree without them though they +// result the different behavior on the window insertion. +// Thus, to prevent users hitting inconsistent behaviors, we normalize the tree after removing +// clients. +void Workspace::Normalize() { + client_tree_.Normalize(); +} + void Workspace::Move(Window window, Workspace* new_workspace) { Client* c = GetClient(window); if (!c) { @@ -113,6 +122,178 @@ void Workspace::Move(Window window, Workspace* new_workspace) { c->set_floating(is_floating); c->set_fullscreen(false); c->set_has_unmap_req_from_wm(has_unmap_req_from_wm); + + client_tree_.Normalize(); +} + +void Workspace::Move(Window window, Window ref, AreaType area_type, + TilingDirection tiling_direction, TilingPosition tiling_position) { + Client* c = GetClient(window); + if (!c) { + return; + } + + Client* ref_client = GetClient(ref); + if (!ref_client) { + return; + } + + Tree::Node* ref_node = client_tree_.GetTreeNode(ref_client); + + // We handle four cases here: + // 1. The window was released in the middle area of `ref` window. + // 2. The window was released on the edge of `ref` window and - + // a. - `ref` window is tiled in the same direction as `tiling_direction`. + // b. - `ref` window is NOT tiled in the same direction as `tiling_direction` but its + // parent window is. + // c. - both of `ref` window and its parent are tiled in the different direction from + // `tiling_direction`. + switch (area_type) { + case AreaType::MID: + MoveAndSplit(window, ref, tiling_direction, tiling_position, false); + break; + case AreaType::EDGE: + if (ref_node->parent()->tiling_direction() == tiling_direction) { + MoveAndInsert(window, ref, tiling_position); + } else if (ref_node->parent()->parent() && + ref_node->parent()->parent()->tiling_direction() == tiling_direction) { + MoveAndInsert(window, ref, tiling_position, true); + } else { + MoveAndSplit(window, ref, tiling_direction, tiling_position, true); + } + break; + default: + break; + } + + client_tree_.Normalize(); +} + +void Workspace::MoveAndSplit(Window window, Window ref, TilingDirection tiling_direction, + TilingPosition tiling_position, bool branch_outer) { + if (window == ref) { + return; + } + + Client* c = GetClient(window); + if (!c) { + return; + } + + Client* ref_client = GetClient(ref); + if (!ref_client) { + return; + } + + Tree::Node* ref_node = client_tree_.GetTreeNode(ref_client); + if (!ref_node) { + return; + } + + bool is_floating = c->is_floating(); + bool is_mapped = c->is_mapped(); + bool has_unmap_req_from_wm = c->has_unmap_req_from_wm(); + + Remove(window); + + unique_ptr new_node = std::make_unique(nullptr); + Tree::Node* new_node_raw = new_node.get(); + Tree::Node* original_parent_node = ref_node->parent(); + + // If `branch_outer` is true, wrap the container containing `ref` by new container and put + // `window` as sibling of the wrapped container. + // Otherwise, split the area where `ref` is placed to put `ref` and `window` there. + if (branch_outer) { + original_parent_node->InsertChildAboveChildren(std::move(new_node)); + client_tree_.set_current_node(new_node_raw); + + new_node_raw->set_tiling_direction(original_parent_node->tiling_direction()); + original_parent_node->set_tiling_direction(tiling_direction); + } else { + ref_node->InsertParent(std::move(new_node)); + client_tree_.set_current_node(ref_node); + + new_node_raw->set_tiling_direction(tiling_direction); + } + + Add(window, tiling_position); + + // Transfer old client's state to the new client. + c = ref_client->workspace()->GetClient(window); + c->set_floating(is_floating); + c->set_fullscreen(false); + c->set_mapped(is_mapped); + c->set_has_unmap_req_from_wm(has_unmap_req_from_wm); +} + +void Workspace::MoveAndInsert(Window window, Window ref, TilingPosition tiling_position, + bool insert_outer) { + if (window == ref) { + return; + } + + Client* c = GetClient(window); + if (!c) { + return; + } + + Client* ref_client = GetClient(ref); + if (!ref_client) { + return; + } + + // If `insert_outer` is true, insert `window` beside the container containing `ref`. + // Otherwise, insert `window` beside `ref`. + Tree::Node* ref_node = insert_outer ? client_tree_.GetTreeNode(ref_client)->parent() + : client_tree_.GetTreeNode(ref_client); + if (!ref_node) { + return; + } + + bool is_floating = c->is_floating(); + bool is_mapped = c->is_mapped(); + bool has_unmap_req_from_wm = c->has_unmap_req_from_wm(); + + Remove(window); + + client_tree_.set_current_node(ref_node); + Add(window, tiling_position); + + // Transfer old client's state to the new client. + c = ref_client->workspace()->GetClient(window); + c->set_floating(is_floating); + c->set_fullscreen(false); + c->set_mapped(is_mapped); + c->set_has_unmap_req_from_wm(has_unmap_req_from_wm); +} + +void Workspace::Swap(Window window0, Window window1) { + Client* c0 = GetClient(window0); + if (!c0) { + return; + } + + Tree::Node* node0 = client_tree_.GetTreeNode(c0); + if (!node0) { + return; + } + + Client* c1 = GetClient(window1); + if (!c1) { + return; + } + + Tree::Node* node1 = client_tree_.GetTreeNode(c1); + if (!node1) { + return; + } + + // Swap the states of the clients whether they are floating or not. + bool is_floating0 = c0->is_floating(); + c0->set_floating(c1->is_floating()); + c1->set_floating(is_floating0); + + node0->Swap(node1); } void Workspace::Tile(const Client::Area& tiling_area) const { diff --git a/src/workspace.h b/src/workspace.h index 70678b4..8ac3c89 100644 --- a/src/workspace.h +++ b/src/workspace.h @@ -14,15 +14,30 @@ extern "C" { namespace wmderland { +enum class AreaType { + CENTER, + MID, + EDGE, + UNDEFINED, +}; + class Workspace { public: Workspace(Display* dpy, Window root_window_, Config* config, int id); virtual ~Workspace() = default; bool Has(Window window) const; - void Add(Window window); + void Add(Window window, TilingPosition tiling_position = TilingPosition::AFTER); void Remove(Window window); + void Normalize(); void Move(Window window, Workspace* new_workspace); + void Move(Window window, Window ref, AreaType area_type, TilingDirection tiling_direction, + TilingPosition tiling_position); + void MoveAndSplit(Window window, Window ref, TilingDirection tiling_direction, + TilingPosition tiling_position, bool branch_outer); + void MoveAndInsert(Window window, Window ref, TilingPosition tiling_position, + bool insert_outer = false); + void Swap(Window window0, Window window1); void Tile(const Client::Area& tiling_area) const; void SetTilingDirection(TilingDirection tiling_direction);