rootlessScreen.c   [plain text]


/*
 * Screen routines for generic rootless X server
 */
/*
 * Copyright (c) 2001 Greg Parker. All Rights Reserved.
 * Copyright (c) 2002-2003 Torrey T. Lyons. All Rights Reserved.
 * Copyright (c) 2002 Apple Computer, Inc. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Except as contained in this notice, the name(s) of the above copyright
 * holders shall not be used in advertising or otherwise to promote the sale,
 * use or other dealings in this Software without prior written authorization.
 */


#ifdef HAVE_DIX_CONFIG_H
#include <dix-config.h>
#endif

#include "mi.h"
#include "scrnintstr.h"
#include "gcstruct.h"
#include "pixmapstr.h"
#include "windowstr.h"
#include "propertyst.h"
#include "mivalidate.h"
#include "picturestr.h"
#include "colormapst.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#include "rootlessCommon.h"
#include "rootlessWindow.h"

/* In milliseconds */
#ifndef ROOTLESS_REDISPLAY_DELAY
#define ROOTLESS_REDISPLAY_DELAY 10
#endif

extern int RootlessMiValidateTree(WindowPtr pRoot, WindowPtr pChild,
                                  VTKind kind);
extern Bool RootlessCreateGC(GCPtr pGC);

// Initialize globals
static int rootlessGCPrivateKeyIndex;
DevPrivateKey rootlessGCPrivateKey = &rootlessGCPrivateKeyIndex;
static int rootlessScreenPrivateKeyIndex;
DevPrivateKey rootlessScreenPrivateKey = &rootlessScreenPrivateKeyIndex;
static int rootlessWindowPrivateKeyIndex;
DevPrivateKey rootlessWindowPrivateKey = &rootlessWindowPrivateKeyIndex;
static int rootlessWindowOldPixmapPrivateKeyIndex;
DevPrivateKey rootlessWindowOldPixmapPrivateKey = &rootlessWindowOldPixmapPrivateKeyIndex;


/*
 * RootlessUpdateScreenPixmap
 *  miCreateScreenResources does not like a null framebuffer pointer,
 *  it leaves the screen pixmap with an uninitialized data pointer.
 *  Thus, rootless implementations typically set the framebuffer width
 *  to zero so that miCreateScreenResources does not allocate a screen
 *  pixmap for us. We allocate our own screen pixmap here since we need
 *  the screen pixmap to be valid (e.g. CopyArea from the root window).
 */
void
RootlessUpdateScreenPixmap(ScreenPtr pScreen)
{
    RootlessScreenRec *s = SCREENREC(pScreen);
    PixmapPtr pPix;
    unsigned int rowbytes;

    pPix = (*pScreen->GetScreenPixmap)(pScreen);
    if (pPix == NULL) {
        pPix = (*pScreen->CreatePixmap)(pScreen, 0, 0, pScreen->rootDepth, 0);
        (*pScreen->SetScreenPixmap)(pPix);
    }

    rowbytes = PixmapBytePad(pScreen->width, pScreen->rootDepth);

    if (s->pixmap_data_size < rowbytes) {
        if (s->pixmap_data != NULL)
            xfree(s->pixmap_data);

        s->pixmap_data_size = rowbytes;
        s->pixmap_data = xalloc(s->pixmap_data_size);
        if (s->pixmap_data == NULL)
            return;

        memset(s->pixmap_data, 0xFF, s->pixmap_data_size);

        pScreen->ModifyPixmapHeader(pPix, pScreen->width, pScreen->height,
                                    pScreen->rootDepth,
                                    BitsPerPixel(pScreen->rootDepth),
                                    0, s->pixmap_data);
        /* ModifyPixmapHeader ignores zero arguments, so install rowbytes
           by hand. */
        pPix->devKind = 0;
    }
}


/*
 * RootlessCreateScreenResources
 *  Rootless implementations typically set a null framebuffer pointer, which
 *  causes problems with miCreateScreenResources. We fix things up here.
 */
