From ef551ed4b85bc29b3eb48775faeadcf41596c40a Mon Sep 17 00:00:00 2001
From: David Robillard <d@drobilla.net>
Date: Thu, 23 Jan 2025 17:00:37 -0500
Subject: Replace puglPostRedisplayRect() with puglObscureRegion()

---
 bindings/cpp/include/pugl/pugl.hpp |  9 +++++---
 doc/c/event-loop.rst               |  2 +-
 examples/pugl_cairo_demo.c         | 43 +++++++++++++++++---------------------
 include/pugl/pugl.h                | 42 ++++++++++++++++++++++++++++++-------
 meson.build                        |  2 +-
 src/mac.m                          | 30 +++++++++++++++++++-------
 src/win.c                          | 20 +++++++++++++-----
 src/x11.c                          | 25 +++++++++++++++-------
 test/test_redisplay.c              | 22 ++++++++++---------
 9 files changed, 129 insertions(+), 66 deletions(-)

diff --git a/bindings/cpp/include/pugl/pugl.hpp b/bindings/cpp/include/pugl/pugl.hpp
index c74962b..bf87ed3 100644
--- a/bindings/cpp/include/pugl/pugl.hpp
+++ b/bindings/cpp/include/pugl/pugl.hpp
@@ -615,10 +615,13 @@ public:
     return static_cast<Status>(puglObscureView(cobj()));
   }
 
-  /// @copydoc puglPostRedisplayRect
-  Status postRedisplayRect(const Rect& rect) noexcept
+  /// "Obscure" a region so it will be exposed in the next render
+  Status obscure(const int      x,
+                 const int      y,
+                 const unsigned width,
+                 const unsigned height)
   {
-    return static_cast<Status>(puglPostRedisplayRect(cobj(), rect));
+    return static_cast<Status>(puglObscureRegion(cobj(), x, y, width, height));
   }
 
   /**
diff --git a/doc/c/event-loop.rst b/doc/c/event-loop.rst
index 430566f..7bee397 100644
--- a/doc/c/event-loop.rst
+++ b/doc/c/event-loop.rst
@@ -23,7 +23,7 @@ while those that draw continuously may use a significant fraction of the frame p
 Redrawing
 *********
 
-Occasional redrawing can be requested by calling :func:`puglObscureView` or :func:`puglPostRedisplayRect`.
+Occasional redrawing can be requested by calling :func:`puglObscureView` or :func:`puglObscureRegion`.
 After these are called,
 a :struct:`PuglExposeEvent` will be dispatched on the next call to :func:`puglUpdate`.
 
diff --git a/examples/pugl_cairo_demo.c b/examples/pugl_cairo_demo.c
index 2859ff0..d973d69 100644
--- a/examples/pugl_cairo_demo.c
+++ b/examples/pugl_cairo_demo.c
@@ -114,13 +114,12 @@ postButtonRedisplay(PuglView* view)
   const ViewScale scale = getScale(view);
 
   for (const Button* b = buttons; b->label; ++b) {
-    const double   span = sqrt(b->w * b->w + b->h * b->h);
-    const PuglRect rect = {(PuglCoord)((b->x - span) * scale.x),
-                           (PuglCoord)((b->y - span) * scale.y),
-                           (PuglSpan)ceil(span * 2.0 * scale.x),
-                           (PuglSpan)ceil(span * 2.0 * scale.y)};
-
-    puglPostRedisplayRect(view, rect);
+    const double span = sqrt(b->w * b->w + b->h * b->h);
+    puglObscureRegion(view,
+                      (int)((b->x - span) * scale.x),
+                      (int)((b->y - span) * scale.y),
+                      (unsigned)ceil(span * 2.0 * scale.x),
+                      (unsigned)ceil(span * 2.0 * scale.y));
   }
 }
 
@@ -172,18 +171,17 @@ onClose(PuglView* view)
   app->quit = 1;
 }
 
-static PuglRect
-mouseCursorViewBounds(const PuglView* const view,
-                      const double          mouseX,
-                      const double          mouseY)
+static PuglStatus
+obscureMouseCursor(PuglView* const view,
+                   const ViewScale scale,
+                   const double    mouseX,
+                   const double    mouseY)
 {
-  const ViewScale scale = getScale(view);
-  const PuglRect  rect  = {(PuglCoord)floor(mouseX - (10.0 * scale.x)),
-                           (PuglCoord)floor(mouseY - (10.0 * scale.y)),
-                           (PuglSpan)ceil(20.0 * scale.x),
-                           (PuglSpan)ceil(20.0 * scale.y)};
-
-  return rect;
+  return puglObscureRegion(view,
+                           (int)floor(mouseX - (10.0 * scale.x)),
+                           (int)floor(mouseY - (10.0 * scale.y)),
+                           (unsigned)ceil(20.0 * scale.x),
+                           (unsigned)ceil(20.0 * scale.y));
 }
 
 static PuglStatus
@@ -193,6 +191,7 @@ onEvent(PuglView* view, const PuglEvent* event)
 
   printEvent(event, "Event: ", app->opts.verbose);
 
+  const ViewScale scale = getScale(view);
   switch (event->type) {
   case PUGL_KEY_PRESS:
     if (event->key.key == 'q' || event->key.key == PUGL_KEY_ESCAPE) {
@@ -209,16 +208,12 @@ onEvent(PuglView* view, const PuglEvent* event)
     break;
   case PUGL_MOTION:
     // Redisplay to clear the old cursor position
-    puglPostRedisplayRect(
-      view,
-      mouseCursorViewBounds(view, app->lastDrawnMouseX, app->lastDrawnMouseY));
+    obscureMouseCursor(view, scale, app->lastDrawnMouseX, app->lastDrawnMouseY);
 
     // Redisplay to show the new cursor position
     app->currentMouseX = event->motion.x;
     app->currentMouseY = event->motion.y;
-    puglPostRedisplayRect(
-      view,
-      mouseCursorViewBounds(view, app->currentMouseX, app->currentMouseY));
+    obscureMouseCursor(view, scale, app->currentMouseX, app->currentMouseY);
 
     app->lastDrawnMouseX = app->currentMouseX;
     app->lastDrawnMouseY = app->currentMouseY;
diff --git a/include/pugl/pugl.h b/include/pugl/pugl.h
index 49e5fe5..d7366c6 100644
--- a/include/pugl/pugl.h
+++ b/include/pugl/pugl.h
@@ -298,7 +298,7 @@ typedef PuglAnyEvent PuglCloseEvent;
 
    This event is sent to every view near the end of a main loop iteration when
    any pending exposures are about to be redrawn.  It is typically used to mark
-   regions to expose with puglObscureView() or puglPostRedisplayRect().  For
+   regions to expose with puglObscureView() or puglObscureRegion().  For
    example, to continuously animate, obscure the view when an update event is
    received, and it will receive an expose event shortly afterwards.
 */
