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:
PointerDragis a frame-polled drag helper.Beginrecords the origin,Currentreturns aVector2you can map to whatever value you are dragging (here, size), andEndclears the active state. because it is polled rather than event-driven, the drag never stalls when the cursor moves faster than the event queue.Tickis the per-frame hook. returningtruesignals goo to callRebuild()at the end of that tick. returningfalsecosts nothing - no rebuild happens on frames where nothing changed.OnMouseDownandOnMouseUpon the grip panel start and stop the drag. everything else (reading position, clamping, sizing) happens inTick, which keeps the event handlers to one line each.
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¶
- events - all six mouse-event properties, the
MousePanelEventpayload, and how pointer-events auto-gating works. - build method - full
Rebuild()mechanics, theTickreturn-value contract, and structural diff. - your first counter - the simpler field-plus-rebuild pattern this guide builds on.
- container reference - full layout and style surface for
Container, includingPosition,Right,Bottom, andPx.Of.