static Bool
RootlessCreateScreenResources(ScreenPtr pScreen)
{
    Bool ret = TRUE;

    SCREEN_UNWRAP(pScreen, CreateScreenResources);

    if (pScreen->CreateScreenResources != NULL)
        ret = (*pScreen->CreateScreenResources)(pScreen);

    SCREEN_WRAP(pScreen, CreateScreenResources);

    if (!ret)
        return ret;

    /* Make sure we have a valid screen pixmap. */

    RootlessUpdateScreenPixmap(pScreen);

    return ret;
}


static Bool
RootlessCloseScreen(int i, ScreenPtr pScreen)
{
    RootlessScreenRec *s;

    s = SCREENREC(pScreen);

    // fixme unwrap everything that was wrapped?
    pScreen->CloseScreen = s->CloseScreen;

    if (s->pixmap_data != NULL) {
        xfree (s->pixmap_data);
        s->pixmap_data = NULL;
        s->pixmap_data_size = 0;
    }

    xfree(s);
    return pScreen->CloseScreen(i, pScreen);
}


static void
RootlessGetImage(DrawablePtr pDrawable, int sx, int sy, int w, int h,
                 unsigned int format, unsigned long planeMask, char *pdstLine)
{
    ScreenPtr pScreen = pDrawable->pScreen;
    SCREEN_UNWRAP(pScreen, GetImage);

    if (pDrawable->type == DRAWABLE_WINDOW) {
        int x0, y0, x1, y1;
        RootlessWindowRec *winRec;

        // Many apps use GetImage to sync with the visible frame buffer
        // FIXME: entire screen or just window or all screens?
        RootlessRedisplayScreen(pScreen);

        // RedisplayScreen stops drawing, so we need to start it again
        RootlessStartDrawing((WindowPtr)pDrawable);

        /* Check that we have some place to read from. */
        winRec = WINREC(TopLevelParent((WindowPtr) pDrawable));
        if (winRec == NULL)
            goto out;

        /* Clip to top-level window bounds. */
        /* FIXME: fbGetImage uses the width parameter to calculate the
           stride of the destination pixmap. If w is clipped, the data
           returned will be garbage, although we will not crash. */

        x0 = pDrawable->x + sx;
        y0 = pDrawable->y + sy;
        x1 = x0 + w;
        y1 = y0 + h;

        x0 = MAX (x0, winRec->x);
        y0 = MAX (y0, winRec->y);
        x1 = MIN (x1, winRec->x + winRec->width);
        y1 = MIN (y1, winRec->y + winRec->height);

        sx = x0 - pDrawable->x;
        sy = y0 - pDrawable->y;
        w = x1 - x0;
        h = y1 - y0;

        if (w <= 0 || h <= 0)
            goto out;
    }

    pScreen->GetImage(pDrawable, sx, sy, w, h, format, planeMask, pdstLine);

out:
    SCREEN_WRAP(pScreen, GetImage);
}


/*
 * RootlessSourceValidate
 *  CopyArea and CopyPlane use a GC tied to the destination drawable.
 *  StartDrawing/StopDrawing wrappers won't be called if source is
 *  a visible window but the destination isn't. So, we call StartDrawing
 *  here and leave StopDrawing for the block handler.
 */
static void
RootlessSourceValidate(DrawablePtr pDrawable, int x, int y, int w, int h)
{
    SCREEN_UNWRAP(pDrawable->pScreen, SourceValidate);
    if (pDrawable->type == DRAWABLE_WINDOW) {
        WindowPtr pWin = (WindowPtr)pDrawable;
        RootlessStartDrawing(pWin);
    }
    if (pDrawable->pScreen->SourceValidate) {
        pDrawable->pScreen->SourceValidate(pDrawable, x, y, w, h);
    }
    SCREEN_WRAP(pDrawable->pScreen, SourceValidate);
}

#ifdef RENDER