@@ -1425,14 +1425,29 @@ PuglStatus
 puglObscureView(PuglView* view);
 
 /**
-   Request a redisplay of the given rectangle within the view.
+   "Obscure" a region so it will be exposed in the next render.
+
+   This will cause an expose event to be dispatched later.  If called from
+   within the event handler, the expose should arrive at the end of the current
+   event loop iteration, though this is not strictly guaranteed on all
+   platforms.  If called elsewhere, an expose will be enqueued to be processed
+   in the next event loop iteration.
+
+   The region is clamped to the size of the view if necessary.
 
-   This has the same semantics as puglObscureView(), but allows giving a precise
-   region for redrawing only a portion of the view.
+   @param view The view to expose later.
+   @param x The top-left X coordinate of the rectangle to obscure.
+   @param y The top-left Y coordinate of the rectangle to obscure.
+   @param width The width of the rectangle to obscure.
+   @param height The height coordinate of the rectangle to obscure.
 */
 PUGL_API
 PuglStatus
-puglPostRedisplayRect(PuglView* view, PuglRect rect);
+puglObscureRegion(PuglView* view,
+                  int       x,
+                  int       y,
+                  unsigned  width,
+                  unsigned  height);
 
 /**
    @}
@@ -1634,8 +1649,8 @@ puglStopTimer(PuglView* view, uintptr_t id);
    Currently, only #PUGL_CLIENT events are supported on all platforms.
 
    X11: A #PUGL_EXPOSE event can be sent, which is similar to calling
-   puglPostRedisplayRect(), but will always send a message to the X server,
-   even when called in an event handler.
+   puglObscureRegion(), but will always send a message to the X server, even
+   when called in an event handler.
 
    @return #PUGL_UNSUPPORTED if sending events of this type is not supported,
    #PUGL_UNKNOWN_ERROR if sending the event failed.
@@ -2210,6 +2225,19 @@ puglPostRedisplay(PuglView* view)
   return puglObscureView(view);
 }
 
+/**
+   Request a redisplay of the given rectangle within the view.
+
+   This has the same semantics as puglPostRedisplay(), but allows giving a
+   precise region for redrawing only a portion of the view.
+*/
+static inline PUGL_DEPRECATED_BY("puglObscureRegion")
+PuglStatus
+puglPostRedisplayRect(PuglView* view, PuglRect rect)
+{
+  return puglObscureRegion(view, rect.x, rect.y, rect.width, rect.height);
+}
+
 #endif // PUGL_DISABLE_DEPRECATED
 
 /**
diff --git a/meson.build b/meson.build
index ec58fb8..1c2d8ca 100644
--- a/meson.build
+++ b/meson.build
@@ -12,7 +12,7 @@ project(
   ],
   license: 'ISC',
   meson_version: '>= 0.54.0',
-  version: '0.5.3',
+  version: '0.5.5',
 )
 
 pugl_src_root = meson.current_source_dir()
diff --git a/src/mac.m b/src/mac.m
index bf8e0f4..5f9d71e 100644
--- a/src/mac.m
+++ b/src/mac.m
@@ -7,6 +7,7 @@
 #include "mac.h"
 
 #include "internal.h"
+#include "macros.h"
 #include "platform.h"
 
 #include "pugl/pugl.h"
@@ -1651,15 +1652,30 @@ puglObscureView(PuglView* view)
 }
 
 PuglStatus
-puglPostRedisplayRect(PuglView* view, const PuglRect rect)
+puglObscureRegion(PuglView*      view,
+                  const int      x,
+                  const int      y,
+                  const unsigned width,
+                  const unsigned height)
 {
-  const NSRect rectPx = {
-    {(double)rect.x,
-     (double)view->lastConfigure.height - (rect.y + rect.height)},
-    {(double)rect.width, (double)rect.height},
-  };
+  if (!puglIsValidPosition(x, y) || !puglIsValidSize(width, height)) {
+    return PUGL_BAD_PARAMETER;
+  }
+
+  const PuglSpan viewHeight = view->lastConfigure.height;
+
+  const int      cx = MAX(0, x);
+  const int      cy = MAX(0, viewHeight - y - (int)height);
+  const unsigned cw = MIN(view->lastConfigure.width, width);
+  const unsigned ch = MIN(view->lastConfigure.height, height);
 
-  [view->impl->drawView setNeedsDisplayInRect:nsRectToPoints(view, rectPx)];
+  if (cw == view->lastConfigure.width && ch == view->lastConfigure.height) {
+    [view->impl->drawView setNeedsDisplay:YES];
+  } else {
+    const NSRect rectPx = NSMakeRect(cx, cy, cw, ch);
+
+    [view->impl->drawView setNeedsDisplayInRect:nsRectToPoints(view, rectPx)];
+  }
 
   return PUGL_SUCCESS;
 }
diff --git a/src/win.c b/src/win.c
index a0d0778..fe8d1b4 100644
--- a/src/win.c
+++ b/src/win.c
@@ -4,6 +4,7 @@
 #include "win.h"
 
 #include "internal.h"
+#include "macros.h"
 #include "platform.h"
 
 #include "pugl/pugl.h"
@@ -1216,13 +1217,22 @@ puglObscureView(PuglView* view)
 }
 
 PuglStatus
-puglPostRedisplayRect(PuglView* view, const PuglRect rect)
+puglObscureRegion(PuglView* const view,
+                  const int       x,
+                  const int       y,
+                  const unsigned  width,
+                  const unsigned  height)
 {
-  const RECT r = {(long)floor(rect.x),
-                  (long)floor(rect.y),
-                  (long)ceil(rect.x + rect.width),
-                  (long)ceil(rect.y + rect.height)};
+  if (!puglIsValidPosition(x, y) || !puglIsValidSize(width, height)) {
+    return PUGL_BAD_PARAMETER;
+  }
+
+  const int      cx = MAX(0, x);
+  const int      cy = MAX(0, y);
+  const unsigned cw = MIN(view->lastConfigure.width, width);
+  const unsigned ch = MIN(view->lastConfigure.height, height);
 
+  const RECT r = {cx, cy, cx + (long)cw, cy + (long)ch};
   InvalidateRect(view->impl->hwnd, &r, false);
 
   return PUGL_SUCCESS;
diff --git a/src/x11.c b/src/x11.c
index 235af39..f60528b 100644
--- a/src/x11.c
+++ b/src/x11.c
@@ -1879,18 +1879,27 @@ puglGetTime(const PuglWorld* const world)
 PuglStatus
 puglObscureView(PuglView* const view)
 {
-  PuglRect rect = puglGetFrame(view);
-  rect.x        = 0;
-  rect.y        = 0;
-
-  return puglPostRedisplayRect(view, rect);
+  return puglObscureRegion(
+    view, 0, 0, view->lastConfigure.width, view->lastConfigure.height);
 }
 
 PuglStatus
-puglPostRedisplayRect(PuglView* const view, const PuglRect rect)
+puglObscureRegion(PuglView* const view,
+                  const int       x,
+                  const int       y,
+                  const unsigned  width,
+                  const unsigned  height)
 {
-  const PuglExposeEvent event = {
-    PUGL_EXPOSE, 0, rect.x, rect.y, rect.width, rect.height};
+  if (!puglIsValidPosition(x, y) || !puglIsValidSize(width, height)) {
+    return PUGL_BAD_PARAMETER;
+  }
+
+  const PuglCoord cx = MAX((PuglCoord)0, (PuglCoord)x);
+  const PuglCoord cy = MAX((PuglCoord)0, (PuglCoord)y);
+  const PuglSpan  cw = MIN(view->lastConfigure.width, (PuglSpan)width);
+  const PuglSpan  ch = MIN(view->lastConfigure.height, (PuglSpan)height);
+
+  const PuglExposeEvent event = {PUGL_EXPOSE, 0, cx, cy, cw, ch};
 
   if (view->world->impl->dispatchingEvents) {
     // Currently dispatching events, add/expand expose for the loop end
diff --git a/test/test_redisplay.c b/test/test_redisplay.c
index f17e4c7..ae7267d 100644
--- a/test/test_redisplay.c
+++ b/test/test_redisplay.c
@@ -51,8 +51,11 @@ typedef struct {
   State           state;
 } PuglTest;
 
-static const PuglRect  redisplayRect   = {2, 4, 8, 16};
-static const uintptr_t postRedisplayId = 42;
+static const PuglCoord obscureX      = 2;
+static const PuglCoord obscureY      = 4;
+static const PuglSpan  obscureWidth  = 8;
+static const PuglSpan  obscureHeight = 16;
+static const uintptr_t obscureId     = 42;
 
 static PuglStatus
 onEvent(PuglView* view, const PuglEvent* event)
@@ -67,7 +70,7 @@ onEvent(PuglView* view, const PuglEvent* event)
   switch (event->type) {
   case PUGL_UPDATE:
     if (test->state == SHOULD_REDISPLAY) {
-      puglPostRedisplayRect(view, redisplayRect);
+      puglObscureRegion(view, obscureX, obscureY, obscureWidth, obscureHeight);
       test->state = POSTED_REDISPLAY;
     }
     break;
@@ -75,13 +78,12 @@ onEvent(PuglView* view, const PuglEvent* event)
   case PUGL_EXPOSE:
     if (test->state == START) {
       test->state = EXPOSED;
-    } else if (test->state == POSTED_REDISPLAY &&
-               event->expose.x <= redisplayRect.x &&
-               event->expose.y <= redisplayRect.y &&
+    } else if (test->state == POSTED_REDISPLAY && event->expose.x <= obscureX &&
+               event->expose.y <= obscureY &&
                (event->expose.x + event->expose.width >=
-                redisplayRect.x + redisplayRect.width) &&
+                obscureX + obscureWidth) &&
                (event->expose.y + event->expose.height >=
-                redisplayRect.y + redisplayRect.height)) {
+                obscureY + obscureHeight)) {
       test->state = REDISPLAYED;
     } else if (test->state == REDISPLAYED) {
       test->state = REREDISPLAYED;
@@ -89,7 +91,7 @@ onEvent(PuglView* view, const PuglEvent* event)
     break;
 
   case PUGL_CLIENT:
-    if (event->client.data1 == postRedisplayId) {
+    if (event->client.data1 == obscureId) {
       test->state = SHOULD_REDISPLAY;
     }
     break;
@@ -128,7 +130,7 @@ main(int argc, char** argv)
 
   // Send a custom event to trigger a redisplay in the event loop
   PuglEvent client_event    = {{PUGL_CLIENT, 0}};
-  client_event.client.data1 = postRedisplayId;
+  client_event.client.data1 = obscureId;
   client_event.client.data2 = 0;
   assert(!puglSendEvent(test.view, &client_event));
 
-- 
cgit v1.2.1