Skip to content

Commit

Permalink
feat(tree, layout): enable mouse clicks on tab bar to activate tabs
Browse files Browse the repository at this point in the history
- Also refactor things so `BonsaiNode.render()` doesn't take
  `BonsaiTree` anymore. We instead DI it where necessary.
  • Loading branch information
aravinda0 committed Jun 12, 2024
1 parent 2737c1c commit 8ef1033
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 20 deletions.
5 changes: 5 additions & 0 deletions src/qtile_bonsai/core/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ def union(self, rect: Rect) -> Rect:

return self.__class__(x1, y1, x2 - x1, y2 - y1)

def has_coord(self, x: int, y: int) -> bool:
if x >= self.x and x < self.x2 and y >= self.y and y < self.y2:
return True
return False

def split(self, axis: AxisParam, ratio: float = 0.5) -> tuple[Rect, Rect]:
"""Returns two new Rect instances that have dimensions according to the
requested split.
Expand Down
19 changes: 17 additions & 2 deletions src/qtile_bonsai/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
TreeEvent,
)
from qtile_bonsai.theme import Gruvbox
from qtile_bonsai.tree import BonsaiNodeMixin, BonsaiPane, BonsaiTree
from qtile_bonsai.tree import (
BonsaiNodeMixin,
BonsaiPane,
BonsaiTabContainer,
BonsaiTree,
)
from qtile_bonsai.utils.process import modify_terminal_cmd_with_cwd


Expand Down Expand Up @@ -1013,7 +1018,12 @@ def _init(self):

# We initialize the tree with arbitrary dimensions. These get reset soon as this
# layout's group is assigned to a screen.
self._tree = BonsaiTree(100, 100, config=config)
self._tree = BonsaiTree(
100,
100,
config=config,
on_click_tab_bar=self._handle_click_tab_bar,
)
self._tree.validate_config()

self._tree.subscribe(
Expand Down Expand Up @@ -1228,3 +1238,8 @@ def _check_for_persisted_state(self) -> dict | None:
def _get_state_file_path(self, group) -> pathlib.Path:
tmp_dir = tempfile.gettempdir()
return pathlib.Path(f"{tmp_dir}/qtile_bonsai/state_{os.getpid()}_{group.name}")

def _handle_click_tab_bar(self, tc: BonsaiTabContainer, i: int, button: int):
tab = tc.children[i]
active_pane = self._tree.find_mru_pane(start_node=tab)
self._request_focus(active_pane)
87 changes: 69 additions & 18 deletions src/qtile_bonsai/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def init_ui(self, qtile: Qtile):
"""
pass

def render(self, screen_rect: ScreenRect, tree: "BonsaiTree"):
def render(self, screen_rect: ScreenRect):
"""Renders UI elements of this node."""
pass

Expand All @@ -35,28 +35,41 @@ def finalize(self):


class BonsaiTabContainer(BonsaiNodeMixin, TabContainer):
def __init__(self):
TabBarClickHandler = Callable[["BonsaiTabContainer", int, int], None]

def __init__(
self,
tree: "BonsaiTree",
on_click_tab_bar: "BonsaiTabContainer.TabBarClickHandler | None" = None,
):
super().__init__()

self.bar_window: Internal
self.bar_drawer: Drawer
self.bar_text_layout: TextLayout

self._tree = tree
self._on_click_tab_bar: "BonsaiTabContainer.TabBarClickHandler | None" = (
on_click_tab_bar
)

def init_ui(self, qtile: Qtile):
# Arbitrary coords on init. Will get rendered to proper screen position
# during layout phase.
self.bar_window = qtile.core.create_internal(0, 0, 1, 1)
self.bar_window.process_button_click = self._handle_click_bar

self.bar_drawer = self.bar_window.create_drawer(1, 1)
self.bar_text_layout = self.bar_drawer.textlayout(
"", "000000", "mono", 15, None
)

def render(self, screen_rect: ScreenRect, tree: "BonsaiTree"):
def render(self, screen_rect: ScreenRect):
if self.tab_bar.is_hidden:
self.hide()
return

tree = self._tree
level = self.tab_level

tab_bar_border_color: str = tree.get_config("tab_bar.border_color", level=level)
Expand Down Expand Up @@ -101,10 +114,7 @@ def render(self, screen_rect: ScreenRect, tree: "BonsaiTree"):

self.bar_drawer.clear(tab_bar_bg_color)