static void
RootlessComposite(CARD8 op, PicturePtr pSrc, PicturePtr pMask, PicturePtr pDst,
                  INT16 xSrc, INT16 ySrc, INT16  xMask, INT16  yMask,
                  INT16 xDst, INT16 yDst, CARD16 width, CARD16 height)
{
    ScreenPtr pScreen = pDst->pDrawable->pScreen;
    PictureScreenPtr ps = GetPictureScreen(pScreen);
    WindowPtr srcWin, dstWin, maskWin = NULL;

    if (pMask) {                        // pMask can be NULL
        maskWin = (pMask->pDrawable->type == DRAWABLE_WINDOW) ?
                  (WindowPtr)pMask->pDrawable :  NULL;
    }
    srcWin  = (pSrc->pDrawable->type  == DRAWABLE_WINDOW) ?
              (WindowPtr)pSrc->pDrawable  :  NULL;
    dstWin  = (pDst->pDrawable->type == DRAWABLE_WINDOW) ?
              (WindowPtr)pDst->pDrawable  :  NULL;

    // SCREEN_UNWRAP(ps, Composite);
    ps->Composite = SCREENREC(pScreen)->Composite;

    if (srcWin  && IsFramedWindow(srcWin))
        RootlessStartDrawing(srcWin);
    if (maskWin && IsFramedWindow(maskWin))
        RootlessStartDrawing(maskWin);
    if (dstWin  && IsFramedWindow(dstWin))
        RootlessStartDrawing(dstWin);

    ps->Composite(op, pSrc, pMask, pDst,
                  xSrc, ySrc, xMask, yMask,
                  xDst, yDst, width, height);

    if (dstWin  && IsFramedWindow(dstWin)) {
        RootlessDamageRect(dstWin, xDst, yDst, width, height);
    }

    ps->Composite = RootlessComposite;
    // SCREEN_WRAP(ps, Composite);
}


static void
RootlessGlyphs(CARD8 op, PicturePtr pSrc, PicturePtr pDst,
               PictFormatPtr maskFormat, INT16 xSrc, INT16 ySrc,
               int nlist, GlyphListPtr list, GlyphPtr *glyphs)
{
    ScreenPtr pScreen = pDst->pDrawable->pScreen;
    PictureScreenPtr ps = GetPictureScreen(pScreen);
    int x, y;
    int n;
    GlyphPtr glyph;
    WindowPtr srcWin, dstWin;

    srcWin = (pSrc->pDrawable->type == DRAWABLE_WINDOW) ?
             (WindowPtr)pSrc->pDrawable  :  NULL;
    dstWin = (pDst->pDrawable->type == DRAWABLE_WINDOW) ?
             (WindowPtr)pDst->pDrawable  :  NULL;

    if (srcWin && IsFramedWindow(srcWin)) RootlessStartDrawing(srcWin);
    if (dstWin && IsFramedWindow(dstWin)) RootlessStartDrawing(dstWin);

    //SCREEN_UNWRAP(ps, Glyphs);
    ps->Glyphs = SCREENREC(pScreen)->Glyphs;
    ps->Glyphs(op, pSrc, pDst, maskFormat, xSrc, ySrc, nlist, list, glyphs);
    ps->Glyphs = RootlessGlyphs;
    //SCREEN_WRAP(ps, Glyphs);

    if (dstWin && IsFramedWindow(dstWin)) {
        x = xSrc;
        y = ySrc;

        while (nlist--) {
            x += list->xOff;
            y += list->yOff;
            n = list->len;

            /* Calling DamageRect for the bounding box of each glyph is
               inefficient. So compute the union of all glyphs in a list
               and damage that. */

            if (n > 0) {
                BoxRec box;

                glyph = *glyphs++;

                box.x1 = x - glyph->info.x;
                box.y1 = y - glyph->info.y;
                box.x2 = box.x1 + glyph->info.width;
                box.y2 = box.y2 + glyph->info.height;

                x += glyph->info.xOff;
                y += glyph->info.yOff;

                while (--n > 0) {
                    short x1, y1, x2, y2;

                    glyph = *glyphs++;

                    x1 = x - glyph->info.x;
                    y1 = y - glyph->info.y;
                    x2 = x1 + glyph->info.width;
                    y2 = y1 + glyph->info.height;

                    box.x1 = MAX (box.x1, x1);
                    box.y1 = MAX (box.y1, y1);
                    box.x2 = MAX (box.x2, x2);
                    box.y2 = MAX (box.y2, y2);

                    x += glyph->info.xOff;
                    y += glyph->info.yOff;
                }

                RootlessDamageBox(dstWin, &box);
            }
            list++;
        }
    }
}

