Generated reference › API — ICoreEssentials/UI/Backends/Gtk4/Graphics
kind: generated#api#icoreessentials-ui-backends-gtk4-graphics

API — ICoreEssentials/UI/Backends/Gtk4/Graphics

The public contract of 5 header(s) under ICoreEssentials/UI/Backends/Gtk4/Graphics — 6 class/struct definition(s), 85 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.

ICoreGtk4GraphicsView.h#

ICoreEssentials/UI/Backends/Gtk4/Graphics/ICoreGtk4GraphicsView.h

Where the portable scene core meets a GtkWidget: the core says what to paint and in what order, and this file runs the item scene's draw walk down that list through the view's pan and zoom, turns pointer coordinates into scene coordinates, and turns the core's dirty region into repaint requests.

⚠ NOTHING ABOVE THE BACKEND ZONE MAY INCLUDE THIS. The peers are Graphics/ICoreGtk4ItemAccess.h, Painting/ICoreGtk4PaintAccess.h and Layouts/ICoreGtk4LayoutAccess.h.

⚠ THE SPLIT WITH THE CORE IS A CONTRACT, NOT AN INTERNAL DETAIL, and it is the AppKit seat's split adopted rather than re-derived:

THE CORE OWNS z-order, item flags, hit-testing, hover, drag, transforms, dirty-rect accumulation -- all in SCENE coordinates, with no

ICoreGtk4ViewBackdrop#

ICoreGtk4GraphicsView.h:50 · struct · 0 declaration(s)

The colour painted under the scene, over the whole damaged rectangle, before any item.

struct ICoreGtk4ViewBackdrop {
public:
    double red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0;
};
};

ICoreGtk4GraphicsView#

ICoreGtk4GraphicsView.h:54 · class · pImpl · 52 declaration(s)

class ICoreGtk4GraphicsView {
public:
    ICoreGtk4GraphicsView();
    ~ICoreGtk4GraphicsView();

    ICoreGtk4GraphicsView(const ICoreGtk4GraphicsView&) = delete;
    ICoreGtk4GraphicsView& operator=(const ICoreGtk4GraphicsView&) = delete;

    // The widget this seat paints into. Exposed because a parent adopts it and
    // the wrapper hands it out as its native handle; everything else goes
    // through this class.
    ICoreGtk4Widget& area();
    const ICoreGtk4Widget& area() const;

    // The scene being shown. NOT owned -- ICoreGraphicsScene owns it and a view
    // borrows it (that seat's own note says so). Null shows an empty viewport.
    //
    // ⚠ THE REPAINT PUMP IS INSTALLED HERE AND TAKEN OUT AGAIN ON THE WAY PAST.
    // A scene keeps ONE damaged hook, so setting a second scene releases the
    // first one's -- otherwise a scene nobody is showing would go on asking a
    // live view to repaint for it.
    void setScene(ICoreGtk4ItemScene* scene);
    [[nodiscard]] ICoreGtk4ItemScene* scene() const;

    // -- the view transform ---------------------------------------------------
    //
    // Scene coordinates map to view coordinates as
    //
    //     view = scene * zoom - scroll
    //
    // which is the composition `translate(-scroll) . scale(zoom)`. Scroll is
    // therefore in SCALED SCENE units, which is exactly what the wrapper's
    // horizontalScroll() has always meant -- see the range note below.
    void setZoom(double zoom);
    [[nodiscard]] double zoom() const;

    // Multiply the current zoom, which is what the wrapper's scaleBy() does.
    // Honours the anchor: see setZoomAnchoredUnderPointer.
    void scaleBy(double factor);

    // Zoom about a specific point of the VIEW, keeping the scene point under it
    // fixed.
    //
    // ⚠ AN EXPLICIT PARAMETER RATHER THAN A QUESTION TO THE WINDOW SYSTEM, and
    // that is A4.1's strict improvement adopted here: Qt gates AnchorUnderMouse
    // on `underMouse()`, which is false with no pointer over the window, so
    // every headless measurement of it runs the FALLBACK path. Taking the point
    // as an argument is what makes this backend's half testable with no display.
    void scaleByAnchoredAt(double factor, double viewX, double viewY);

    void setZoomAnchoredUnderPointer(bool anchored);
    [[nodiscard]] bool isZoomAnchoredUnderPointer() const;

    // Where the pointer is, in VIEW coordinates, and whether it is over this
    // view at all. Backs `ICoreGraphicsView::pointerViewportPos()` -- see its
    // note for why a shared caller must not build this out of
    // `mapViewportToGlobal()` and the OS cursor (`W10.108`). On THIS seat that
    // pairing is doubly wrong: mapViewportToGlobal answers in TOPLEVEL space,
    // as its own comment says, because Wayland will not tell a client where its
    // toplevel is.
    [[nodiscard]] bool lastPointerViewPos(double& viewX, double& viewY) const;

