java.lang.Object
com.ultikits.ultitools.abstracts.gui.declarative.engine.GuiRenderer

public class GuiRenderer extends Object
GuiRenderer is responsible for coordinating Widget tree construction, diffing, and Inventory updates.

It is the declarative framework's core engine, with the main responsibilities of:

  • managing the Element tree's lifecycle
  • scheduling rebuilds (with frame coalescing)
  • running the diff algorithm
  • applying changes to the actual Inventory

Workflow:

 1. Initialize: createRootElement → build the RenderNode tree
 2. Render: diff → apply to Inventory
 3. Update: setState → scheduleBuild → rebuild → diff → apply
 
Since:
6.2.0
Version:
1.0.0
Author:
UltiTools Team
  • Field Details

    • gui

      private final mc.obliviate.inventory.Gui gui
    • player

      private final org.bukkit.entity.Player player
    • scheduler

      private final GuiScheduler scheduler
    • differ

      private final RenderNodeDiffer differ
    • rootElement

      @Nullable private @Nullable Element rootElement
    • lastRenderNodes

      @Nullable private @Nullable List<RenderNode> lastRenderNodes
    • widgetSupplier

      @Nullable private @Nullable Supplier<Widget> widgetSupplier
    • rootContext

      @Nullable private @Nullable BuildContext rootContext
    • clickHandlers

      private final Map<Integer,Consumer<org.bukkit.event.inventory.InventoryClickEvent>> clickHandlers
    • buildInProgress

      private boolean buildInProgress
      True while performBuild() is running its build body. Main-thread-confined: it is only read and written on the server main thread (performBuild() redirects itself there first, and scheduleBuild() reads it only after confirming it is on the main thread), so it needs no synchronization.
    • rebuildRequestedDuringBuild

      private boolean rebuildRequestedDuringBuild
      True when a rebuild was requested while buildInProgress was set. Main-thread- confined like buildInProgress. Cleared, and replayed as exactly one scheduleBuild() call, when the build that recorded it exits.
  • Constructor Details

    • GuiRenderer

      public GuiRenderer(@NotNull @NotNull mc.obliviate.inventory.Gui gui, @NotNull @NotNull org.bukkit.entity.Player player)
      Creates a GuiRenderer.
      Parameters:
      gui - the associated Gui instance
      player - the player
    • GuiRenderer

      public GuiRenderer(@NotNull @NotNull mc.obliviate.inventory.Gui gui, @NotNull @NotNull org.bukkit.entity.Player player, @NotNull @NotNull GuiScheduler scheduler)
      Creates a GuiRenderer with the given scheduler.
      Parameters:
      gui - the associated Gui instance
      player - the player
      scheduler - the scheduler
  • Method Details

    • initialize

      public void initialize(@NotNull @NotNull Supplier<Widget> widgetSupplier, @NotNull @NotNull BuildContext context)
      Initializes the renderer.

      D-09 item 1: no longer accepts a Widget built once up front; instead it accepts a Supplier that is called again at the start of every frame, re-deriving the Widget tree from the current state -- this is where the framework's UI = f(state) tagline first actually holds. Creating the root Element is deferred until inside the first performBuild() call, so the supplier is called exactly once on the first frame too, rather than once here and again inside performBuild().

      Parameters:
      widgetSupplier - the source re-evaluated every frame to produce the Widget tree
      context - the build context (used for the root Element; later frames' rebuilds and remounts reuse the same one)
    • scheduleBuild

      public void scheduleBuild()
      Schedules a rebuild.

      Uses frame coalescing, so multiple calls within a short window trigger only one rebuild.

      Requests raised during a build are coalesced, never re-entrant. When this is called on the main thread while performBuild() is running (for example a State.setState inside initState while the tree is being mounted, or a request raised by a child created during later frame work), the request is only recorded. Every request recorded during one build becomes exactly one follow-up scheduleBuild() after that build exits, so it runs against a fully mounted tree under the scheduler's normal frame timing. Calls from other threads are unchanged: the scheduler marshals them onto the main thread, where they run after the current main-thread task.

    • performBuild

      private void performBuild()
      Runs a rebuild immediately (must be called on the main thread).

      D-09 item 1: widgetSupplier is called again at the start of every frame, and its result is fed to the already-mounted root Element (Element.update(com.ultikits.ultitools.abstracts.gui.declarative.core.Widget), whose base-class implementation now marks it dirty -- see D-09 item 2), rather than being built once at initialize(java.util.function.Supplier<com.ultikits.ultitools.abstracts.gui.declarative.core.Widget>, com.ultikits.ultitools.abstracts.gui.declarative.core.BuildContext) time and never re-derived again.

      No re-entrance. A call that arrives while a build is already running on the main thread (for instance a scheduler drain reached from inside the build) does not start a nested build; it is recorded like a scheduleBuild() request. When the outermost build exits, normally or by exception, the in-progress flag is cleared first and a recorded request is replayed as exactly one scheduleBuild(). A replay that runs after dispose() builds nothing, because it returns at the widgetSupplier == null check below.

    • mountRoot

      private void mountRoot(@NotNull @NotNull Widget widget)
      Creates and mounts the root Element.

      Shared by both the first build and the "supplier returned an incompatible type" case.

      The root build scheduler is registered before mount on purpose: a rebuild request raised while the tree is being mounted (a setState inside initState, or a value source that delivers synchronously on subscribe) must not be dropped. It reaches scheduleBuild() while the build is in progress, is recorded there, and is replayed as one follow-up frame after this build exits.

      Parameters:
      widget - the root Widget
    • rebuildElement

      private void rebuildElement(@NotNull @NotNull Element element)
      Recursively rebuilds the Element tree.

      Guarded by RenderDepthGuard (T-05-62 / #371): a module-authored tree deeper than RenderDepthGuard.MAX_DEPTH raises RenderDepthExceededException here rather than overflowing the JVM stack mid-frame.

      Parameters:
      element - the Element to rebuild
    • collectRenderNodes

      @NotNull private @NotNull List<RenderNode> collectRenderNodes(@NotNull @NotNull Element element)
      Collects every RenderNode (post-order traversal).
      Parameters:
      element - the root Element
      Returns:
      the list of RenderNodes
    • collectRenderNodesRecursive

      private void collectRenderNodesRecursive(@NotNull @NotNull Element element, @NotNull @NotNull List<RenderNode> nodes)
    • applyDiff

      private void applyDiff(@NotNull @NotNull DiffResult diffResult)
      Applies the diff result to the Inventory.
      Parameters:
      diffResult - the diff result
    • updateClickHandlers

      private void updateClickHandlers(@NotNull @NotNull List<RenderNode> renderNodes)
      Updates the click-handler mapping.
      Parameters:
      renderNodes - the current list of RenderNodes
    • handleClick

      public void handleClick(@NotNull @NotNull org.bukkit.event.inventory.InventoryClickEvent event)
      Handles a click event.

      D-09 item 4: obliviate-invs' InvListener.onClick unconditionally calls Gui.onClick(event) (i.e. DeclarativeGui.onClick(org.bukkit.event.inventory.InventoryClickEvent), which forwards here) before applying its own getRawSlot() check -- this was confirmed from the jar's bytecode, not assumed. That means this method sees every click, including the player clicking their own inventory; it cannot assume the library has already filtered anything out.

      This uses InventoryClickEvent.getRawSlot() rather than InventoryClickEvent.getSlot() for the bounds check: getRawSlot() is an absolute index relative to the whole combined view (top GUI + player inventory) -- the top GUI occupies [0, gui.getSize()) and the player's own inventory starts at gui.getSize() -- whereas getSlot() is numbered independently relative to "whichever Inventory was clicked": the top GUI and the player inventory each count from 0 on their own, so the same value (say, 4) could be both the GUI's 4th slot and the player inventory's 4th slot. Clicking outside the window entirely (e.g. dropping an item outside it) makes Bukkit report a rawSlot of -999, which this same bounds check also rejects without throwing.

      clickHandlers is the sole record of this click routing -- it is populated by updateClickHandlers(java.util.List<com.ultikits.ultitools.abstracts.gui.declarative.core.RenderNode>) using RenderNode.getSlotIndex() (the same "slot within the top GUI" numbering space), and this method looks things up in that same space; there is no second record that could drift from it.

      Parameters:
      event - the click event
    • setSlot

      private void setSlot(int slot, @Nullable @Nullable mc.obliviate.inventory.Icon icon)
      Sets a slot's content.
      Parameters:
      slot - the slot index
      icon - the icon
    • clearSlot

      private void clearSlot(int slot)
      Clears a slot.
      Parameters:
      slot - the slot index
    • dispose

      public void dispose()
      Destroys the renderer and releases its resources.
    • getGui

      @NotNull public @NotNull mc.obliviate.inventory.Gui getGui()
      Gets the associated Gui instance.
      Returns:
      the Gui instance
    • getScheduler

      @NotNull public @NotNull GuiScheduler getScheduler()
      Gets the scheduler.
      Returns:
      the GuiScheduler