#endif // RENDER


/*
 * RootlessValidateTree
 *  ValidateTree is modified in two ways:
 *   - top-level windows don't clip each other
 *   - windows aren't clipped against root.
 *  These only matter when validating from the root.
 */
static int
RootlessValidateTree(WindowPtr pParent, WindowPtr pChild, VTKind kind)
{
    int result;
    RegionRec saveRoot;
    ScreenPtr pScreen = pParent->drawable.pScreen;

    SCREEN_UNWRAP(pScreen, ValidateTree);
    RL_DEBUG_MSG("VALIDATETREE start ");

    // Use our custom version to validate from root
    if (IsRoot(pParent)) {
        RL_DEBUG_MSG("custom ");
        result = RootlessMiValidateTree(pParent, pChild, kind);
    } else {
        HUGE_ROOT(pParent);
        result = pScreen->ValidateTree(pParent, pChild, kind);
        NORMAL_ROOT(pParent);
    }

    SCREEN_WRAP(pScreen, ValidateTree);
    RL_DEBUG_MSG("VALIDATETREE end\n");

    return result;
}


/*
 * RootlessMarkOverlappedWindows
 *  MarkOverlappedWindows is modified to ignore overlapping
 *  top-level windows.
 */
static Bool
RootlessMarkOverlappedWindows(WindowPtr pWin, WindowPtr pFirst,
                              WindowPtr *ppLayerWin)
{
    RegionRec saveRoot;
    Bool result;
    ScreenPtr pScreen = pWin->drawable.pScreen;
    SCREEN_UNWRAP(pScreen, MarkOverlappedWindows);
    RL_DEBUG_MSG("MARKOVERLAPPEDWINDOWS start ");

    HUGE_ROOT(pWin);
    if (IsRoot(pWin)) {
        // root - mark nothing
        RL_DEBUG_MSG("is root not marking ");
        result = FALSE;
    }
    else if (! IsTopLevel(pWin)) {
        // not top-level window - mark normally
        result = pScreen->MarkOverlappedWindows(pWin, pFirst, ppLayerWin);
    }
    else {
        //top-level window - mark children ONLY - NO overlaps with sibs (?)
        // This code copied from miMarkOverlappedWindows()

        register WindowPtr pChild;
        Bool anyMarked = FALSE;
        void (* MarkWindow)() = pScreen->MarkWindow;

        RL_DEBUG_MSG("is top level! ");
        /* single layered systems are easy */
        if (ppLayerWin) *ppLayerWin = pWin;

        if (pWin == pFirst) {
            /* Blindly mark pWin and all of its inferiors.   This is a slight
             * overkill if there are mapped windows that outside pWin's border,
             * but it's better than wasting time on RectIn checks.
             */
            pChild = pWin;
            while (1) {
                if (pChild->viewable) {
                    if (REGION_BROKEN (pScreen, &pChild->winSize))
                        SetWinSize (pChild);
                    if (REGION_BROKEN (pScreen, &pChild->borderSize))
                        SetBorderSize (pChild);
                    (* MarkWindow)(pChild);
                    if (pChild->firstChild) {
                        pChild = pChild->firstChild;
                        continue;
                    }
                }
                while (!pChild->nextSib && (pChild != pWin))
                    pChild = pChild->parent;
                if (pChild == pWin)
                    break;
                pChild = pChild->nextSib;
            }
            anyMarked = TRUE;
            pFirst = pFirst->nextSib;
        }
        if (anyMarked)
            (* MarkWindow)(pWin->parent);
        result = anyMarked;
    }
    NORMAL_ROOT(pWin);
    SCREEN_WRAP(pScreen, MarkOverlappedWindows);
    RL_DEBUG_MSG("MARKOVERLAPPEDWINDOWS end\n");

    return result;
}

