Merge pull request 'Tab-Overhaul' (#2) from Tab-Overhaul into master
Reviewed-on: Ivan/Ren-Browser#2
This commit was merged in pull request #4.
This commit is contained in:
@@ -18,7 +18,7 @@ class TabsManager:
|
||||
Handles tab creation, switching, closing, and content rendering.
|
||||
"""
|
||||
|
||||
def __init__(self, page: ft.Page):
|
||||
def __init__(self, page: ft.Page) -> None:
|
||||
"""Initialize the tab manager.
|
||||
|
||||
Args:
|
||||
@@ -28,10 +28,14 @@ class TabsManager:
|
||||
import ren_browser.app as app_module
|
||||
|
||||
self.page = page
|
||||
self.page.on_resize = self._on_resize
|
||||
self.manager = SimpleNamespace(tabs=[], index=0)
|
||||
self.tab_bar = ft.Row(spacing=4)
|
||||
self.tab_bar = ft.Row(
|
||||
spacing=4,
|
||||
)
|
||||
self.overflow_menu = None
|
||||
self.content_container = ft.Container(
|
||||
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5)
|
||||
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5),
|
||||
)
|
||||
|
||||
default_content = (
|
||||
@@ -41,15 +45,75 @@ class TabsManager:
|
||||
)
|
||||
self._add_tab_internal("Home", default_content)
|
||||
self.add_btn = ft.IconButton(
|
||||
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click
|
||||
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click,
|
||||
)
|
||||
self.close_btn = ft.IconButton(
|
||||
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click
|
||||
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click,
|
||||
)
|
||||
self.tab_bar.controls.extend([self.add_btn, self.close_btn])
|
||||
self.tab_bar.controls.append(self.add_btn)
|
||||
self.tab_bar.controls.append(self.close_btn)
|
||||
self.select_tab(0)
|
||||
self._update_tab_visibility()
|
||||
|
||||
def _add_tab_internal(self, title: str, content: ft.Control):
|
||||
def _on_resize(self, e) -> None: # type: ignore
|
||||
"""Handle page resize event and update tab visibility."""
|
||||
self._update_tab_visibility()
|
||||
|
||||
def _update_tab_visibility(self) -> None:
|
||||
"""Dynamically adjust tab visibility based on page width.
|
||||
|
||||
Hides tabs that do not fit and moves them to an overflow menu.
|
||||
"""
|
||||
if not self.page.width or self.page.width == 0:
|
||||
return
|
||||
|
||||
if self.overflow_menu and self.overflow_menu in self.tab_bar.controls:
|
||||
self.tab_bar.controls.remove(self.overflow_menu)
|
||||
self.overflow_menu = None
|
||||
|
||||
"""Estimate available width for tabs (Page width - buttons - padding)."""
|
||||
available_width = self.page.width - 100
|
||||
|
||||
cumulative_width = 0
|
||||
visible_tabs_count = 0
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
|
||||
for i, tab in enumerate(self.manager.tabs):
|
||||
"""Estimate tab width: (char count * avg char width) + padding + spacing."""
|
||||
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.spacing
|
||||
|
||||
"""Always show at least one tab."""
|
||||
if cumulative_width + estimated_width <= available_width or i == 0:
|
||||
cumulative_width += estimated_width
|
||||
if i < len(tab_containers):
|
||||
tab_containers[i].visible = True
|
||||
visible_tabs_count += 1
|
||||
elif i < len(tab_containers):
|
||||
tab_containers[i].visible = False
|
||||
|
||||
if len(self.manager.tabs) > visible_tabs_count:
|
||||
"""Move extra tabs to overflow menu."""
|
||||
overflow_items = []
|
||||
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
||||
tab_data = self.manager.tabs[i]
|
||||
overflow_items.append(
|
||||
ft.PopupMenuItem(
|
||||
text=tab_data["title"],
|
||||
on_click=lambda e, idx=i: self.select_tab(idx), # type: ignore
|
||||
),
|
||||
)
|
||||
|
||||
self.overflow_menu = ft.PopupMenuButton(
|
||||
icon=ft.Icons.MORE_HORIZ,
|
||||
tooltip=f"{len(self.manager.tabs) - visible_tabs_count} more tabs",
|
||||
items=overflow_items,
|
||||
)
|
||||
|
||||
self.tab_bar.controls.insert(visible_tabs_count, self.overflow_menu)
|
||||
|
||||
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
|
||||
"""Add a new tab to the manager with the given title and content."""
|
||||
idx = len(self.manager.tabs)
|
||||
url_field = ft.TextField(
|
||||
value=title,
|
||||
@@ -76,19 +140,22 @@ class TabsManager:
|
||||
"go_btn": go_btn,
|
||||
"content_control": content_control,
|
||||
"content": tab_content,
|
||||
}
|
||||
},
|
||||
)
|
||||
btn = ft.Container(
|
||||
tab_container = ft.Container(
|
||||
content=ft.Text(title),
|
||||
on_click=lambda e, i=idx: self.select_tab(i),
|
||||
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
||||
border_radius=5,
|
||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
||||
)
|
||||
"""Insert the new tab before the add/close buttons."""
|
||||
insert_pos = max(0, len(self.tab_bar.controls) - 2)
|
||||
self.tab_bar.controls.insert(insert_pos, btn)
|
||||
self.tab_bar.controls.insert(insert_pos, tab_container)
|
||||
self._update_tab_visibility()
|
||||
|
||||
def _on_add_click(self, e):
|
||||
def _on_add_click(self, e) -> None: # type: ignore
|
||||
"""Handle the add tab button click event."""
|
||||
title = f"Tab {len(self.manager.tabs) + 1}"
|
||||
content_text = f"Content for {title}"
|
||||
import ren_browser.app as app_module
|
||||
@@ -102,19 +169,28 @@ class TabsManager:
|
||||
self.select_tab(len(self.manager.tabs) - 1)
|
||||
self.page.update()
|
||||
|
||||
def _on_close_click(self, e):
|
||||
def _on_close_click(self, e) -> None: # type: ignore
|
||||
"""Handle the close tab button click event."""
|
||||
if len(self.manager.tabs) <= 1:
|
||||
return
|
||||
idx = self.manager.index
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
control_to_remove = tab_containers[idx]
|
||||
|
||||
self.manager.tabs.pop(idx)
|
||||
self.tab_bar.controls.pop(idx)
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
control.on_click = lambda e, i=i: self.select_tab(i)
|
||||
self.tab_bar.controls.remove(control_to_remove)
|
||||
|
||||
updated_tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
for i, control in enumerate(updated_tab_containers):
|
||||
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
|
||||
|
||||
new_idx = min(idx, len(self.manager.tabs) - 1)
|
||||
self.select_tab(new_idx)
|
||||
self._update_tab_visibility()
|
||||
self.page.update()
|
||||
|
||||
def select_tab(self, idx: int):
|
||||
def select_tab(self, idx: int) -> None:
|
||||
"""Select and display the tab at the given index.
|
||||
|
||||
Args:
|
||||
@@ -122,15 +198,19 @@ class TabsManager:
|
||||
|
||||
"""
|
||||
self.manager.index = idx
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
for i, control in enumerate(tab_containers):
|
||||
if i == idx:
|
||||
control.bgcolor = ft.Colors.PRIMARY_CONTAINER
|
||||
else:
|
||||
control.bgcolor = ft.Colors.SURFACE_CONTAINER_HIGHEST
|
||||
|
||||
self.content_container.content = self.manager.tabs[idx]["content"]
|
||||
self.page.update()
|
||||
|
||||
def _on_tab_go(self, e, idx: int):
|
||||
def _on_tab_go(self, e, idx: int) -> None: # type: ignore
|
||||
"""Handle the go button click event for a tab, loading new content."""
|
||||
tab = self.manager.tabs[idx]
|
||||
url = tab["url_field"].value.strip()
|
||||
if not url:
|
||||
|
||||
@@ -13,6 +13,7 @@ class TestTabsManager:
|
||||
@pytest.fixture
|
||||
def tabs_manager(self, mock_page):
|
||||
"""Create a TabsManager instance for testing."""
|
||||
mock_page.width = 800 # Simulate page width for adaptive logic
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
@@ -34,6 +35,8 @@ class TestTabsManager:
|
||||
assert len(manager.manager.tabs) == 1
|
||||
assert manager.manager.index == 0
|
||||
assert isinstance(manager.tab_bar, ft.Row)
|
||||
assert manager.tab_bar.scroll is None
|
||||
assert manager.overflow_menu is None
|
||||
assert isinstance(manager.content_container, ft.Container)
|
||||
|
||||
def test_tabs_manager_init_micron_renderer(self, mock_page):
|
||||
@@ -150,7 +153,7 @@ class TestTabsManager:
|
||||
"""Test that tab bar has correct controls."""
|
||||
controls = tabs_manager.tab_bar.controls
|
||||
|
||||
# Should have: home tab, add button, close button
|
||||
# Should have: home tab, add button, close button (and potentially overflow menu)
|
||||
assert len(controls) >= 3
|
||||
assert isinstance(controls[-2], ft.IconButton) # Add button
|
||||
assert isinstance(controls[-1], ft.IconButton) # Close button
|
||||
@@ -233,3 +236,26 @@ class TestTabsManager:
|
||||
tabs_manager.content_container.content
|
||||
== tabs_manager.manager.tabs[2]["content"]
|
||||
)
|
||||
|
||||
def test_adaptive_overflow_behavior(self, tabs_manager):
|
||||
"""Test that the overflow menu adapts to tab changes."""
|
||||
# With page width at 800, add enough tabs that some should overflow.
|
||||
for i in range(10): # Total 11 tabs
|
||||
tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock())
|
||||
|
||||
# Check that an overflow menu exists
|
||||
assert tabs_manager.overflow_menu is not None
|
||||
|
||||
# Simulate a smaller screen, expecting more tabs to overflow
|
||||
tabs_manager.page.width = 400
|
||||
tabs_manager._update_tab_visibility()
|
||||
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
|
||||
assert visible_tabs_small < 11
|
||||
|
||||
# Simulate a larger screen, expecting all tabs to be visible
|
||||
tabs_manager.page.width = 1600
|
||||
tabs_manager._update_tab_visibility()
|
||||
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
|
||||
|
||||
assert visible_tabs_large == 11
|
||||
assert tabs_manager.overflow_menu is None
|
||||
|
||||
Reference in New Issue
Block a user