Add clipping for Buffer draw operations.
authorRadek Brich <radek.brich@devl.cz>
Wed, 26 Mar 2014 09:08:10 +0100
changeset 91 de80e140b0ec
parent 90 781774a8d568
child 92 b97c4e25ed6d
Add clipping for Buffer draw operations.
tuikit/core/application.py
tuikit/core/buffer.py
tuikit/core/container.py
tuikit/core/coords.py
tuikit/driver/curses.py
--- a/tuikit/core/application.py	Wed Mar 19 20:42:52 2014 +0100
+++ b/tuikit/core/application.py	Wed Mar 26 09:08:10 2014 +0100
@@ -1,6 +1,7 @@
 from tuikit.core.window import WindowManager, Window
 from tuikit.core.theme import default_theme
 from tuikit.core.timer import Timer
+from tuikit.core.buffer import ProxyBuffer
 
 import logging
 
@@ -57,8 +58,9 @@
         self._started = True
         self.window_manager.handle_event('resize', *self.driver.size)
 
+        screen = ProxyBuffer(self.driver)
         while not self._quit:
-            self.window_manager.draw(self.driver)
+            self.window_manager.draw(screen)
             self.driver.flush()
 
             timeout = self.timer.nearest_timeout()
--- a/tuikit/core/buffer.py	Wed Mar 19 20:42:52 2014 +0100
+++ b/tuikit/core/buffer.py	Wed Mar 26 09:08:10 2014 +0100
@@ -1,5 +1,5 @@
 from tuikit.core.signal import Signal
-from tuikit.core.coords import Size
+from tuikit.core.coords import Size, Rect
 from tuikit.core.theme import default_theme
 
 from contextlib import contextmanager
@@ -82,12 +82,50 @@
     def attr(self, attr_desc):
         """Set attribute for block of commands, then restore previous attribute."""
         original_attr = self.getattr()
-        self.setattr(attr_desc)
-        yield
-        self.setattr(original_attr)
+        try:
+            self.setattr(attr_desc)
+            yield
+        finally:
+            self.setattr(original_attr)
 
 
-class Buffer(BufferOperationsMixin):
+class BufferClippingMixin:
+
+    @property
+    def clip_rect(self):
+        try:
+            return self._clip_rect
+        except AttributeError:
+            self.clip_rect = None
+            return self._clip_rect
+
+    @clip_rect.setter
+    def clip_rect(self, value):
+        if value:
+            self._clip_rect = value
+        else:
+            self._clip_rect = Rect(0, 0, self.size.w, self.size.h)
+
+    def testxy(self, x, y):
+        """Test if x/y coords are inside current clipping region."""
+        return (x, y) in self.clip_rect
+
+    @contextmanager
+    def clip(self, x, y, w, h):
+        """Update clipping region as intersection of original and new region.
+
+        Restore original region before returning.
+
+        """
+        original_rect = self.clip_rect
+        try:
+            self.clip_rect = original_rect.intersect(Rect(x, y, w, h))
+            yield
+        finally:
+            self.clip_rect = original_rect
+
+
+class Buffer(BufferOperationsMixin, BufferClippingMixin):
 
     """Rectangular character buffer.
 
@@ -118,6 +156,7 @@
     def clear(self):
         """Reset buffer data."""
         self._data = [Cell() for _i in range(self._size.w * self._size.h)]
+        self.clip_rect = None
 
     def get(self, x, y):
         """Get cell data at `x`, `y` coords.
@@ -144,10 +183,11 @@
 
     def putch(self, x, y, ch):
         """Set character on xy coords to c."""
-        self._data[y * self._size.w + x].set(ch, self._current_attr)
+        if self.testxy(x, y):
+            self._data[y * self._size.w + x].set(ch, self._current_attr)
 
 
-class ProxyBuffer(BufferOperationsMixin):
+class ProxyBuffer(BufferOperationsMixin, BufferClippingMixin):
 
     """Special buffer which proxies the operations
     to another buffer or buffer-like class."""
@@ -168,6 +208,7 @@
     def clear(self):
         """Reset buffer data."""
         self.target.clear()
+        self.clip_rect = None
 
     def get(self, x, y):
         """Get cell data at `x`, `y` coords."""
@@ -183,5 +224,6 @@
 
     def putch(self, x, y, ch):
         """Set character on xy coords to c."""
-        self.target.putch(x, y, ch)
+        if self.testxy(x, y):
+            self.target.putch(x, y, ch)
 
--- a/tuikit/core/container.py	Wed Mar 19 20:42:52 2014 +0100
+++ b/tuikit/core/container.py	Wed Mar 26 09:08:10 2014 +0100
@@ -24,9 +24,10 @@
     def draw(self, buffer, x=0, y=0):
         """Draw child widgets."""
         for child in self.children:
-            child.draw(buffer,
-                       x + child.x,
-                       y + child.y)
+            with buffer.clip(x, y, child.width, child.height):
+                child.draw(buffer,
+                           x + child.x,
+                           y + child.y)
 
     def set_theme(self, theme):
         Widget.set_theme(self, theme)
--- a/tuikit/core/coords.py	Wed Mar 19 20:42:52 2014 +0100
+++ b/tuikit/core/coords.py	Wed Mar 26 09:08:10 2014 +0100
@@ -104,3 +104,43 @@
 
     def __repr__(self):
         return 'ReadonlySize(w={0.w},h={0.h})'.format(self._size)
+
+
+class Rect:
+
+    """Rectangle is defined by coordinates and size."""
+
+    def __init__(self, x=0, y=0, w=0, h=0):
+        self.x = x
+        self.y = y
+        self.w = w
+        self.h = h
+
+    def __repr__(self):
+        return 'Rect(x={0.x},y={0.y},w={0.w},h={0.h})'.format(self)
+
+    def __contains__(self, point):
+        """Test if point is positioned inside rectangle.
+
+        `point` should be either Point or pair of int values.
+
+        """
+        x, y = point
+        return (self.x <= x < self.x + self.w
+            and self.y <= y < self.y + self.h)
+
+    def intersect(self, other):
+        x1 = max(self.x, other.x)
+        y1 = max(self.y, other.y)
+        x2 = min(self.x + self.w, other.x + other.w)
+        y2 = min(self.y + self.h, other.y + other.h)
+        if x1 >= x2 or y1 >= y2:
+            return Rect()
+        return Rect(x1, y1, x2-x1, y2-y1)
+
+    def union(self, other):
+        x = min(self.x, other.x)
+        y = min(self.y, other.y)
+        w = max(self.x + self.w, other.x + other.w) - x
+        h = max(self.y + self.h, other.y + other.h) - y
+        return Rect(x, y, w, h)
--- a/tuikit/driver/curses.py	Wed Mar 19 20:42:52 2014 +0100
+++ b/tuikit/driver/curses.py	Wed Mar 26 09:08:10 2014 +0100
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import curses.ascii
 import math
 import logging
@@ -118,12 +116,10 @@
 
     ## drawing ##
 
-    def erase(self):
+    def clear(self):
         self.stdscr.erase()
 
     def putch(self, x, y, ch):
-        if not self.clipstack.test(x, y):
-            return
         try:
             if isinstance(ch, str) and len(ch) == 1:
                 self.stdscr.addstr(y, x, ch)