void expose_1 (WindowPtr pWin) {
    WindowPtr pChild;
    
    if (!pWin->realized)
        return;
    
    (*pWin->drawable.pScreen->PaintWindowBackground) (pWin, &pWin->borderClip,
                                                      PW_BACKGROUND);
    
    /* FIXME: comments in windowstr.h indicate that borderClip doesn't
     include subwindow visibility. But I'm not so sure.. so we may
     be exposing too much.. */
    
    miSendExposures (pWin, &pWin->borderClip,
                     pWin->drawable.x, pWin->drawable.y);
    
    for (pChild = pWin->firstChild; pChild != NULL; pChild = pChild->nextSib)
        expose_1 (pChild);
}

void
RootlessScreenExpose (ScreenPtr pScreen)
{
    expose_1 (WindowTable[pScreen->myNum]);
}


ColormapPtr
RootlessGetColormap (ScreenPtr pScreen)
{
  RootlessScreenRec *s = SCREENREC (pScreen);

  return s->colormap;
}

static void
RootlessInstallColormap (ColormapPtr pMap)
{
  ScreenPtr pScreen = pMap->pScreen;
  RootlessScreenRec *s = SCREENREC (pScreen);

  SCREEN_UNWRAP(pScreen, InstallColormap);

  if (s->colormap != pMap) {
    s->colormap = pMap;
    s->colormap_changed = TRUE;
    RootlessQueueRedisplay (pScreen);
  }

  pScreen->InstallColormap (pMap);

  SCREEN_WRAP (pScreen, InstallColormap);
}

static void
RootlessUninstallColormap (ColormapPtr pMap)
{
  ScreenPtr pScreen = pMap->pScreen;
  RootlessScreenRec *s = SCREENREC (pScreen);

  SCREEN_UNWRAP(pScreen, UninstallColormap);

  if (s->colormap == pMap)
    s->colormap = NULL;

  pScreen->UninstallColormap (pMap);

  SCREEN_WRAP(pScreen, UninstallColormap);
}

static void
RootlessStoreColors (ColormapPtr pMap, int ndef, xColorItem *pdef)
{
  ScreenPtr pScreen = pMap->pScreen;
  RootlessScreenRec *s = SCREENREC (pScreen);

  SCREEN_UNWRAP(pScreen, StoreColors);

  if (s->colormap == pMap && ndef > 0) {
    s->colormap_changed = TRUE;
    RootlessQueueRedisplay (pScreen);
  }

  pScreen->StoreColors (pMap, ndef, pdef);

  SCREEN_WRAP(pScreen, StoreColors);
}


static CARD32
RootlessRedisplayCallback(OsTimerPtr timer, CARD32 time, void *arg)
{
    RootlessScreenRec *screenRec = arg;

    if (!screenRec->redisplay_queued) {
        /* No update needed. Stop the timer. */

        screenRec->redisplay_timer_set = FALSE;
        return 0;
    }

    screenRec->redisplay_queued = FALSE;

    /* Mark that we should redisplay before waiting for I/O next time */
    screenRec->redisplay_expired = TRUE;

    /* Reinstall the timer immediately, so we get as close to our
       redisplay interval as possible. */

    return ROOTLESS_REDISPLAY_DELAY;
}


/*
 * RootlessQueueRedisplay
 *  Queue a redisplay after a timer delay to ensure we do not redisplay
 *  too frequently.
 */
void
RootlessQueueRedisplay(ScreenPtr pScreen)
{
    RootlessScreenRec *screenRec = SCREENREC(pScreen);

    screenRec->redisplay_queued = TRUE;

    if (screenRec->redisplay_timer_set)
        return;

    screenRec->redisplay_timer = TimerSet(screenRec->redisplay_timer,
                                          0, ROOTLESS_REDISPLAY_DELAY,
                                          RootlessRedisplayCallback,
                                          screenRec);
    screenRec->redisplay_timer_set = TRUE;
}


/*
 * RootlessBlockHandler
 *  If the redisplay timer has expired, flush drawing before blocking
 *  on select().
 */
static void
RootlessBlockHandler(pointer pbdata, OSTimePtr pTimeout, pointer pReadmask)
{
    ScreenPtr pScreen = pbdata;
    RootlessScreenRec *screenRec = SCREENREC(pScreen);

    if (screenRec->redisplay_expired) {
        screenRec->redisplay_expired = FALSE;

        RootlessRedisplayScreen(pScreen);
    }
}


static void
RootlessWakeupHandler(pointer data, int i, pointer LastSelectMask)
{
    // nothing here
}


static Bool
RootlessAllocatePrivates(ScreenPtr pScreen)
{
    RootlessScreenRec *s;

    // no allocation needed for screen privates
    if (!dixRequestPrivate(rootlessGCPrivateKey, sizeof(RootlessGCRec)))
        return FALSE;

    s = xalloc(sizeof(RootlessScreenRec));
    if (! s) return FALSE;
    SETSCREENREC(pScreen, s);

    s->pixmap_data = NULL;
    s->pixmap_data_size = 0;

    s->redisplay_timer = NULL;
    s->redisplay_timer_set = FALSE;

    return TRUE;
}


static void
RootlessWrap(ScreenPtr pScreen)
{
    RootlessScreenRec *s = SCREENREC(pScreen);

#define WRAP(a) \
    if (pScreen->a) { \
        s->a = pScreen->a; \
    } else { \
        RL_DEBUG_MSG("null screen fn " #a "\n"); \
        s->a = NULL; \
    } \
    pScreen->a = Rootless##a

    WRAP(CreateScreenResources);
    WRAP(CloseScreen);
    WRAP(CreateGC);
    WRAP(CopyWindow);
    WRAP(GetImage);
    WRAP(SourceValidate);
    WRAP(CreateWindow);
    WRAP(DestroyWindow);
    WRAP(RealizeWindow);
    WRAP(UnrealizeWindow);
    WRAP(MoveWindow);
    WRAP(PositionWindow);
    WRAP(ResizeWindow);
    WRAP(RestackWindow);
    WRAP(ReparentWindow);
    WRAP(ChangeBorderWidth);
    WRAP(MarkOverlappedWindows);
    WRAP(ValidateTree);
    WRAP(ChangeWindowAttributes);
    WRAP(InstallColormap);
    WRAP(UninstallColormap);
    WRAP(StoreColors);

    WRAP(SetShape);

#ifdef RENDER
    {
        // Composite and Glyphs don't use normal screen wrapping
        PictureScreenPtr ps = GetPictureScreen(pScreen);
        s->Composite = ps->Composite;
        ps->Composite = RootlessComposite;
        s->Glyphs = ps->Glyphs;
        ps->Glyphs = RootlessGlyphs;
    }
#endif

    // WRAP(ClearToBackground); fixme put this back? useful for shaped wins?

#undef WRAP
}


/*
 * RootlessInit
 *  Called by the rootless implementation to initialize the rootless layer.
 *  Rootless wraps lots of stuff and needs a bunch of devPrivates.
 */
Bool RootlessInit(ScreenPtr pScreen, RootlessFrameProcsPtr procs)
{
    RootlessScreenRec *s;

    if (!RootlessAllocatePrivates(pScreen))
        return FALSE;

    s = SCREENREC(pScreen);

    s->imp = procs;
    s->colormap = NULL;
    s->redisplay_expired = FALSE;

    RootlessWrap(pScreen);

    if (!RegisterBlockAndWakeupHandlers(RootlessBlockHandler,
                                        RootlessWakeupHandler,
                                        (pointer) pScreen))
    {
        return FALSE;
    }

    return TRUE;
}

void RootlessUpdateRooted (Bool state) {
    int i;
    
    if (!state)
    {
        for (i = 0; i < screenInfo.numScreens; i++)
            RootlessDisableRoot (screenInfo.screens[i]);
    }
    else
    {
        for (i = 0; i < screenInfo.numScreens; i++)
            RootlessEnableRoot (screenInfo.screens[i]);
    }
}