    // ⚠⚠ SCROLL IS A RANGED VALUE, NOT A FREE TRANSLATION, AND EVERY NUMBER
    // BELOW WAS MEASURED FROM A LIVE `QGraphicsView` ON THIS MACHINE rather than
    // copied from the sibling seat that measured it on another. Offscreen
    // platform, viewport 200x100, frame and both bars off:
    //
    //   scene 1001 wide                  ->  range [0, 801]
    //   scene 1001 wide starting at -500 ->  range [-500, 301]
    //   scene 51 wide                    ->  range [0, 0], scene CENTRED at 74
    //   scene 41 wide and 801 tall       ->  x [0,0] centred, y [0,701] scrolls
    //   zoom 2 over the 1001 scene       ->  range [0, 1802]
    //
    // ⚠ TWO OF THOSE ARE THE REVERSE OF THE OBVIOUS SPELLING, and the sibling
    // board records getting both wrong before measuring:
    //
    //   * THE MINIMUM IS NOT ZERO. A scroll value is the view's left (or top)
    //     edge in SCALED SCENE coordinates, so a scene starting at x = -500 has
    //     a range that starts at -500. Clamping to [0, max] makes the left half
    //     of such a scene unreachable, with no error anywhere.
    //   * AN AXIS THAT FITS IS CENTRED, NOT FLUSH. The range collapses to one
    //     value and that value centres what there is -- per AXIS, so a narrow
    //     tall scene is centred across while it scrolls down. A seat that left
    //     it flush puts every small canvas in the top-left corner of its
    //     viewport, which looks like a layout bug and is not one.
    //
    // ⚠ AND ONE MORE THAT ONLY A THIRD BACKEND WOULD NOTICE: the OTHER toolkit
    // clamps DESTRUCTIVELY. Scrolled to 700, growing the viewport 200 -> 900
    // clamps the value to 101, and shrinking it back to 200 leaves it at 101,
    // not 700 -- measured on the same view. This seat reproduces that, because
    // the clamp happens at every observation point and WRITES BACK.
    void setScroll(double x, double y);
    [[nodiscard]] double horizontalScroll() const;
    [[nodiscard]] double verticalScroll() const;

    // The legal interval on each axis. A minimum equal to its maximum means that
    // axis fits and does not scroll -- and the single value it reports is the
    // centred one.
    void scrollRange(double& minX, double& maxX, double& minY, double& maxY) const;

    // ⚠ THE FRACTION IS KEPT, WHICH IS A STATED DIVERGENCE FROM THE OTHER
    // TOOLKIT AND NOT AN OVERSIGHT. Qt's scroll bars are `int`-valued, so its
    // wrapper truncates on the way in (37.9 becomes 37) and its reported range
    // is rounded. There is no integer scroll bar here to round to, and the
    // AppKit seat took the same decision from the other side. The visible
    // consequence is that an L8.2 screenshot diff can differ by up to a pixel
    // after a fractional scroll; it is asserted in the rig rather than omitted,
    // so it cannot quietly widen.

    // The transform in force, scene -> view. Handed out because a caller that
    // positions a native child over the scene -- L4.3's proxy overlay -- needs
    // the same matrix this file paints with, and computing a second one from the
    // zoom and the scroll is how the two drift apart.
    [[nodiscard]] ICoreSceneTransform viewTransform() const;

    // -- the two conversions --------------------------------------------------
    //
    // ⚠ EVERY CORE CALL TAKES SCENE COORDINATES AND EVERY TOOLKIT EVENT ARRIVES
    // IN VIEW COORDINATES, so one of these is the first line of almost every
    // handler below. Public because the wrapper's mapToScene()/mapFromScene()
    // are, and because a rig that could not ask would have to reimplement them.
    [[nodiscard]] ICoreScenePoint mapToScene(double viewX, double viewY) const;
    [[nodiscard]] ICoreScenePoint mapFromScene(const ICoreScenePoint& scenePoint) const;
    [[nodiscard]] ICoreSceneRect mapToScene(const ICoreSceneRect& viewRect) const;
    [[nodiscard]] ICoreSceneRect mapFromScene(const ICoreSceneRect& sceneRect) const;

    // Put `scenePoint` in the middle of the viewport -- centerViewOn(). Clamped,
    // so centring on a corner of the scene stops at the edge exactly as the
    // other toolkit's does (measured: centerOn(0,0) over a 1001 scene lands at
    // scroll 0, not at -100).
    void centerOn(const ICoreScenePoint& scenePoint);

    // The viewport, in the view's own coordinates. The whole widget: this
    // backend's view has no frame and no scroll bars to inset for.
    void viewportSize(double& width, double& height) const;

    // -- painting -------------------------------------------------------------

    // ⚠ THE FLAG SETS THE CONTEXT'S DEFAULT AND AN ITEM MAY STILL OVERRIDE IT,
    // measured rather than assumed: ICoreGtk4ItemScene's walk builds one
    // ICorePainter per item and the object tier's content thunk calls
    // setAntialiasing(true) on it, which reaches the same `cairo_t`. That is the
    // same layering the other toolkit has -- a view render hint that a
    // QGraphicsItem::paint() is free to change -- so it is reproduced rather
    // than fought.
    void setAntialiased(bool antialiased);
    [[nodiscard]] bool isAntialiased() const;

    void setBackdrop(const ICoreGtk4ViewBackdrop& backdrop);
    [[nodiscard]] ICoreGtk4ViewBackdrop backdrop() const;

