Skip to content

Conversation

@yuto-trd
Copy link
Member

Description

Implement Presenter classes (DrawablePresenter, FilterEffectPresenter, BrushPresenter, TransformPresenter) that enable weak object referencing via Reference<T>. These Presenters allow the same object to be "presented" in multiple places without violating the single-parent constraint.

Breaking changes

None

Fixed issues

Enables object reuse in multiple locations through delegation pattern

Copilot AI review requested due to automatic review settings January 19, 2026 10:25
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f51cfda815

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 90 to 94
if (_target != newTarget)
{
_target = newTarget;
_targetResource?.Dispose();
_targetResource = (Drawable.Resource?)newTarget.Value?.ToResource(context);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh target when reference resolves to same Id

When Target is first an unresolved Reference (only Id) and later resolved via Reference.Resolved(...), the Id stays the same but Value becomes non-null. Reference<T> equality compares only Id, so _target != newTarget stays false and _targetResource is never created. That leaves the presenter permanently no-op for deserialized or lazily-resolved references. Consider also checking newTarget.Value (or whether _targetResource is null) to trigger resource creation even when the Id is unchanged.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces a delegation pattern through four new Presenter classes (DrawablePresenter, FilterEffectPresenter, BrushPresenter, TransformPresenter) that enable weak object referencing via Reference<T>. These classes allow the same graphical object to be "presented" in multiple locations without violating the single-parent constraint inherent in the object hierarchy.

Changes:

  • Added four new Presenter classes that delegate operations to target objects through weak references
  • Updated BrushConstructor to handle BrushPresenter resources by unwrapping and delegating to the target brush
  • Each Presenter maintains its own Resource class that manages the lifecycle of the target resource

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/Beutl.Engine/Media/Brushes/BrushPresenter.cs New class that delegates brush operations to a referenced Brush object with resource management
src/Beutl.Engine/Graphics/Transformation/TransformPresenter.cs New lightweight presenter for Transform objects that delegates matrix creation
src/Beutl.Engine/Graphics/FilterEffects/FilterEffectPresenter.cs New class that delegates filter effect operations with resource lifecycle management
src/Beutl.Engine/Graphics/DrawablePresenter.cs New class that delegates drawable rendering and measurement operations with resource management
src/Beutl.Engine/Graphics/BrushConstructor.cs Added logic to unwrap BrushPresenter resources and delegate to their target brushes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Handle BrushPresenter by delegating to the target brush
if (Brush is BrushPresenter.Resource presenter && presenter.TargetResource != null)
{
return new BrushConstructor(Bounds, presenter.TargetResource as Brush.Resource, BlendMode).CreateShader();
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast presenter.TargetResource as Brush.Resource is redundant. According to the class definition, TargetResource is already of type Brush.Resource?. The as operator will always succeed (or return null if TargetResource is null, but that's already checked). Consider simplifying to just pass presenter.TargetResource directly.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Creates the transformation matrix by delegating to the target Transform.
/// Returns Identity matrix if the target is null.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider documenting the behavior when the target reference is null or unresolved. While the code handles this gracefully by returning Identity, it would be helpful for API consumers to explicitly state this behavior in the documentation.

Suggested change
/// Returns Identity matrix if the target is null.
/// Returns the identity matrix if the target is null or the reference cannot be resolved.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 69
using Beutl.Engine;
using Beutl.Graphics.Rendering;

namespace Beutl.Media;

/// <summary>
/// A presenter that delegates brush operations to a referenced Brush object.
/// This allows the same Brush to be "presented" in multiple places without violating
/// the single-parent constraint.
/// </summary>
[SuppressResourceClassGeneration]
public sealed class BrushPresenter : Brush
{
public BrushPresenter()
{
ScanProperties<BrushPresenter>();
}

/// <summary>
/// Gets the property that holds a reference to the target Brush object.
/// </summary>
public IProperty<Reference<Brush>> Target { get; } = Property.Create<Reference<Brush>>();

public override Brush.Resource ToResource(RenderContext context)
{
var resource = new Resource();
bool updateOnly = true;
resource.Update(this, context, ref updateOnly);
return resource;
}

public new sealed class Resource : Brush.Resource
{
private Reference<Brush> _target;
private Brush.Resource? _targetResource;

public Reference<Brush> Target
{
get => _target;
set => _target = value;
}

public Brush.Resource? TargetResource
{
get => _targetResource;
set => _targetResource = value;
}

public override void Update(EngineObject obj, RenderContext context, ref bool updateOnly)
{
base.Update(obj, context, ref updateOnly);
var presenter = (BrushPresenter)obj;
var newTarget = context.Get(presenter.Target);

if (_target != newTarget)
{
_target = newTarget;
_targetResource?.Dispose();
_targetResource = (Brush.Resource?)newTarget.Value?.ToResource(context);
if (!updateOnly)
{
Version++;
updateOnly = true;
}
}
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
}
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_targetResource?.Dispose();
}
base.Dispose(disposing);
}
}
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new BrushPresenter class lacks test coverage. Given that the repository has comprehensive testing for other drawable and brush-related functionality (as seen in ShapeTests.cs), consider adding tests to verify: (1) proper delegation to the target brush, (2) correct resource lifecycle management, and (3) behavior when the target reference is null or changes.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 86
using Beutl.Engine;
using Beutl.Graphics.Rendering;

namespace Beutl.Graphics;

/// <summary>
/// A presenter that delegates drawable operations to a referenced Drawable object.
/// This allows the same Drawable to be "presented" in multiple places without violating
/// the single-parent constraint.
/// </summary>
[SuppressResourceClassGeneration]
public sealed class DrawablePresenter : Drawable
{
public DrawablePresenter()
{
ScanProperties<DrawablePresenter>();
}

/// <summary>
/// Gets the property that holds a reference to the target Drawable object.
/// </summary>
public IProperty<Reference<Drawable>> Target { get; } = Property.Create<Reference<Drawable>>();

/// <summary>
/// Renders the drawable by delegating to the target Drawable.
/// Does nothing if the target is null.
/// </summary>
public override void Render(GraphicsContext2D context, Drawable.Resource resource)
{
var r = (Resource)resource;
if (r.TargetResource != null)
{
((Drawable)r.TargetResource.GetOriginal()).Render(context, r.TargetResource);
}
}

/// <summary>
/// This method is not called because Render() delegates to the target directly.
/// </summary>
protected override void OnDraw(GraphicsContext2D context, Drawable.Resource resource)
{
// Not called because Render() delegates to the target directly
}

/// <summary>
/// Measures the drawable by delegating to the target Drawable.
/// Returns Size.Empty if the target is null.
/// </summary>
protected override Size MeasureCore(Size availableSize, Drawable.Resource resource)
{
var r = (Resource)resource;
if (r.TargetResource == null)
{
return Size.Empty;
}
return ((Drawable)r.TargetResource.GetOriginal()).MeasureInternal(availableSize, r.TargetResource);
}

public override Drawable.Resource ToResource(RenderContext context)
{
var resource = new Resource();
bool updateOnly = true;
resource.Update(this, context, ref updateOnly);
return resource;
}

public new sealed class Resource : Drawable.Resource
{
private Reference<Drawable> _target;
private Drawable.Resource? _targetResource;

public Reference<Drawable> Target
{
get => _target;
set => _target = value;
}

public Drawable.Resource? TargetResource
{
get => _targetResource;
set => _targetResource = value;
}

public override void Update(EngineObject obj, RenderContext context, ref bool updateOnly)
{
base.Update(obj, context, ref updateOnly);
var presenter = (DrawablePresenter)obj;
var newTarget = context.Get(presenter.Target);

if (_target != newTarget)
{
_target = newTarget;
_targetResource?.Dispose();
_targetResource = (Drawable.Resource?)newTarget.Value?.ToResource(context);
if (!updateOnly)
{
Version++;
updateOnly = true;
}
}
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
}
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_targetResource?.Dispose();
}
base.Dispose(disposing);
}
}
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new DrawablePresenter class lacks test coverage. Given that the repository has comprehensive testing for other drawable functionality (as seen in ShapeTests.cs), consider adding tests to verify: (1) proper delegation to the target drawable for rendering and measuring, (2) correct resource lifecycle management, and (3) behavior when the target reference is null or changes.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 58
public override void Update(EngineObject obj, RenderContext context, ref bool updateOnly)
{
base.Update(obj, context, ref updateOnly);
var presenter = (BrushPresenter)obj;
var newTarget = context.Get(presenter.Target);

if (_target != newTarget)
{
_target = newTarget;
_targetResource?.Dispose();
_targetResource = (Brush.Resource?)newTarget.Value?.ToResource(context);
if (!updateOnly)
{
Version++;
updateOnly = true;
}
}
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
}
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Update method contains duplicated logic that appears across all Presenter classes (BrushPresenter, DrawablePresenter, FilterEffectPresenter). Consider extracting this common pattern into a base class or helper method to reduce code duplication and improve maintainability. The pattern includes: (1) comparing target references, (2) disposing and recreating resources on change, (3) updating existing resources when unchanged, and (4) managing version increments.

Copilot uses AI. Check for mistakes.
Comment on lines 84 to 74
public override void Update(EngineObject obj, RenderContext context, ref bool updateOnly)
{
base.Update(obj, context, ref updateOnly);
var presenter = (DrawablePresenter)obj;
var newTarget = context.Get(presenter.Target);

if (_target != newTarget)
{
_target = newTarget;
_targetResource?.Dispose();
_targetResource = (Drawable.Resource?)newTarget.Value?.ToResource(context);
if (!updateOnly)
{
Version++;
updateOnly = true;
}
}
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
}
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential issue when a target reference's Value becomes null while keeping the same Id. If _target == newTarget (same Id) but newTarget.Value is null, the else-if condition is skipped, leaving _targetResource pointing to a stale resource. Consider adding logic to dispose _targetResource when newTarget.Value is null, even if the reference Id hasn't changed.

Copilot uses AI. Check for mistakes.
Comment on lines 79 to 88
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential issue when a target reference's Value becomes null while keeping the same Id. If _target == newTarget (same Id) but newTarget.Value is null, the else-if condition is skipped, leaving _targetResource pointing to a stale resource. Consider adding logic to dispose _targetResource when newTarget.Value is null, even if the reference Id hasn't changed.

Suggested change
else if (_targetResource != null && newTarget.Value != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
else
{
if (newTarget.Value == null)
{
if (_targetResource != null)
{
_targetResource.Dispose();
_targetResource = null;
if (!updateOnly)
{
Version++;
updateOnly = true;
}
}
}
else if (_targetResource != null)
{
var oldVersion = _targetResource.Version;
bool _ = false;
_targetResource.Update(newTarget.Value, context, ref _);
if (!updateOnly && oldVersion != _targetResource.Version)
{
Version++;
updateOnly = true;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +24
var resource = new Resource();
bool updateOnly = true;
resource.Update(this, context, ref updateOnly);
return resource;
}

public new sealed class Resource : Brush.Resource
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class has the same name as its base class.

Suggested change
var resource = new Resource();
bool updateOnly = true;
resource.Update(this, context, ref updateOnly);
return resource;
}
public new sealed class Resource : Brush.Resource
var resource = new BrushPresenterResource();
bool updateOnly = true;
resource.Update(this, context, ref updateOnly);
return resource;
}
public sealed class BrushPresenterResource : Brush.Resource

Copilot uses AI. Check for mistakes.
return resource;
}

public new sealed class Resource : Drawable.Resource
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class has the same name as its base class.

Copilot uses AI. Check for mistakes.
return resource;
}

