Files
Browser/ren_browser/tabs/tabs.py
Ivan a32a542c54 Improve TabsManager for adaptive tab visibility and overflow handling
- Updated the constructor to set the resize event handler for dynamic tab visibility.
- Refactored tab visibility logic to adjust based on available page width, moving excess tabs to an overflow menu.
- Enhanced tab addition and removal methods to ensure proper overflow management.
- Updated unit tests to verify adaptive behavior of the overflow menu based on page width changes.
2025-09-28 20:26:03 -05:00

231 lines
8.2 KiB
Python

"""Tab management system for Ren Browser.
Provides tab creation, switching, and content management functionality
for the browser interface.
"""
from types import SimpleNamespace
import flet as ft
from ren_browser.renderer.micron import render_micron
from ren_browser.renderer.plaintext import render_plaintext
class TabsManager:
"""Manages browser tabs and their content.
Handles tab creation, switching, closing, and content rendering.
"""
def __init__(self, page: ft.Page) -> None:
"""Initialize the tab manager.
Args:
page: Flet page instance for UI updates.
"""
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.overflow_menu = None
self.content_container = ft.Container(
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5),
)
default_content = (
render_micron("Welcome to Ren Browser")
if app_module.RENDERER == "micron"
else render_plaintext("Welcome to Ren Browser")
)
self._add_tab_internal("Home", default_content)
self.add_btn = ft.IconButton(
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,
)
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 _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,
expand=True,
text_style=ft.TextStyle(size=12),
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8),
)
go_btn = ft.IconButton(
ft.Icons.OPEN_IN_BROWSER,
tooltip="Load URL",
on_click=lambda e, i=idx: self._on_tab_go(e, i),
)
content_control = content
tab_content = ft.Column(
expand=True,
controls=[
content_control,
],
)
self.manager.tabs.append(
{
"title": title,
"url_field": url_field,
"go_btn": go_btn,
"content_control": content_control,
"content": tab_content,
},
)
tab_container = ft.Container(
content=ft.Text(title),
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, tab_container)
self._update_tab_visibility()
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
content = (
render_micron(content_text)
if app_module.RENDERER == "micron"
else render_plaintext(content_text)
)
self._add_tab_internal(title, content)
self.select_tab(len(self.manager.tabs) - 1)
self.page.update()
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.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) -> None:
"""Select and display the tab at the given index.
Args:
idx: Index of the tab to select.
"""
self.manager.index = idx
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) -> 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:
return
placeholder_text = f"Loading content for {url}"
import ren_browser.app as app_module
new_control = (
render_micron(placeholder_text)
if app_module.RENDERER == "micron"
else render_plaintext(placeholder_text)
)
tab["content_control"] = new_control
tab["content"].controls[0] = new_control
if self.manager.index == idx:
self.content_container.content = tab["content"]
self.page.update()