    // Paint the backdrop and then the scene, into a context in VIEW coordinates.
    // Returns how many items were drawn.
    //
    // ⚠ THE DAMAGED RECTANGLE IS AN INPUT, NOT AN ASSUMPTION. GTK hands the
    // snapshot the whole widget and this seat is given the same by its paint
    // hook, but a rig -- and L4.4's dirty-region pass -- needs to paint a strip
    // and prove the culling, so the region is a parameter with the whole
    // viewport as its default.
    int draw(cairo_t* cr, double width, double height) const;
    int draw(cairo_t* cr, const ICoreSceneRect& damagedViewRect) const;

    // The draw order this seat would run for a damaged VIEW rectangle: the
    // culling and the composed transforms, without painting. Exists so a rig can
    // assert what WOULD be drawn without reading pixels back, and so L4.3's
    // proxy overlay can position native children with the same matrices.
    void drawOrderForViewRect(const ICoreSceneRect& damagedViewRect,
                              std::vector<ICoreSceneDrawCommand>& out) const;

    // -- native children over the scene (L4.3) --------------------------------
    //
    // ⚠⚠ AN ITEM CANNOT CARRY A REAL CONTROL INTO A SCENE ON ANY OF THE THREE
    // NATIVE BACKENDS, AND WHAT REPLACES IT IS AN OVERLAY THIS SEAT KEEPS IN
    // STEP. `QGraphicsProxyWidget` hands its widget to the scene, which paints
    // it through the same transform, clips it with the same clip and routes
    // events to it through the scene graph. There is no such transport here: a
    // `GtkWidget` draws itself, into its own place in its parent's child list.
    // So the control stays a CHILD OF THIS VIEW'S AREA and this seat moves it to
    // wherever the item would have been drawn.
    //
    // ⚠⚠ AND THIS BACKEND REPRODUCES TWO OF THE THREE THINGS THE APPKIT SEAT
    // RECORDS AS COSTS, WHICH IS WHY THE OVERLAY IS NOT SIMPLY THE CONTROL.
    // Each overlay is wrapped in a `GtkFixed` this seat owns, and the two
    // capabilities are the wrapper's:
    //
    //   * THE CONTENT SCALES WITH THE ZOOM. `gtk_fixed_set_child_transform()`
    //     takes a `GskTransform`, so the control is ALLOCATED at its item-space
    //     size and RENDERED under the item's full view matrix -- measured: a
    //     `GtkEntry` allocated 120x34 reports bounds of 240x68 under a scale of
    //     2, and `gtk_widget_compute_point()` maps a press back through it. The
    //     AppKit peer states the opposite ("the content does not scale with
    //     zoom") and defers the fix to its own A4.4; nothing is deferred here,
    //     because the toolkit has the verb.
    //   * A CLIP THE SCENE APPLIES TO THE ITEM IS APPLIED TO THE OVERLAY. The
    //     draw command already carries the composed clip in view coordinates, so
    //     the host's frame is the INTERSECTION and the child transform absorbs
    //     the difference, under `GTK_OVERFLOW_HIDDEN`.
    //
    // ⚠ THE ONE COST THAT REMAINS IS Z-ORDER, and it is a real limit rather
    // than an omission: GTK snapshots a widget's own body BEFORE its children,
    // so every overlay is above everything this seat paints, and a z value
    // orders an overlay only against another overlay.
    //
    // `setItemOverlay` adopts `control` into this view; `removeItemOverlay`
    // takes it back out. NEITHER OWNS IT -- the proxy wrapper above does, and
    // the `GtkFixed` between them is this seat's.
    void setItemOverlay(const ICoreSceneItemId& item, GtkWidget* control);
    void removeItemOverlay(const ICoreSceneItemId& item);
    [[nodiscard]] int overlayCount() const;

    // Put every overlay where its item currently is.
    //
    // ⚠ RUN ON EVERY PAINT AND ALSO PUBLIC, AND BOTH HALVES ARE LOAD-BEARING.
    // The paint call is what keeps an overlay glued to an item being ANIMATED;
    // the public call is for a caller that has just MOVED an item and wants to
    // read the control's position back before anything repaints.
    void syncOverlays();

    // Run before every sync, so the overlay tier can settle registrations it had
    // no way to make earlier.
    //
    // ⚠ IT EXISTS BECAUSE THE MOMENT A PROXY ACQUIRES A VIEW REACHES THE PROXY
    // THROUGH NOTHING: its parent item is put into a scene, the scene is given
    // to a view, and the attach mints the proxy a FRESH id -- three things that
    // all happen without the proxy being called. ⚠ AND IT IS A HOOK RATHER THAN
    // A CALL SO THAT THE DEPENDENCY STAYS ONE-WAY: the overlay tier includes
    // this file and this file includes nothing of it.
    static void setOverlayResolveHook(std::function<void()> hook);

    // Where the last sync put `item`'s overlay, in this view's coordinates, and
    // whether it was shown at all. False for an item with no overlay.
    //
    // ⚠ READ FROM THIS SEAT'S RECORD, NOT FROM THE WIDGET'S FRAME, so a rig can
    // tell "the seat decided to hide it" from "the seat never looked at it" -- a
    // hidden widget keeps whatever frame it had, so the frame alone answers
    // neither question.
    bool overlayPlacementForTest(const ICoreSceneItemId& item,
                                 ICoreSceneRect& frame, bool& shown) const;