public new sealed class Resource : FilterEffect.Resource
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class has the same name as its base class.

Copilot uses AI. Check for mistakes.
Add DrawablePresenter, FilterEffectPresenter, BrushPresenter, and TransformPresenter to enable weak object referencing via Reference<T>. This allows the same object to be "presented" in multiple places without violating single-parent constraints.

- All Presenters delegate method calls to target objects
- Custom Resource classes manage target lifecycle
- BrushConstructor updated to support BrushPresenter
@yuto-trd yuto-trd force-pushed the yuto-trd/object-presenter branch from 70a0561 to d7a25d9 Compare January 20, 2026 18:18
Add DropDownButton-based UI for selecting target objects when property
value is a Presenter class (FilterEffectPresenter, TransformPresenter,
BrushPresenter). Uses ObjectSearcher to enumerate available targets
within the same Scene.

- Add TargetObjectInfo record for holding target information
- Add TargetPickerFlyout and TargetPickerFlyoutViewModel
- Update FilterEffectEditor, TransformEditor, BrushEditor with
  Presenter mode layout (property name | DropDownButton | menu)
- Update FilterEffectListItemEditor, TransformListItemEditor with
  Presenter mode layout (reorderHandle | DropDownButton | buttons)
@yuto-trd yuto-trd force-pushed the yuto-trd/object-presenter branch from d7a25d9 to 81e2745 Compare January 20, 2026 18:52
@github-actions
Copy link
Contributor

No TODO comments were found.

@github-actions
Copy link
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
Beutl.Operators 3% 0% 87
Beutl.Editor 98% 94% 594
Beutl.Core 40% 36% 2939
Beutl.Engine 27% 23% 10585
Beutl.Threading 100% 94% 118
Beutl.Utilities 73% 67% 358
Beutl.Configuration 39% 19% 288
Beutl.Extensibility 14% 15% 105
Beutl.ProjectSystem 19% 11% 1879
Beutl.Language 0% 0% 792
Summary 30% (12709 / 42252) 28% (3456 / 12134) 17745

Minimum allowed line rate is 0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants