a resizable panel

drag a corner handle to resize a box live

by the end of this guide you will have a panel whose width and height update in real time as you drag a grip handle in its bottom-right corner. you will see how PointerDrag tracks an in-progress drag without relying on mouse-move events, how the Tick method gates rebuilds to frames where something actually changed, and how OnMouseDown and OnMouseUp start and stop a drag.

hold the size in a field

create Code/Demos/ResizeBoxUI.cs. the only state the panel needs is a Vector2 for the current size. start with a fixed box so you can see the shape before wiring any interaction.

using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Goo.Demo;

public class ResizeBoxUI : GooPanel<Container>
{
    static readonly Vector2 MinSize = new( 120f, 90f );

    Vector2 _size = new( 320f, 220f );

    protected override Container Build()
    {
        var box = new Container
        {
            Position        = PositionMode.Absolute,
            Left            = Px.Of( 200 ), Top = Px.Of( 160 ),
            Width           = Px.Of( _size.x ), Height = Px.Of( _size.y ),
            BorderRadius    = Px.Of( 10 ),
            BackgroundColor = new Color( 0.16f, 0.18f, 0.24f, 0.92f ),
            AlignItems      = Align.Center, JustifyContent = Justify.Center,
            PointerEvents   = PointerEvents.All,
            Children        =
            {
                new Text( $"{_size.x:0} x {_size.y:0}" )
                {
                    FontFamily = "Roboto", FontSize = Px.Of( 26 ), FontWeight = 600,
                    FontColor  = Color.White,
                },
            },
        };

        return new Container
        {
            Width         = Length.Percent( 100 ),
            Height        = Length.Percent( 100 ),
            PointerEvents = PointerEvents.None,
            Children      = { box },
        };
    }
}

_size drives both Width and Height on the box and the label inside it. when you change _size and call Rebuild(), goo reruns Build() and the box snaps to the new dimensions.

add a drag handle

a PointerDrag tracks an ongoing drag. call Begin with the starting value when the mouse goes down, poll Current each tick to read where the pointer is now, and call End when the mouse comes up. the key property is Active - it is true while a drag is in progress.

add the PointerDrag field and a grip panel inside the box:

readonly PointerDrag _resize = new();

protected override Container Build()
{
    var grip = new Container
    {
        Position        = PositionMode.Absolute,
        Right           = Px.Of( 0 ), Bottom = Px.Of( 0 ),
        Width           = Px.Of( 18 ), Height = Px.Of( 18 ),
        BorderRadius    = Px.Of( 4 ),
        BackgroundColor = new Color( 0.18f, 0.62f, 0.95f ),
        PointerEvents   = PointerEvents.All,
        OnMouseDown     = _ => _resize.Begin( _size ),
        OnMouseUp       = _ => _resize.End(),
    };

    var box = new Container
    {
        Position        = PositionMode.Absolute,
        Left            = Px.Of( 200 ), Top = Px.Of( 160 ),
        Width           = Px.Of( _size.x ), Height = Px.Of( _size.y ),
        BorderRadius    = Px.Of( 10 ),
        BackgroundColor = new Color( 0.16f, 0.18f, 0.24f, 0.92f ),
        AlignItems      = Align.Center, JustifyContent = Justify.Center,
        PointerEvents   = PointerEvents.All,
        Children        =
        {
            new Text( $"{_size.x:0} x {_size.y:0}" )
            {
                FontFamily = "Roboto", FontSize = Px.Of( 26 ), FontWeight = 600,
                FontColor  = Color.White,
            },
            grip,
        },
    };

    return new Container
    {
        Width         = Length.Percent( 100 ),
        Height        = Length.Percent( 100 ),
        PointerEvents = PointerEvents.None,
        Children      = { box },
    };
}

OnMouseDown calls _resize.Begin( _size ) - that records _size as the drag origin. OnMouseUp calls _resize.End() to clear the active state.

poll the drag in tick

mouse-move events can drop frames when the cursor moves fast. PointerDrag avoids that: instead of reacting to individual events it stores where the pointer is now and you read it each frame. override Tick to do that poll:

protected override bool Tick( float dt )
{
    if ( _resize.Active )
    {
        Vector2 cur = _resize.Current( Panel );
        _size = new Vector2( MathF.Max( cur.x, MinSize.x ), MathF.Max( cur.y, MinSize.y ) );
        return true;
    }
    return false;
}

_resize.Current( Panel ) returns the cursor position relative to the host panel in CSS pixels. MathF.Max( cur.x, MinSize.x ) clamps so the box cannot shrink below MinSize. returning true from Tick signals goo to call Rebuild() automatically; false skips the rebuild on frames where nothing changed.

the complete class

using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Goo.Demo;

public class ResizeBoxUI : GooPanel<Container>
{
    static readonly Vector2 MinSize = new( 120f, 90f );

    Vector2 _size = new( 320f, 220f );
    readonly PointerDrag _resize = new();

    protected override bool Tick( float dt )
    {
        if ( _resize.Active )
        {
            Vector2 cur = _resize.Current( Panel );
            _size = new Vector2( MathF.Max( cur.x, MinSize.x ), MathF.Max( cur.y, MinSize.y ) );
            return true;
        }
        return false;
    }

    protected override Container Build()
    {
        var grip = new Container
        {
            Position        = PositionMode.Absolute,
            Right           = Px.Of( 0 ), Bottom = Px.Of( 0 ),
            Width           = Px.Of( 18 ), Height = Px.Of( 18 ),
            BorderRadius    = Px.Of( 4 ),
            BackgroundColor = new Color( 0.18f, 0.62f, 0.95f ),
            PointerEvents   = PointerEvents.All,
            OnMouseDown     = _ => _resize.Begin( _size ),
            OnMouseUp       = _ => _resize.End(),
        };

        var box = new Container
        {
            Position        = PositionMode.Absolute,
            Left            = Px.Of( 200 ), Top = Px.Of( 160 ),
            Width           = Px.Of( _size.x ), Height = Px.Of( _size.y ),
            BorderRadius    = Px.Of( 10 ),
            BackgroundColor = new Color( 0.16f, 0.18f, 0.24f, 0.92f ),
            AlignItems      = Align.Center, JustifyContent = Justify.Center,
            PointerEvents   = PointerEvents.All,
            Children        =
            {
                new Text( $"{_size.x:0} x {_size.y:0}" )
                {
                    FontFamily = "Roboto", FontSize = Px.Of( 26 ), FontWeight = 600,
                    FontColor  = Color.White,
                },
                grip,
            },
        };

        return new Container
        {
            Width         = Length.Percent( 100 ),
            Height        = Length.Percent( 100 ),
            PointerEvents = PointerEvents.None,
            Children      = { box },
        };
    }
}

press play, drop ResizeBoxUI on a screen panel, and drag the blue corner. the box and its label update every frame while the drag is active.

what just happened

three ideas worked together:

the root container sets PointerEvents = PointerEvents.None so the full-screen backing panel does not block clicks beneath it; the box and grip opt back in with PointerEvents.All.

see also