    // The scale the last sync gave `item`'s overlay -- the capability the
    // sibling seat does not have, so a rig that only read frames could not tell
    // this seat from one that ignored the zoom.
    bool overlayScaleForTest(const ICoreSceneItemId& item, double& scale) const;

    // The view currently showing `scene`, or nullptr.
    //
    // ⚠ A SCENE DOES NOT KNOW ITS VIEWS AND AN OVERLAY BELONGS TO EXACTLY ONE
    // OF THEM. A proxy is built with a parent ITEM and never sees a view, so
    // something has to make the connection; having the proxy walk every live
    // view puts the same lookup in a file with no business knowing views exist.
    // ⚠ ONE VIEW PER SCENE IS A LIMIT AND NOT AN INVARIANT -- the same limit
    // setDamagedHook already has, one member down -- and the honest shape is to
    // answer the LAST view given this scene.
    static ICoreGtk4GraphicsView* viewShowing(const ICoreGtk4ItemScene* scene);

    // Whether `view` is still alive. ⚠ THE OVERLAY TIER HOLDS A RAW POINTER TO
    // ITS HOST SO THAT LEAVING IS SYMMETRICAL WITH ARRIVING, and a lookup by
    // scene cannot answer "which view did I give it to". A view adds itself here
    // on construction and takes itself out on destruction, so this is a question
    // about this process's live views rather than a guess about an address.
    static bool isLive(const ICoreGtk4GraphicsView* view);

    // -- the repaint path -----------------------------------------------------
    //
    // ⚠ THE CORE ACCUMULATES DAMAGE IN SCENE COORDINATES AND CLEARS IT WHEN
    // ASKED; the toolkit wants view rectangles. This is the one call that
    // crosses between them. Returns how many rectangles were requested; zero
    // means the scene had no damage, not that the call failed.
    int flushDirtyRegion();

    // ⚠ NO POSTED SCHEDULER HERE, UNLIKE THE APPKIT PEER, AND THE REASON IS
    // MEASURED. That seat coalesces flushes through a posted callback so a
    // mutation batch cannot repaint half way through itself. On GTK4 a repaint
    // REQUEST is already deferred: `gtk_widget_queue_draw_area()` records an
    // invalidation and returns, and nothing is painted until the frame clock
    // runs, which cannot happen inside a mutation because the mutation holds the
    // main loop. So this seat flushes straight from the damage hook; the rig
    // pins that a flush paints nothing by counting paint-hook calls across one.
    void flushedRectsForTest(std::vector<ICoreSceneRect>& out) const;
    [[nodiscard]] int flushCountForTest() const;

    // How wide the fringe around each dirty rect is, in VIEW pixels, at the
    // current zoom.
    //
    // ⚠ THE CORE EXPANDS ITS RECTS BY ONE SCENE PIXEL and says so; that covers a
    // pen of width <= 2 at zoom 1. It cannot cover more, because it does not
    // know the zoom -- one scene pixel is a fraction of a device pixel zoomed
    // out and many zoomed in. Scaling that fringe is this file's job, which is
    // why this is a function and not a constant.
    [[nodiscard]] double dirtyFringeInViewPixels() const;

    // -- input ----------------------------------------------------------------
    //
    // ⚠ THESE ARE THE PRODUCTION PATH AND NOT A TEST CONVENIENCE. The seat
    // installs its own hooks on the widget base and drives the core from them --
    // but the base holds ONE hook per input, so a wrapper that wants to see a
    // press first (ICoreGraphicsView::mousePressed(), which answers "handled?"
    // and may consume the gesture) REPLACES the seat's hook rather than sitting
    // in front of it, and hands back what it declined through these.
    void deliverMouseMove(double viewX, double viewY, ICoreMouseButtons buttons,
                          ICoreKeyModifiers modifiers);
    void deliverMousePress(double viewX, double viewY, ICoreMouseButton button,
                           ICoreKeyModifiers modifiers, int pressCount);
    void deliverMouseRelease(double viewX, double viewY, ICoreMouseButton button,
                             ICoreKeyModifiers modifiers);

    // A wheel event in this tree's units -- eighths of a degree, 120 per detent,
    // as Events/ICoreGtk4EventMap.h converts GDK's to.
    //
    // ⚠ HOW FAR ONE DETENT SCROLLS IS MEASURED, NOT CHOSEN: the other toolkit
    // moves `wheelScrollLines()` (3) x the scroll bar's single step, and a
    // QGraphicsView sets that step to a TWENTIETH of its viewport -- read off a
    // live view, viewport 186x86 gave steps of 9 and 4, and one detent moved it
    // 12 pixels. So a detent is 3/20 of the viewport, which this reproduces
    // exactly and which scales with the widget as the other toolkit's does.
    void deliverWheel(double viewX, double viewY, double deltaX, double deltaY,
                      ICoreKeyModifiers modifiers);

    // The pointer left the widget: hover is cleared, which is the one transition
    // no mouse position can express.
    void deliverPointerLeft();

    // With `zooms` on, a wheel with no modifier zooms about the pointer; with it
    // off it scrolls. Off by default, which is what a plain QGraphicsView does.
    void setWheelZooms(bool zooms);
    [[nodiscard]] bool wheelZooms() const;

