You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ueberzug/ueberzug/ui.py

178 lines
6.4 KiB
Python

"""This module contains user interface related classes and methods.
"""
import abc
import weakref
import attr
import PIL.Image as Image
import ueberzug.xutil as xutil
import ueberzug.geometry as geometry
import ueberzug.scaling as scaling
import ueberzug.X as X
def roundup(value, unit):
return ((value + (unit - 1)) & ~(unit - 1)) >> 3
class WindowFactory:
"""Window factory class"""
def __init__(self, display):
self.display = display
@abc.abstractmethod
def create(self, *window_infos: xutil.TerminalWindowInfo):
"""Creates a child window for each window id."""
raise NotImplementedError()
class CanvasWindow(X.OverlayWindow):
"""Ensures unmapping of windows"""
class Factory(WindowFactory):
"""CanvasWindows factory class"""
def __init__(self, display, view):
super().__init__(display)
self.view = view
def create(self, *window_infos: xutil.TerminalWindowInfo):
return [CanvasWindow(self.display, self.view, info)
for info in window_infos]
class Placement:
@attr.s
class TransformedImage:
"""Data class which contains the options
an image was transformed with
and the image data."""
options = attr.ib(type=tuple)
data = attr.ib(type=bytes)
def __init__(self, x: int, y: int, width: int, height: int,
scaling_position: geometry.Point,
scaler: scaling.ImageScaler,
path: str, image: Image, last_modified: int,
cache: weakref.WeakKeyDictionary = None):
# x, y are useful names in this case
# pylint: disable=invalid-name
self.x = x
self.y = y
self.width = width
self.height = height
self.scaling_position = scaling_position
self.scaler = scaler
self.path = path
self.image = image
self.last_modified = last_modified
self.cache = cache or weakref.WeakKeyDictionary()
def transform_image(self, term_info: xutil.TerminalWindowInfo,
width: int, height: int,
format_scanline: tuple):
"""Scales to image and calculates
the width & height needed to display it.
Returns:
tuple of (width: int, height: int, image: bytes)
"""
image = self.image.await_image()
scanline_pad, scanline_unit = format_scanline
transformed_image = self.cache.get(term_info)
final_size = self.scaler.calculate_resolution(
image, width, height)
options = (self.scaler.get_scaler_name(),
self.scaling_position, final_size)
if (transformed_image is None
or transformed_image.options != options):
image = self.scaler.scale(
image, self.scaling_position, width, height)
stride = roundup(image.width * scanline_unit, scanline_pad)
transformed_image = self.TransformedImage(
options, image.tobytes("raw", 'BGRX', stride, 0))
self.cache[term_info] = transformed_image
return (*final_size, transformed_image.data)
def resolve(self, pane_offset: geometry.Distance,
term_info: xutil.TerminalWindowInfo,
format_scanline):
"""Resolves the position and size of the image
according to the teminal window information.
Returns:
tuple of (x: int, y: int, width: int, height: int,
image: PIL.Image)
"""
# x, y are useful names in this case
# pylint: disable=invalid-name
image = self.image.await_image()
x = int((self.x + pane_offset.left) * term_info.font_width +
term_info.padding_horizontal)
y = int((self.y + pane_offset.top) * term_info.font_height +
term_info.padding_vertical)
width = int((self.width and (self.width * term_info.font_width))
or image.width)
height = \
int((self.height and (self.height * term_info.font_height))
or image.height)
return (x, y, *self.transform_image(
term_info, width, height, format_scanline))
def __init__(self, display: X.Display,
view, parent_info: xutil.TerminalWindowInfo):
"""Changes the foreground color of the gc object.
Args:
display (X.Display): any created instance
parent_id (int): the X11 window id of the parent window
"""
super().__init__(display, parent_info.window_id)
self.parent_info = parent_info
self._view = view
self.scanline_pad = display.bitmap_format_scanline_pad
self.scanline_unit = display.bitmap_format_scanline_unit
self.screen_width = display.screen_width
self.screen_height = display.screen_height
self._image = X.Image(
display,
self.screen_width,
self.screen_height)
def __enter__(self):
self.draw()
return self
def __exit__(self, *args):
pass
def draw(self):
"""Draws the window and updates the visibility mask."""
rectangles = []
if not self.parent_info.ready:
self.parent_info.calculate_sizes(
self.width, self.height)
for placement in self._view.media.values():
# x, y are useful names in this case
# pylint: disable=invalid-name
x, y, width, height, image = \
placement.resolve(self._view.offset, self.parent_info,
(self.scanline_pad, self.scanline_unit))
rectangles.append((x, y, width, height))
self._image.draw(x, y, width, height, image)
self._image.copy_to(
self.id,
0, 0,
min(self.width, self.screen_width),
min(self.height, self.screen_height))
self.set_visibility_mask(rectangles)
super().draw()
def reset_terminal_info(self):
"""Resets the terminal information of this window."""
self.parent_info.reset()