/* * Copyright (c) 2008-2023 Julien Nadeau Carriere * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Scrollable view container. It allows child widgets to extend past its own * boundaries. It handles clipping (including clipping of mouse events for * partially-visible widgets) and culling of non-visible widgets. */ #include #ifdef AG_WIDGETS #include #include #include #include #include #include #if 0 /* Detect and clip widgets which are completely hidden. */ static void ClipWidgets(AG_Scrollview *_Nonnull sv, AG_Widget *_Nonnull wt) { AG_Rect2 r, rx; AG_Widget *chld; r = WIDGET(sv)->rView; r.w -= sv->wBar; r.h -= sv->hBar; r.x2 = r.x1 + r.w; r.y2 = r.y1 + r.h; if (r.w < 0 || r.h < 0) return; if (!AG_RectIntersect2(&rx, &r, &wt->rView)) { AG_WidgetHideAll(wt); } else { AG_WidgetShowAll(wt); } OBJECT_FOREACH_CHILD(chld, wt, ag_widget) ClipWidgets(sv, chld); } #endif /* Place child widgets at the current offset in the Scrollview. */ static void PlaceWidgets(AG_Scrollview *_Nonnull sv, int *_Nullable wTot, int *_Nullable hTot) { AG_SizeReq rChld; AG_SizeAlloc aChld; AG_Widget *chld; aChld.x = WIDGET(sv)->paddingLeft - sv->xOffs; aChld.y = WIDGET(sv)->paddingTop - sv->yOffs; OBJECT_FOREACH_CHILD(chld, sv, ag_widget) { if (chld == WIDGET(sv->vbar) || chld == WIDGET(sv->hbar)) { continue; } AG_WidgetSizeReq(chld, &rChld); aChld.w = rChld.w; aChld.h = rChld.h; AG_WidgetSizeAlloc(chld, &aChld); switch (sv->type) { case AG_SCROLLVIEW_HORIZ: aChld.x += aChld.w; break; case AG_SCROLLVIEW_VERT: aChld.y += aChld.h; break; } #if 0 ClipWidgets(sv, chld); #endif AG_WidgetUpdateCoords(chld, 0,0); } switch (sv->type) { case AG_SCROLLVIEW_HORIZ: if (wTot != NULL) { *wTot = aChld.x + sv->xOffs + WIDGET(sv)->paddingRight; } if (hTot != NULL) { *hTot = aChld.y+aChld.h + sv->yOffs + WIDGET(sv)->paddingBottom; } break; case AG_SCROLLVIEW_VERT: if (wTot != NULL) { *wTot = aChld.x+aChld.w + sv->xOffs + WIDGET(sv)->paddingRight; } if (hTot != NULL) { *hTot = aChld.y + sv->yOffs + WIDGET(sv)->paddingBottom; } break; } } static void ScrollbarChanged(AG_Event *_Nonnull event) { AG_Scrollview *sv = AG_SCROLLVIEW_PTR(1); PlaceWidgets(sv, NULL, NULL); /* Update clipping */ WIDGET(sv)->flags |= AG_WIDGET_UPDATE_WINDOW; AG_Redraw(sv); } static void MouseMotion(void *obj, int x, int y, int dx, int dy) { AG_Scrollview *sv = obj; if ((sv->flags & AG_SCROLLVIEW_BY_MOUSE) == 0) return; if (sv->flags & AG_SCROLLVIEW_PANNING) { sv->xOffs -= dx; sv->yOffs -= dy; if (sv->xOffs + sv->r.w > sv->xMax) sv->xOffs = sv->xMax - sv->r.w; if (sv->yOffs + sv->r.h > sv->yMax) sv->yOffs = sv->yMax - sv->r.h; if (sv->xOffs < 0) sv->xOffs = 0; if (sv->yOffs < 0) sv->yOffs = 0; } PlaceWidgets(sv, NULL, NULL); /* Update clipping */ WIDGET(sv)->flags |= AG_WIDGET_UPDATE_WINDOW; AG_Redraw(sv); } static void MouseButtonUp(void *obj, AG_MouseButton button, int x, int y) { AG_Scrollview *sv = obj; if ((sv->flags & AG_SCROLLVIEW_BY_MOUSE) == 0) return; switch (button) { case AG_MOUSE_MIDDLE: sv->flags &= ~(AG_SCROLLVIEW_PANNING); break; case AG_MOUSE_LEFT: if (sv->flags & AG_SCROLLVIEW_PAN_LEFT) { sv->flags &= ~(AG_SCROLLVIEW_PANNING); } break; case AG_MOUSE_RIGHT: if (sv->flags & AG_SCROLLVIEW_PAN_RIGHT) { sv->flags &= ~(AG_SCROLLVIEW_PANNING); } break; default: break; } } static void MouseButtonDown(void *obj, AG_MouseButton button, int x, int y) { AG_Scrollview *sv = obj; if ((sv->flags & AG_SCROLLVIEW_BY_MOUSE) == 0) return; switch (button) { case AG_MOUSE_MIDDLE: sv->flags |= AG_SCROLLVIEW_PANNING; if (!AG_WidgetIsFocused(sv)) { AG_WidgetFocus(sv); } break; case AG_MOUSE_LEFT: if (sv->flags & AG_SCROLLVIEW_PAN_LEFT) { sv->flags |= AG_SCROLLVIEW_PANNING; if (!AG_WidgetIsFocused(sv)) AG_WidgetFocus(sv); } break; case AG_MOUSE_RIGHT: if (sv->flags & AG_SCROLLVIEW_PAN_RIGHT) { sv->flags |= AG_SCROLLVIEW_PANNING; if (!AG_WidgetIsFocused(sv)) AG_WidgetFocus(sv); } break; case AG_MOUSE_WHEELUP: if ((sv->yOffs -= AG_GetInt(sv,"y-scroll-amount")) < 0) { sv->yOffs = 0; } break; case AG_MOUSE_WHEELDOWN: if ((sv->yOffs += AG_GetInt(sv,"y-scroll-amount")) > sv->xMax) { sv->yOffs = sv->xMax; } break; case AG_MOUSE_X1: if ((sv->xOffs -= AG_GetInt(sv,"x-scroll-amount")) < 0) { sv->xOffs = 0; } break; case AG_MOUSE_X2: if ((sv->xOffs += AG_GetInt(sv,"x-scroll-amount")) > sv->xMax) { sv->xOffs = sv->xMax; } break; default: break; } PlaceWidgets(sv, NULL, NULL); /* Update clipping */ WIDGET(sv)->flags |= AG_WIDGET_UPDATE_WINDOW; AG_Redraw(sv); } AG_Scrollview * AG_ScrollviewNew(void *parent, Uint flags) { AG_Scrollview *sv; AG_Scrollbar *sb; sv = Malloc(sizeof(AG_Scrollview)); AG_ObjectInit(sv, &agScrollviewClass); if (flags & AG_SCROLLVIEW_HFILL) { WIDGET(sv)->flags |= AG_WIDGET_HFILL; } if (flags & AG_SCROLLVIEW_VFILL) { WIDGET(sv)->flags |= AG_WIDGET_VFILL; } sv->flags |= flags; if (!(flags & AG_SCROLLVIEW_NOPAN_X)) { sb = sv->hbar = AG_ScrollbarNew(sv, AG_SCROLLBAR_HORIZ, AG_SCROLLBAR_EXCL); AG_BindInt(sb, "min", &sv->xMin); AG_BindInt(sb, "max", &sv->xMax); AG_BindInt(sb, "visible", &sv->r.w); AG_BindInt(sb, "value", &sv->xOffs); AG_SetEvent(sb, "scrollbar-changed", ScrollbarChanged,"%p",sv); } if (!(flags & AG_SCROLLVIEW_NOPAN_Y)) { sv->vbar = sb = AG_ScrollbarNew(sv, AG_SCROLLBAR_VERT, AG_SCROLLBAR_EXCL); AG_BindInt(sb, "min", &sv->yMin); AG_BindInt(sb, "max", &sv->yMax); AG_BindInt(sb, "visible", &sv->r.h); AG_BindInt(sb, "value", &sv->yOffs); AG_SetEvent(sb, "scrollbar-changed", ScrollbarChanged,"%p",sv); } if (flags & AG_SCROLLVIEW_BY_MOUSE) { WIDGET(sv)->flags |= AG_WIDGET_UNFOCUSED_MOTION | AG_WIDGET_UNFOCUSED_BUTTONDOWN | AG_WIDGET_UNFOCUSED_BUTTONUP; } AG_ScrollviewSetIncrement(sv, 10); AG_ObjectAttach(parent, sv); return (sv); } void AG_ScrollviewSetIncrement(AG_Scrollview *sv, int incr) { AG_OBJECT_ISA(sv, "AG_Widget:AG_Scrollview:*"); AG_ObjectLock(sv); if (sv->hbar) { AG_SetInt(sv->hbar, "inc", incr); } if (sv->vbar) { AG_SetInt(sv->vbar, "inc", incr); } AG_ObjectUnlock(sv); } static void Init(void *_Nonnull obj) { AG_Scrollview *sv = obj; sv->flags = 0; sv->type = AG_SCROLLVIEW_VERT; /* Pack vertically */ sv->style = AG_SCROLLVIEW_STYLE_WELL; /* 3D well */ memset(&sv->wPre, 0, sizeof(int) + /* wPre */ sizeof(int) + /* hPre */ sizeof(int) + /* xOffs */ sizeof(int) + /* yOffs */ sizeof(int) + /* xMin */ sizeof(int) + /* xMax */ sizeof(int) + /* yMin */ sizeof(int) + /* yMax */ sizeof(int) + /* wBar */ sizeof(int) + /* hBar */ sizeof(AG_Rect) + /* r */ sizeof(AG_Scrollbar *) + /* hbar */ sizeof(AG_Scrollbar *)); /* vbar */ AG_SetInt(sv, "x-scroll-amount", 40); AG_SetInt(sv, "y-scroll-amount", 40); } /* Request an explicit size in pixels (default = 0 = auto) */ void AG_ScrollviewSizeHint(AG_Scrollview *sv, Uint w, Uint h) { AG_OBJECT_ISA(sv, "AG_Widget:AG_Scrollview:*"); sv->wPre = w; sv->hPre = h; } static void SizeRequest(void *_Nonnull p, AG_SizeReq *_Nonnull r) { AG_Scrollview *sv = p; AG_SizeReq rBar, rChld; AG_Widget *chld; r->w = WIDGET(sv)->paddingLeft + sv->wPre + WIDGET(sv)->paddingRight; r->h = WIDGET(sv)->paddingTop + sv->hPre + WIDGET(sv)->paddingBottom; if (sv->hbar) { AG_WidgetSizeReq(sv->hbar, &rBar); r->h += rBar.h; } if (sv->vbar) { AG_WidgetSizeReq(sv->vbar, &rBar); r->w += rBar.w; } if (sv->wPre == 0 || sv->hPre == 0) { /* Auto-size */ int wMax = 0, hMax = 0; OBJECT_FOREACH_CHILD(chld, sv, ag_widget) { if (chld == WIDGET(sv->vbar) || chld == WIDGET(sv->hbar)) { continue; } AG_WidgetSizeReq(chld, &rChld); if (rChld.w > wMax) { wMax = rChld.w; } if (rChld.h > hMax) { hMax = rChld.h; } switch (sv->type) { case AG_SCROLLVIEW_HORIZ: r->h = MAX(r->h, hMax); r->w += rChld.w; break; case AG_SCROLLVIEW_VERT: r->w = MAX(r->w, wMax); r->h += rChld.h; break; } } } } static int SizeAllocate(void *_Nonnull p, const AG_SizeAlloc *_Nonnull a) { AG_Scrollview *sv = p; AG_SizeReq rBar; AG_SizeAlloc aBar; int wTot, hTot; sv->r.w = a->w; sv->r.h = a->h; if (sv->hbar) { AG_WidgetSizeReq(sv->hbar, &rBar); aBar.w = a->w - rBar.h; aBar.h = rBar.h; aBar.x = 0; aBar.y = a->h - rBar.h; AG_WidgetSizeAlloc(sv->hbar, &aBar); sv->r.h -= aBar.h; sv->hBar = aBar.h; if (sv->r.h < 0) { sv->r.h = 0; } } else { sv->hBar = 0; } if (sv->vbar) { AG_WidgetSizeReq(sv->vbar, &rBar); aBar.w = rBar.w; aBar.h = a->h - rBar.w; aBar.x = a->w - rBar.w; aBar.y = 0; AG_WidgetSizeAlloc(sv->vbar, &aBar); sv->r.w -= aBar.w; sv->wBar = aBar.w; if (sv->r.w < 0) { sv->r.w = 0; } } else { sv->wBar = 0; } PlaceWidgets(sv, &wTot, &hTot); sv->xMax = wTot; sv->yMax = hTot; if (sv->hbar) { if ((sv->xMax - sv->r.w - sv->xOffs) < 0) sv->xOffs = MAX(0, sv->xMax - sv->r.w); } if (sv->vbar) { if ((sv->yMax - sv->r.h - sv->yOffs) < 0) sv->yOffs = MAX(0, sv->yMax - sv->r.h); } #if 0 if (a->w >= (wTot - sv->xOffs)) { sv->xOffs = wTot - a->w; if (sv->xOffs < 0) { sv->xOffs = 0; } } if (a->h >= (hTot - sv->yOffs)) { sv->yOffs = hTot - a->h; if (sv->yOffs < 0) { sv->yOffs = 0; } } #endif return (0); } static void Draw(void *_Nonnull p) { static void (*pfBox[])(void *, const AG_Rect *, const AG_Color *) = { ag_draw_rect_noop, /* NONE */ ag_draw_box_raised, /* BOX */ ag_draw_box_sunk, /* WELL */ ag_draw_rect /* PLAIN */ }; AG_Scrollview *sv = p; AG_Widget *chld; AG_Rect r = WIDGET(sv)->r; int x2,y2; #ifdef AG_DEBUG if (sv->style >= AG_SCROLLVIEW_STYLE_LAST) AG_FatalError("style"); #endif pfBox[sv->style](sv, &r, &WCOLOR(sv, BG_COLOR)); if (sv->hbar) { AG_WidgetDraw(sv->hbar); r.h -= HEIGHT(sv->hbar); } if (sv->vbar) { AG_WidgetDraw(sv->vbar); r.w -= WIDTH(sv->vbar); } x2 = WIDGET(sv)->rView.x2 - sv->wBar; y2 = WIDGET(sv)->rView.y2 - sv->hBar; r.x++; r.y++; r.w -= 2; r.h -= 2; /* * TODO determine earlier if there are partially visible widgets * and a clipping rectangle is really needed. */ AG_PushClipRect(sv, &r); OBJECT_FOREACH_CHILD(chld, sv, ag_widget) { if (!(chld->flags & AG_WIDGET_VISIBLE) || chld == WIDGET(sv->hbar) || chld == WIDGET(sv->vbar)) { continue; } AG_WidgetDraw(chld); if (chld->rView.x2 > x2) { chld->rSens.w = x2 - WIDGET(chld)->rView.x1; } else { chld->rSens.w = chld->w; } chld->rSens.x2 = chld->rSens.x1 + chld->rSens.w; if (chld->rView.y2 > y2) { chld->rSens.h = y2 - WIDGET(chld)->rView.y1; } else { chld->rSens.h = chld->h; } chld->rSens.y2 = chld->rSens.y1 + chld->rSens.h; } AG_PopClipRect(sv); } AG_WidgetClass agScrollviewClass = { { "Agar(Widget:Scrollview)", sizeof(AG_Scrollview), { 1,0, AGC_SCROLLVIEW, 0xE02D }, Init, NULL, /* reset */ NULL, /* destroy */ NULL, /* load */ NULL, /* save */ NULL /* edit */ }, Draw, SizeRequest, SizeAllocate, MouseButtonDown, MouseButtonUp, MouseMotion, NULL, /* key_down */ NULL, /* key_up */ NULL, /* touch */ NULL, /* ctrl */ NULL /* joy */ }; #endif /* AG_WIDGETS */