    // What the core decided, handed out so the wrapper can act on it without
    // repeating the hit test.
    void setHoverChangedHook(std::function<void(const ICoreSceneHoverChange&)> hook);
    void setPressedHook(std::function<void(const ICoreScenePressResult&,
                                           const ICoreScenePoint&)> hook);
    void setDraggedHook(std::function<void(const ICoreSceneDragResult&,
                                           const ICoreScenePoint&)> hook);
    void setReleasedHook(std::function<void(const ICoreSceneReleaseResult&,
                                            const ICoreScenePoint&)> hook);

    // -- what the rig cannot otherwise see ------------------------------------
    [[nodiscard]] int itemsPaintedForTest() const;
    void resetItemsPaintedForTest();

    // The last item each kind of event was DELIVERED to, which is the half no
    // geometry check can reach: the core answers "item 7" whether or not
    // anything called item 7's hook.
    [[nodiscard]] int pointerEventsDeliveredForTest() const;
    void resetPointerEventsDeliveredForTest();

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

ICoreGtk4ItemAccess.h#

ICoreEssentials/UI/Backends/Gtk4/Graphics/ICoreGtk4ItemAccess.h

Backend-internal access to the three things an item does NOT publish: the scene it is in, which node of that scene it is, and how a ROOT item is put into one.

⚠ NOTHING ABOVE THE BACKEND ZONE MAY INCLUDE THIS. The peers are Painting/ICoreGtk4PaintAccess.h, Painting/ICoreGtk4PathAccess.h and Layouts/ICoreGtk4LayoutAccess.h, and the argument is the same each time: a seat hands its own machinery what the public surface deliberately does not.

⚠ WITHOUT THESE, AN ITEM TREE CANNOT BE DRAWN AT ALL, which is why they are part of the seat rather than a later convenience. ICoreGraphicsItem's whole public surface is about ONE item; drawing happens over a whole scene, and the object that owns the scene is reachable from any attached node and nowhere else. The view and the scene wrapper are the callers this exists for.

Declares no class of its own — see the file.

ICoreGtk4ItemScene.h#

ICoreEssentials/UI/Backends/Gtk4/Graphics/ICoreGtk4ItemScene.h

The scene an ICoreGraphicsItem is painted from on this backend: one ICoreScene from the portable core, plus the one thing that core deliberately does not hold -- what each item LOOKS like and how it draws itself.

⚠ NOTHING ABOVE THE BACKEND ZONE MAY INCLUDE THIS. It is the peer of Painting/ICoreGtk4PaintAccess.h and Layouts/ICoreGtk4LayoutAccess.h: a seat handing its own machinery what the public surface has no verb for.

⚠ THE NODE IS AUTHORITATIVE AND THE CORE IS A PROJECTION OF IT (see Graphics/ICoreGtk4SceneNode.h). An item that is in no scene is still fully usable -- setWidth() before the item is ever parented is what every construction site in this tree does -- so each wrapper's Impl holds its own state and PUSHES it into the core on attach and on every change while attached. Reading a value back never depends on being in a scene.

ICoreGtk4ItemVisual#

ICoreGtk4ItemScene.h:55 · struct · 0 declaration(s)

What one item looks like, read from the live wrapper at draw time rather than cached.

struct ICoreGtk4ItemVisual {
public:
    bool hasFill = false;
    int fillR = 0, fillG = 0, fillB = 0, fillA = 255;

    bool hasBorder = false;
    int borderR = 0, borderG = 0, borderB = 0, borderA = 255;

    double borderWidth = 2.0;
    double cornerRadius = 5.0;
};
};

ICoreGtk4ItemScene#

ICoreGtk4ItemScene.h:75 · class · pImpl · 15 declaration(s)

class ICoreGtk4ItemScene {
public:
    ICoreGtk4ItemScene();
    ~ICoreGtk4ItemScene();

    ICoreGtk4ItemScene(const ICoreGtk4ItemScene&) = delete;
    ICoreGtk4ItemScene& operator=(const ICoreGtk4ItemScene&) = delete;

    ICoreScene& scene();
    const ICoreScene& scene() const;

    // Attach the appearance and the content hook to an id already in the scene.
    // Re-binding the same id replaces both.
    void bind(const ICoreSceneItemId& id,
              ICoreGtk4ItemVisualFn visual,
              ICoreGtk4ItemContentFn content);

    void unbind(const ICoreSceneItemId& id);
    bool isBound(const ICoreSceneItemId& id) const;
    unsigned int boundCount() const;

    // Ask every node in this scene whether its wrapper still agrees with the
    // bounds the core holds, and push the ones that do not.
    //
    // ⚠⚠ THE VIEW DRIVES THIS, AND ITS THREE MOMENTS ARE A PAINT, A HIT TEST
    // AND AN OVERLAY PLACEMENT -- see ICoreGtk4SceneNode::reconcileBounds for
    // WHY the answer is pulled rather than pushed, and what it cost while
    // nothing pulled it.
    void reconcileBounds();

    // Draw the whole scene back to front into `cr`.
    //
    // Returns the number of items DRAWN. An entry with no binding is skipped and
    // not counted -- an item exists in the core for the instant between being
    // added and being bound, and drawing a half-registered item would be worse
    // than drawing nothing.
    //
    // ⚠ THE CONTEXT IS ASSUMED TO BE IN VIEW SPACE ON ENTRY and is left exactly
    // as it was found: each command is bracketed by cairo_save()/cairo_restore(),
    // so a view's pan and zoom survive the walk and no item can leak a clip or a
    // transform onto the next.
    int draw(cairo_t* cr) const;

    // Draw one command. The unit the suite drives, and what a dirty-region walk
    // calls after filtering the list itself.
    bool drawCommand(const ICoreSceneDrawCommand& command, cairo_t* cr) const;

    // -- the repaint pump (L4.1) ---------------------------------------------
    //
    // ⚠⚠ A DIRTY CORE REPAINTS NOTHING. The scene core accumulates damage in
    // scene coordinates and waits to be asked for it; turning that into a
    // toolkit invalidation needs a view, and until one existed nothing in this
    // zone did it. `ICoreGtk4SceneNode::markDirty()` -- which every mutation in
    // every seat here goes through -- calls notifyDamaged() right after marking
    // the core, and the VIEW showing this scene is what installs the hook.
    //
    // ⚠ ONE HOOK, AND A SCENE NOBODY IS SHOWING HAS NONE. That is a LIMIT and
    // not an invariant, stated rather than hidden: the other toolkit shows one
    // scene in many views and schedules all of them. Nothing in this tree does
    // (measured: one scene per view at all three construction sites), and the
    // honest shape is to serve the last view given this scene rather than to
    // pretend the question cannot arise.
    void setDamagedHook(std::function<void()> hook);
    void notifyDamaged() const;

    // ⚠⚠ A TOKEN THAT DIES WITH THIS SCENE, AND IT IS NOT DEFENSIVE
    // PROGRAMMING -- THE ORDER IT GUARDS IS THE ORDER EVERY TEARDOWN TAKES.
    // The scene owns the items and normally outlives the view, which is why the
    // view holds a RAW pointer to it. But "normally" is not "always": a scene
    // and a view declared in the same scope are destroyed in reverse
    // declaration order, so a view declared FIRST outlives its scene -- and its
    // destructor, which has to take its repaint hook back out, then writes into
    // a destroyed std::function. Found by this row's own rig on its first run,
    // as a segfault inside std::swap with the scene already gone.
    //
    // A weak_ptr to this is what a view holds beside the raw pointer: expired
    // means "the scene went first", and the view answers null from then on
    // rather than touching it.
    [[nodiscard]] std::weak_ptr<const int> lifetime() const;

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

File-scope declarations#

using ICoreGtk4ItemVisualFn = std::function<ICoreGtk4ItemVisual()>;

// The paintContent() hook, already bound to its owner.
// 
// ⚠ IT IS A THUNK AND NOT A POINTER TO THE WRAPPER, because paintContent() is
// PROTECTED. Only ICoreGraphicsItem::Impl -- a member of the class -- may call
// it, so the call has to be manufactured on that side and handed here.
using ICoreGtk4ItemContentFn = std::function<void(ICorePainter&)>;

ICoreGtk4SceneNode.h#

ICoreEssentials/UI/Backends/Gtk4/Graphics/ICoreGtk4SceneNode.h

ICoreGtk4ScenePointerEvent#

ICoreGtk4SceneNode.h:97 · struct · 0 declaration(s)

struct ICoreGtk4ScenePointerEvent {
public:
    ICoreGtk4ScenePointerKind kind = ICoreGtk4ScenePointerKind::Pressed;

    ICoreScenePoint scenePoint;    // ICoreMouseEvent::scenePos
    ICoreScenePoint itemPoint;     // the item's OWN coordinates -- ::pos
    ICoreScenePoint globalPoint;   // toplevel space, this backend's "screen"

    ICoreMouseButton button = ICoreMouseButton::None;
    ICoreMouseButtons buttons = 0;
    ICoreKeyModifiers modifiers = 0;

    // Wheel travel in eighths of a degree, for WheelScrolled and zero for every
    // other kind -- the unit ICoreWheelEvent publishes, so the seat that builds
    // the event passes them straight through.
    double deltaX = 0.0;
    double deltaY = 0.0;
};
};

ICoreGtk4SceneNode#

ICoreGtk4SceneNode.h:115 · struct · 18 declaration(s)

struct ICoreGtk4SceneNode {
public:
    ICoreGtk4SceneNode() = default;
    virtual ~ICoreGtk4SceneNode();

    ICoreGtk4SceneNode(const ICoreGtk4SceneNode&) = delete;
    ICoreGtk4SceneNode& operator=(const ICoreGtk4SceneNode&) = delete;

    // Null until the item is put into a scene, directly or by being parented to
    // something already in one. NULL IS A NORMAL ANSWER, not an error.
    ICoreGtk4ItemScene* host = nullptr;
    ICoreSceneItemId id;

