scrollbars

This commit is contained in:
Will McGugan 2021-05-28 16:09:56 +01:00
parent 068cb86555
commit 6aac8da698
11 changed files with 194 additions and 35 deletions

View File

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [10.3.0] - Unreleased
### Added
- Added Console.size setter
- Added Console.width setter
- Added Console.height setter
## [10.2.2] - 2021-05-19
### Fixed

View File

@ -962,8 +962,16 @@ class Console:
Returns:
int: The width (in characters) of the console.
"""
width, _ = self.size
return width
return self.size.width
@width.setter
def width(self, width: int) -> None:
"""Set width.
Args:
width (int): New width.
"""
self._width = width
@property
def height(self) -> int:
@ -972,8 +980,16 @@ class Console:
Returns:
int: The height (in lines) of the console.
"""
_, height = self.size
return height
return self.size.height
@height.setter
def height(self, height: int) -> None:
"""Set height.
Args:
height (int): new height.
"""
self._height = height
def bell(self) -> None:
"""Play a 'bell' sound (if supported by the terminal)."""

View File

@ -24,6 +24,8 @@ class Screen:
style (StyleType, optional): Optional background style. Defaults to None.
"""
renderable: "RenderableType"
def __init__(
self,
*renderables: "RenderableType",

View File

@ -7,7 +7,11 @@ from .style import Style
from itertools import filterfalse
from operator import attrgetter
from typing import Iterable, List, Sequence, Union, Tuple
from typing import Iterable, List, Sequence, Union, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
class ControlType(IntEnum):
@ -404,6 +408,30 @@ class Segment(NamedTuple):
yield cls(text, None, control)
class Segments:
"""A simple renderable to render an iterable of segments.
Args:
segments (Iterable[Segment]): An iterable of segments.
new_lines (bool, optional): Add new lines between segments. Defaults to False.
"""
def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
self.segments = list(segments)
self.new_lines = new_lines
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.new_lines:
line = Segment.line()
for segment in self.segments:
yield segment
yield line
else:
yield from self.segments
if __name__ == "__main__": # pragma: no cover
from rich.syntax import Syntax
from rich.text import Text

View File

@ -4,8 +4,8 @@ import logging
import signal
from typing import Any, Dict, Set
from rich.live import Live
from rich.control import Control
from rich.repr import rich_repr, RichReprResult
from rich.screen import Screen
from . import events
@ -22,23 +22,27 @@ log = logging.getLogger("rich")
LayoutDefinition = Dict[str, Any]
@rich_repr
class App(MessagePump):
view: View
def __init__(
self,
console: Console = None,
view: View = None,
screen: bool = True,
auto_refresh=4,
title: str = "Megasoma Application",
):
super().__init__()
self.console = console or get_console()
self._screen = screen
self._auto_refresh = auto_refresh
self.title = title
self.view = view or LayoutView()
self.children: Set[MessagePump] = set()
def __rich_repr__(self) -> RichReprResult:
yield "title", self.title
@classmethod
def run(cls, console: Console = None, screen: bool = True):
async def run_app() -> None:
@ -99,8 +103,6 @@ class App(MessagePump):
if __name__ == "__main__":
import asyncio
from logging import FileHandler
from rich.layout import Layout
from rich.panel import Panel
from .widgets.header import Header

View File

@ -136,7 +136,7 @@ class Timer(Event, type=EventType.TIMER, priority=10):
self.callback = callback
def __rich_repr__(self) -> RichReprResult:
yield "timer", self.timer
yield self.timer.name
class Focus(Event, type=EventType.FOCUS):

View File

@ -1,6 +0,0 @@
from rich.layout import Layout
class LayoutManager:
def __init__(self) -> None:
self.layout = Layout()

View File

@ -105,7 +105,7 @@ class MessagePump:
except Exception:
log.exception("error getting message")
break
log.debug(repr(message))
log.debug("%r -> %r", message, self)
await self.dispatch_message(message, priority)
async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]:
@ -150,16 +150,3 @@ class MessagePump:
async def on_timer(self, event: events.Timer) -> None:
if event.callback is not None:
await event.callback()
if __name__ == "__main__":
class Widget(MessagePump):
pass
widget1 = Widget()
widget2 = Widget()
import asyncio
asyncio.get_event_loop().run_until_complete(widget1.run_message_loop())

85
rich/tui/scrollbar.py Normal file
View File

@ -0,0 +1,85 @@
from typing import List, Optional
from rich.segment import Segment
from rich.style import Style
def render_bar(
height: int = 25,
size: float = 100,
window_size: float = 25,
position: float = 0,
bar_style: Optional[Style] = None,
back_style: Optional[Style] = None,
ascii_only: bool = False,
vertical: bool = True,
) -> List[Segment]:
if vertical:
if ascii_only:
solid = "|"
half_start = "|"
half_end = "|"
else:
solid = ""
half_start = ""
half_end = ""
else:
if ascii_only:
solid = "-"
half_start = "-"
half_end = "-"
else:
solid = ""
half_start = ""
half_end = ""
_bar_style = bar_style or Style.parse("bright_magenta")
_back_style = back_style or Style.parse("#555555")
_Segment = Segment
start_bar_segment = _Segment(half_start, _bar_style)
end_bar_segment = _Segment(half_end, _bar_style)
bar_segment = _Segment(solid, _bar_style)
start_back_segment = _Segment(half_end, _back_style)
end_back_segment = _Segment(half_end, _back_style)
back_segment = _Segment(solid, _back_style)
segments = [back_segment] * height
step_size = size / height
start = position / step_size
end = (position + window_size) / step_size
start_index = int(start)
end_index = int(end)
bar_height = (end_index - start_index) + 1
segments[start_index:end_index] = [bar_segment] * bar_height
sub_position = start % 1.0
if sub_position >= 0.5:
segments[start_index] = start_bar_segment
elif start_index:
segments[start_index - 1] = end_back_segment
sub_position = end % 1.0
if sub_position < 0.5:
segments[end_index] = end_bar_segment
elif end_index + 1 < len(segments):
segments[end_index + 1] = start_back_segment
return segments
if __name__ == "__main__":
from rich.console import Console
from rich.segment import Segments
console = Console()
bar = render_bar(height=20, position=10, vertical=False, ascii_only=False)
console.print(Segments(bar, new_lines=False))

View File

@ -1,8 +1,9 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from rich.console import Console, RenderableType
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.layout import Layout
from rich.live import Live
from rich.repr import rich_repr, RichReprResult
from . import events
from ._context import active_app
@ -14,7 +15,8 @@ if TYPE_CHECKING:
from .app import App
class View(MessagePump):
@rich_repr
class View(ABC, MessagePump):
@property
def app(self) -> "App":
return active_app.get()
@ -23,16 +25,34 @@ class View(MessagePump):
def console(self) -> Console:
return active_app.get().console
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
return
yield
def __rich_repr__(self) -> RichReprResult:
return
yield
async def on_resize(self, event: events.Resize) -> None:
pass
@abstractmethod
async def mount(self, widget: Widget, *, slot: str = "main") -> None:
...
class LayoutView(View):
layout: Layout
def __init__(
self, layout: Layout = None, title: str = "Layout Application"
self,
layout: Layout = None,
name: str = "default",
title: str = "Layout Application",
) -> None:
self.name = name
self.title = title
if layout is None:
layout = Layout()
@ -49,6 +69,9 @@ class LayoutView(View):
self.layout = layout
super().__init__()
def __rich_repr__(self) -> RichReprResult:
yield "name", self.name
def __rich__(self) -> RenderableType:
return self.layout

View File

@ -0,0 +1,14 @@
from typing import Optional
from rich.console import RenderableType
from ..widget import Widget
class Window(Widget):
renderable: Optional[RenderableType]
def __init__(self, renderable: RenderableType):
self.renderable = renderable
def update(self, renderable: RenderableType) -> None:
self.renderable = renderable