| name | beutl-drawable |
| description | Implementation guide for Beutl's Drawable class. Use when adding a new Drawable (drawable object) to a Beutl project. Triggers on "create a Drawable", "implement a drawable object", "add an element like SourceImage/Shape/TextBlock". Covers both the Beutl core and extension packages. |
Beutl Drawable implementation guide
Guide for implementing a custom Drawable (drawable object) in Beutl.
Architecture overview
EngineObject (base)
└── Drawable (abstract)
├── Shape (abstract) — shape family
│ ├── RectShape
│ ├── EllipseShape
│ └── GeometryShape
├── SourceImage — image
├── SourceVideo — video
├── SourceBackdrop — backdrop
├── TextBlock — text
└── DrawableGroup — group
Where you implement it matters
In the Beutl core
using Beutl.Language;
namespace Beutl.Graphics;
[Display(Name = nameof(Strings.MyDrawable), ResourceType = typeof(Strings))]
public partial class MyDrawable : Drawable
{
}
In an extension package
using MyExtension.Strings;
namespace MyExtension.Graphics;
[Display(Name = nameof(ExtensionStrings.MyDrawable), ResourceType = typeof(ExtensionStrings))]
public partial class MyDrawable : Drawable
{
}
[Display(Name = "My Drawable")]
public partial class MyDrawable : Drawable
{
}
Notes for extension packages:
- Use your extension's own namespace.
- Create string resources inside the extension project.
Beutl.Language.Strings is not available (internal).
Basic pattern
1. Minimal Drawable
using System.ComponentModel.DataAnnotations;
using Beutl.Engine;
using Beutl.Graphics.Rendering;
using Beutl.Media;
namespace Beutl.Graphics;
[Display(Name = "My Drawable")]
public partial class MyDrawable : Drawable
{
public MyDrawable()
{
ScanProperties<MyDrawable>();
}
protected override Size MeasureCore(Size availableSize, Drawable.Resource resource)
{
var r = (Resource)resource;
return new Size(100, 100);
}
protected override void OnDraw(GraphicsContext2D context, Drawable.Resource resource)
{
var r = (Resource)resource;
}
}
2. Required elements
| Element | Why |
|---|
partial class | Required — a source generator emits the Resource class |
[Display] attribute | Sets the display name shown in the editor |
ScanProperties<T>() in the constructor | Registers properties with the system |
MeasureCore | Returns the drawable's size |
OnDraw | The actual drawing logic |
Property definitions
Value properties (primitives)
[Display(Name = "Width")]
[Range(0, float.MaxValue)]
public IProperty<float> Width { get; } = Property.CreateAnimatable<float>(100);
[Display(Name = "Mode")]
public IProperty<MyMode> Mode { get; } = Property.Create(MyMode.Default);
public IProperty<bool> IsVisible { get; } = Property.CreateAnimatable(true);
Object properties (EngineObject-derived types)
[Display(Name = "Fill")]
public IProperty<Brush?> Fill { get; } = Property.Create<Brush?>();
[Display(Name = "Stroke")]
public IProperty<Pen?> Pen { get; } = Property.Create<Pen?>();
[Display(Name = "Transform")]
public IProperty<Transform?> Transform { get; } = Property.Create<Transform?>();
[Display(Name = "Filter")]
public IProperty<FilterEffect?> FilterEffect { get; } = Property.Create<FilterEffect?>();
[Display(Name = "Source")]
public IProperty<ImageSource?> Source { get; } = Property.Create<ImageSource?>();
List properties (IListProperty)
Use when the drawable owns a collection of child elements:
public IListProperty<Drawable> Children { get; } = Property.CreateList<Drawable>();
public IListProperty<GradientStop> GradientStops { get; } = Property.CreateList<GradientStop>();
Behavior of IListProperty:
- The source generator emits a
List<T.Resource> field.
- Adds, removes, and updates are tracked automatically.
- Inside the
Resource class the collection is accessible as List<T.Resource>.
Example (DrawableGroup):
public sealed partial class DrawableGroup : Drawable
{
public IListProperty<Drawable> Children { get; } = Property.CreateList<Drawable>();
protected override void OnDraw(GraphicsContext2D context, Drawable.Resource resource)
{
var r = (Resource)resource;
foreach (Drawable.Resource item in r.Children)
{
context.DrawDrawable(item);
}
}
}
SuppressResourceClassGeneration
When you want to suppress auto-generation of the Resource class and manage values manually:
[SuppressResourceClassGeneration]
[Display(Name = "FontFamily")]
public IProperty<FontFamily?> FontFamily { get; } = Property.Create<FontFamily?>();
The Resource class
To extend the auto-generated Resource class:
public partial class MyDrawable : Drawable
{
public partial class Resource
{
private MyInternalData? _cachedData;
partial void PreUpdate(MyDrawable obj, RenderContext context)
{
}
partial void PostUpdate(MyDrawable obj, RenderContext context)
{
if (_needsUpdate)
{
Version++;
_cachedData = null;
}
}
partial void PostDispose(bool disposing)
{
_cachedData?.Dispose();
}
}
}
Version management
Resource.Version drives cache invalidation. Bump Version++ whenever the value changes.
partial void PostUpdate(MyDrawable obj, RenderContext context)
{
if (_geometryResource is null)
{
_geometryResource = _geometry.ToResource(context);
Version++;
}
else
{
var oldVersion = _geometryResource.Version;
_geometryResource.Update(_geometry, context, ref _);
if (oldVersion != _geometryResource.Version)
{
Version++;
}
}
}
Drawing
Main methods on GraphicsContext2D
protected override void OnDraw(GraphicsContext2D context, Drawable.Resource resource)
{
var r = (Resource)resource;
context.DrawGeometry(geometry, r.Fill, r.Pen);
context.DrawImageSource(imageSource, Brushes.Resource.White, null);
context.DrawText(formattedText, r.Fill, r.Pen);
context.DrawBackdrop(backdrop);
context.DrawDrawable(childResource);
}
Push/Pop pattern
protected override void OnDraw(GraphicsContext2D context, Drawable.Resource resource)
{
var r = (Resource)resource;
using (context.PushTransform(Matrix.CreateTranslation(10, 10)))
using (context.PushOpacity(0.5f))
{
context.DrawGeometry(geometry, r.Fill, r.Pen);
}
}
Overriding Render
Overriding Render lets you completely replace the base class's draw logic. You can customize the order in which BlendMode, Transform, Opacity, and FilterEffect are applied.
public override void Render(GraphicsContext2D context, Drawable.Resource resource)
{
if (resource.IsEnabled)
{
var r = (Resource)resource;
Size availableSize = context.Size.ToSize(1);
Size size = MeasureCore(availableSize, resource);
Matrix transform = GetTransformMatrix(availableSize, size, resource);
using (context.PushBlendMode(r.BlendMode))
using (context.PushTransform(transform))
using (context.PushOpacity(r.Opacity / 100f))
using (r.FilterEffect == null ? new() : context.PushFilterEffect(r.FilterEffect))
{
OnDraw(context, resource);
}
}
}
Use cases for overriding Render:
- You need to grab a backdrop before drawing (
SourceBackdrop).
- You want to customize the order in which children are drawn (
DrawableGroup).
- You use a custom RenderNode.
- Transform calculation depends on the children's bounds.
DrawableGroup example (custom Transform application):
public override void Render(GraphicsContext2D context, Drawable.Resource resource)
{
if (resource.IsEnabled)
{
var r = (Resource)resource;
Size availableSize = context.Size.ToSize(1);
var boundsMemory = context.UseMemory<Rect>();
using (context.PushBlendMode(r.BlendMode))
using (context.PushNode(...))
using (r.FilterEffect == null ? new() : context.PushFilterEffect(r.FilterEffect))
using (context.PushNode(...))
{
OnDraw(context, r);
}
}
}
Shape-derived classes
For subclasses of Shape, implement GetGeometry:
public sealed partial class MyShape : Shape
{
public partial class Resource
{
private readonly MyGeometry _geometry = new();
private MyGeometry.Resource? _geometryResource;
partial void PostUpdate(MyShape obj, RenderContext context)
{
_geometry.Width.CurrentValue = Math.Max(Width, 0);
_geometry.Height.CurrentValue = Math.Max(Height, 0);
if (_geometryResource is null)
{
_geometryResource = _geometry.ToResource(context);
Version++;
}
else
{
if (_geometryResource.GetOriginal() != _geometry)
{
var oldGeometry = _geometryResource;
_geometryResource = _geometry.ToResource(context);
oldGeometry.Dispose();
Version++;
}
else
{
var oldVersion = _geometryResource.Version;
var _ = false;
_geometryResource.Update(_geometry, context, ref _);
if (oldVersion != _geometryResource.Version)
{
Version++;
}
}
}
}
partial void PostDispose(bool disposing)
{
_geometryResource?.Dispose();
}
public override Geometry.Resource? GetGeometry() => _geometryResource;
}
}
Checklist
When you add a new Drawable, confirm:
Related files
src/Beutl.Engine/Graphics/Drawable.cs — base class
src/Beutl.Engine/Graphics/Shapes/Shape.cs — Shape base
src/Beutl.Engine/Graphics/DrawableGroup.cs — Group implementation example
src/Beutl.Engine/Engine/EngineObject.cs — EngineObject base
src/Beutl.Engine/Engine/Property.cs — property factories
src/Beutl.Engine.SourceGenerators/EngineObjectResourceGenerator.cs — source generator