    // ⚠⚠ THE THEME SUBSCRIPTION'S LIFETIME, AND IT WAS THE MISSING HALF OF A
    // DECISION L6.1 WROTE DOWN AND LEFT OPEN. `ICoreThemeBinding::subscribe()`
    // has an ICoreNativeItem overload; on this backend it ran `apply()` once and
    // subscribed to nothing, because a subscription needs something to be
    // severed by and the graphics tier did not exist. It does now, and the thing
    // that dies with the wrapper is this node -- so the scope lives here and the
    // overload is real. **33 item-tier classes subscribe through that overload**
    // (measured across src/), so what the gap cost was every block face, port
    // label and config dialog keeping its old colours through a Light/Dark
    // switch: recoloured in memory, unchanged on screen.
    //
    // ⚠ DECLARED BEFORE THE TREE MEMBERS SO IT IS DESTROYED AFTER THEM. A slot
    // delivered while the node was half torn down is the one thing that could
    // reach a wrapper mid-destruction, and destruction order is reverse
    // declaration order.
    ICoreSignalScope themeScope;

    // The scene TREE, which exists whether or not either end is in a scene.
    ICoreGtk4SceneNode* parent = nullptr;
    std::vector<ICoreGtk4SceneNode*> children;

    // -- the operations, one implementation for every seat -------------------

    // Put this node and its subtree into `newHost` under `parentNode` (null for
    // a root). No-op when already attached or when the host is null.
    void attachTo(ICoreGtk4ItemScene* newHost, ICoreGtk4SceneNode* parentNode);

    // Leave the scene, this node only. Children are handled by detachSubtree.
    void detachFromScene();
    void detachSubtree();

    // Unlink from the current parent's child list; the tree only, no scene.
    void detachFromParent();

    // Re-parent. Refuses self-parenting and cycles, migrates in place when both
    // ends are already in the SAME scene, and otherwise detaches the subtree and
    // re-attaches it under the new parent.
    //
    // ⚠ A NULL PARENT MAKES THIS NODE A ROOT OF ITS OWN TREE, NOT AN ITEM IN NO
    // TREE -- which is what an unparented, unscened item is on the toolkit,
    // where it still carries a position, a z and a visibility, and the families
    // here set all three before adding themselves to anything.
    void setParentNode(ICoreGtk4SceneNode* newParent);

    // What every seat's ~Impl calls FIRST. Children become usable roots rather
    // than going hollow; then this node leaves its parent and its scene.
    void teardownNode();

    // The wrapper above this node WHEN IT IS AN ICoreGraphicsObject, and null
    // for every other item type.
    //
    // ⚠ A VIRTUAL, NOT A dynamic_cast AND NOT A REGISTRY. The AppKit seat
    // answers the same question through a map keyed on the node, because its
    // node is a plain struct with no vtable to hang the question on. This one
    // already has one, so the answer costs a slot instead of a hash lookup and a
    // process-lifetime table's teardown order. Declining by default is what
    // makes ICoreGraphicsItem's answer -- null -- the same honest one a foreign
    // toolkit item gives on the Qt seat.
    virtual ICoreGraphicsObject* asGraphicsObject();

    // The wrapper above this node WHEN IT IS AN ICoreGraphicsText, and null for
    // every other item type.
    //
    // ⚠ A THIRD ITEM HIERARCHY, NOT A SUBCLASS OF EITHER OTHER ONE, which is
    // what makes a second question necessary rather than a widening of the one
    // above. ICoreGraphicsText.h says so at its top: the text tier could not
    // even be PASSED as a parent to a converted item until it was given
    // ICoreNativeItem, precisely because it derives from neither
    // ICoreGraphicsItem nor ICoreGraphicsObject.
    //
    // ⚠ THE CALLER IS FOCUS HAND-OVER AND THERE IS NO OTHER WAY TO WRITE IT.
    // The core moves the focus as DATA -- it calls nothing -- so the wrapper
    // that is losing the focus would never hear focusLosing(), and every label
    // in this tier commits its text from inside that hook. Answering "which
    // wrapper is scene item N" is what turns the core's id back into an object
    // whose virtuals can be called, and icoreGtk4SceneNodeOf() below is the
    // first half of it.
    virtual ICoreGraphicsText* asGraphicsText();

    // The cursor shape this item asks for, when it asks for one at all.
    //
    // ⚠ A VIRTUAL ON THE NODE RATHER THAN A READ OF THE SEAT'S FIELD, AND THE
    // REASON IS ACCESS CONTROL RATHER THAN TASTE. The shape is held by
    // ICoreGraphicsObject::Impl, which is declared in that class's PRIVATE
    // section -- a free accessor may not name the type, so it cannot reach the
    // member however it is spelled. Dispatching through the base it CAN name is
    // the same manoeuvre attaching a root needed, one member over.
    //
    // False for an item that has no cursor, and for ICoreGraphicsItem, which has
    // no such capability at all.
    virtual bool cursorShape(ICoreCursorShape& shapeOut) const;

    // Deliver one pointer event to THIS item's own hooks. False when the item
    // did not handle it -- and false by default, which is the honest answer for
    // ICoreGraphicsItem, a family with no pointer hooks at all.
    //
    // ⚠ A VIRTUAL FOR cursorShape()'s REASON EXACTLY, one member over: the
    // hooks are protected, `Impl` is a member of the wrapper and may call them,
    // and nothing at namespace scope can even name `Impl`'s type.
    virtual bool deliverPointerEvent(const ICoreGtk4ScenePointerEvent& event);

    // ⚠⚠ RE-ASK THE WRAPPER FOR ITS BOUNDS AND PUSH THEM IF THE CORE DISAGREES.
    // THIS IS A PULL, AND UNTIL IT EXISTED A SUBCLASS THAT RESIZED ITSELF
    // THROUGH ITS OWN SETTER WAS INVISIBLE TO THE CORE.
    //
    // `prepareGeometryChange()` damages the OLD rectangle and pushes nothing --
    // its own comment says so and defers the gap to "a reconcile pass its VIEW
    // runs", which the AppKit and WinUI seats both have and this one did not.
    // The classes that fall in the gap are the ones that publish a `setHeight`
    // of their own and so never reach `ICoreGraphicsObject::setHeight`:
    // ICoreGraphicsTable, ICoreGraphicsScrollPane and the entry row. Measured on
    // the gallery's catalogue: adding a row grew the table's painted border and
    // NOT the bounds the core clips its children to, so every row past the
    // height the core last heard about was clipped away -- a table that grows an
    // empty box at the bottom instead of a row.
    //
    // ⚠ A PULL, NOT A PENDING LIST. Recording the node from
    // prepareGeometryChange() looks right and fails: every caller follows Qt's
    // contract and calls it BEFORE the mutation, so the record is drained while
    // the old size is still in place. Asking each node "does your wrapper still
    // agree with the core?" is order-independent and has no list to keep.
    //
    // ⚠ AND IT MUST COMPARE BEFORE IT WRITES. icoreSceneSetContentBounds()
    // dirties unconditionally rather than on a change of value, so a reconcile
    // that pushed every node every pass would mark the whole scene dirty on
    // every frame and every mouse move.
    virtual void reconcileBounds();

    // ⚠⚠ ON THE BASE BECAUSE IT IS ALSO THE REPAINT PUMP, AND IT WAS FOUR
    // IDENTICAL PRIVATE COPIES BEFORE THAT. Every seat in this tier had its own
    // `markDirty()` reading `if (host) icoreSceneMarkDirty(host->scene(), id)`;
    // marking the core is only half of what a mutation owes, because a dirty
    // core repaints nothing on its own -- something has to turn the damage into
    // a toolkit invalidation. Four copies meant four places to remember that in,
    // so the copies are gone and this is the one place it happens.
    void markDirty();

protected:
    // Mirror everything this seat holds into the core. Called on every attach
    // and by the seat's own setters. The base calls it; only the seat knows what
    // it has.
    virtual void pushAll() = 0;

    // Register this node's appearance and its content hook with the host.
    // Called immediately after the id exists.
    virtual void bindSelf() = 0;
};
};

File-scope declarations#

// The ONE type every scene item's native handle points at on this backend, and
// the ONE implementation of attaching, detaching and re-parenting one.
// 
// ⚠ THE ROW THAT ADDED THIS IS NAMED IN ICoreGtk4SceneNode.cpp AND NOT HERE,
// per this zone's README: the docs generator lifts HEADER comments into public
// API pages, so a board reference in a .h ships a product reader an instruction
enum class ICoreGtk4ScenePointerKind {
    Pressed,
    Moved,           // a move with a grab open -- ICoreGraphicsObject::mouseMoved
    Released,
    DoubleClicked,
    PointerEntered,
    PointerMoved,    // a HOVER move over the item already hovered
    PointerLeft,

    // ⚠ A RIGHT PRESS IS ALSO A CONTEXT MENU (`L10.6`/`L10.10`). Raised by the
    // view AFTER the press and offered down the hit stack, topmost first --
    // AppKit's ICoreAppKitSceneEventKind::ContextMenu and WinUI's
    // ICoreSceneViewEventKind::ContextMenu. Until this value existed the gtk4
    // scene had no way to reach `contextMenuRequested` at all, so neither the
    // canvas menu nor a block's config pane ever opened on right click.
    ContextMenu,

    // ⚠ THE WHEEL RIDES THE POINTER STRUCT RATHER THAN GETTING A SECOND ONE,
    // AND THE REASON IS THE FOUR COORDINATE SPACES. A notch offered to an item
    // needs exactly what a press needs -- the scene point, the item's own point,
    // the toplevel point and the modifiers -- plus a travel; a parallel struct
    // would be those four fields copied so that one more could be added beside
    // them, and two dispatch paths to keep in step.
    WheelScrolled
};

ICoreGtk4SceneRender.h#

ICoreEssentials/UI/Backends/Gtk4/Graphics/ICoreGtk4SceneRender.h

Run a whole scene down a painter: the body ICoreGraphicsScene::renderTo has carried since L4.2, lifted out so a SECOND caller does not become a second copy of it.

⚠ WHY IT MOVED. The chart tier needs the same operation from the other end: ICoreChartBase's three export routes render THE SCENE THE CHART SITS IN, and they reach it from an ITEM rather than from a scene wrapper. The other toolkit answers both with one call (QGraphicsScene::render); here the walk is ours, so the choice was a shared body or a duplicated one -- and a duplicated draw loop is the shape that ends with two callers disagreeing about clipping in a way nothing reports. Both sibling zones reached this conclusion first and by the same road; see ../AppKit/Graphics/ ICoreAppKitSceneRender.h and ../WinUI/Graphics/ICoreWinUISceneRender.h.

Declares no class of its own — see the file.