if tab_width == "auto":
per_tab_w: int = bar_rect.w // len(self.children)
else:
per_tab_w = tab_width
per_tab_w = self._get_per_tab_width()

# NOTE: This is accurate for monospaced fonts, but is still a safe enough
# approximation for non-monospaced fonts as we may only over-estimate.
Expand All @@ -125,12 +135,7 @@ def render(self, screen_rect: ScreenRect, tree: "BonsaiTree"):
self.bar_text_layout.font_family = tab_font_family
self.bar_text_layout.font_size = tab_font_size

tab_box = Box(
principal_rect=Rect(i * per_tab_w, 0, per_tab_w, bar_rect.h),
margin=tab_margin,
border=0, # Individual tabs don't have borders
padding=tab_padding,
)
tab_box = self._get_object_space_tab_box(i, per_tab_w)

if tab_title_provider is not None:
active_pane = tree.find_mru_pane(start_node=tab)
Expand Down Expand Up @@ -163,6 +168,37 @@ def finalize(self):
self.bar_drawer.finalize()
self.bar_window.kill()

def _handle_click_bar(self, x: int, y: int, button: int):
if self._on_click_tab_bar is None:
return

per_tab_w = self._get_per_tab_width()
i = x // per_tab_w

if i > len(self.children) - 1:
return

tab_box = self._get_object_space_tab_box(i, per_tab_w)

# Note that the `x`, `y` provided here by qtile are object space coords.
if tab_box.border_rect.has_coord(x, y):
self._on_click_tab_bar(self, i, button)

def _get_per_tab_width(self) -> int:
tab_width_config: int | str = self._tree.get_config("tab_bar.tab.width")
if tab_width_config == "auto":
return self.tab_bar.box.principal_rect.w // len(self.children)
return int(tab_width_config)

def _get_object_space_tab_box(self, i: int, per_tab_w: int) -> Box:
bar_rect = self.tab_bar.box.principal_rect
return Box(
principal_rect=Rect((i * per_tab_w), 0, per_tab_w, bar_rect.h),
margin=self._tree.get_config("tab_bar.tab.margin", level=self.tab_level),
border=0,
padding=self._tree.get_config("tab_bar.tab.padding", level=self.tab_level),
)


class BonsaiTab(BonsaiNodeMixin, Tab):
pass
Expand All @@ -179,6 +215,7 @@ def __init__(
*,
margin: PerimieterParams = 0,
border: PerimieterParams = 1,
tree: "BonsaiTree",
):
super().__init__(
principal_rect=principal_rect,
Expand All @@ -189,16 +226,18 @@ def __init__(

self.window: Window | None = None

def render(self, screen_rect: ScreenRect, tree: "BonsaiTree"):
self._tree = tree

def render(self, screen_rect: ScreenRect):
if self.window is None:
return

if self.window.has_focus:
window_border_color = tree.get_config(
window_border_color = self._tree.get_config(
"window.active.border_color", level=self.tab_level
)
else:
window_border_color = tree.get_config(
window_border_color = self._tree.get_config(
"window.border_color", level=self.tab_level
)

Expand All @@ -218,6 +257,17 @@ def as_dict(self) -> dict:


class BonsaiTree(Tree):
def __init__(
self,
width: int,
height: int,
config: Tree.MultiLevelConfig | None = None,
on_click_tab_bar: BonsaiTabContainer.TabBarClickHandler | None = None,
):
super().__init__(width, height, config)

self._on_click_tab_bar = on_click_tab_bar

def create_pane(
self,
principal_rect: Rect | None = None,
Expand All @@ -235,6 +285,7 @@ def create_pane(
principal_rect=principal_rect,
margin=margin,
border=border,
tree=self,
)

def create_split_container(self) -> BonsaiSplitContainer:
Expand All @@ -244,12 +295,12 @@ def create_tab(self, title: str = "") -> BonsaiTab:
return BonsaiTab(title)

def create_tab_container(self) -> BonsaiTabContainer:
return BonsaiTabContainer()
return BonsaiTabContainer(tree=self, on_click_tab_bar=self._on_click_tab_bar)

def render(self, screen_rect: ScreenRect):
for node in self.iter_walk():
if self.is_visible(node):
node.render(screen_rect, self)
node.render(screen_rect)
else:
node.hide()

Expand Down

0 comments on commit 8ef1033

Please sign in to comment.