Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1caad3c
feat: implement expression support in animatable properties and relat…
yuto-trd Dec 2, 2025
bbd112b
feat: add expression support to property adapters and related classes
yuto-trd Dec 2, 2025
86c7054
feat: add Microsoft.CodeAnalysis.CSharp.Scripting package reference
yuto-trd Dec 2, 2025
b10de0f
feat: add expression editing functionality to property editor
yuto-trd Dec 2, 2025
d52c5cd
feat: replace Observable.Return with Observable.ReturnThenNever in va…
yuto-trd Dec 2, 2025
d3849fe
feat: replace dialog with ExpressionEditorFlyout for editing expressi…
yuto-trd Dec 2, 2025
0798e5f
feat: update property editor to use Observable.ReturnThenNever for be…
yuto-trd Dec 2, 2025
2b2d1dc
feat: add expression status indication in property editor menu
yuto-trd Dec 2, 2025
9756625
fix: hash set usage in CoreObjectExtensions
yuto-trd Dec 2, 2025
6842756
feat: add copy property path and get property code functionality in p…
yuto-trd Dec 2, 2025
5b2eb83
feat: add circular reference detection during expression evaluation
yuto-trd Dec 2, 2025
4ba2ad8
feat: update GetValue method to use RenderContext instead of TimeSpan
yuto-trd Dec 2, 2025
7f795dc
feat: enhance expression evaluation with context management and circu…
yuto-trd Dec 2, 2025
a7c87be
feat: add expression handling in EngineObject for serialization and d…
yuto-trd Dec 2, 2025
3261bdd
Update src/Beutl.ProjectSystem/NodeTree/NodePropertyAdapter.cs
yuto-trd Dec 2, 2025
cb0faf6
refactor: PropertyEditorMenu.axaml.cs
yuto-trd Dec 3, 2025
9b893ed
feat: refactor Expression class to use lazy parsing and improve error…
yuto-trd Dec 3, 2025
1c85656
refactor: ValueEditorViewModel.cs
yuto-trd Dec 3, 2025
0558869
test: add unit tests for Expression, ExpressionContext, and PropertyL…
yuto-trd Dec 3, 2025
d193b89
Initial plan
Copilot Dec 4, 2025
f529b98
Add error display when expression validation fails
Copilot Dec 4, 2025
d40ca44
Use SystemFillColorCriticalBrush and refactor SetExpression to return…
Copilot Dec 4, 2025
d242806
Fix formatting - normalize blank line before return statement
Copilot Dec 4, 2025
95ffbc1
Remove Validator, perform validation in Confirmed event handler
Copilot Dec 4, 2025
310d0f0
refactor: streamline expression confirmation handling and improve tex…
yuto-trd Dec 4, 2025
e602fee
Merge pull request #1432 from b-editor/copilot/sub-pr-1431
yuto-trd Dec 4, 2025
deaf005
refactor: Prevent AnimatableProperty.GetValue from swallowing exceptions
yuto-trd Dec 4, 2025
046be01
feat: implement ExpressionEditorFlyoutPresenter for improved expressi…
yuto-trd Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<PackageVersion Include="ILGPU" Version="1.5.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.12.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.3" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#nullable enable

using System.Reactive.Disposables;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;

using Beutl.Reactive;

namespace Beutl.Controls.PropertyEditors;

public class ExpressionEditorFlyoutPresenter : DraggablePickerFlyoutPresenter
{
public static readonly StyledProperty<string?> ExpressionTextProperty =
AvaloniaProperty.Register<ExpressionEditorFlyoutPresenter, string?>(nameof(ExpressionText));

public static readonly StyledProperty<string?> ErrorMessageProperty =
AvaloniaProperty.Register<ExpressionEditorFlyoutPresenter, string?>(nameof(ErrorMessage));

private readonly CompositeDisposable _disposables = [];
private const string HasErrorPseudoClass = ":has-error";
private const string HelpTabPseudoClass = ":help-tab";

private RadioButton? _inputTabButton;
private RadioButton? _helpTabButton;

public string? ExpressionText
{
get => GetValue(ExpressionTextProperty);
set => SetValue(ExpressionTextProperty, value);
}

public string? ErrorMessage
{
get => GetValue(ErrorMessageProperty);
set => SetValue(ErrorMessageProperty, value);
}

protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_disposables.Clear();
base.OnApplyTemplate(e);

_inputTabButton = e.NameScope.Find<RadioButton>("InputTabButton");
_helpTabButton = e.NameScope.Find<RadioButton>("HelpTabButton");

_inputTabButton?.AddDisposableHandler(ToggleButton.IsCheckedChangedEvent, OnTabButtonIsCheckedChanged)
.DisposeWith(_disposables);

_helpTabButton?.AddDisposableHandler(ToggleButton.IsCheckedChangedEvent, OnTabButtonIsCheckedChanged)
.DisposeWith(_disposables);

if (_inputTabButton != null)
{
_inputTabButton.IsChecked = true;
}
}

private void OnTabButtonIsCheckedChanged(object? sender, RoutedEventArgs e)
{
if (_helpTabButton != null)
{
PseudoClasses.Set(HelpTabPseudoClass, _helpTabButton.IsChecked == true);
}
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ErrorMessageProperty)
{
PseudoClasses.Set(HasErrorPseudoClass, !string.IsNullOrWhiteSpace(ErrorMessage));
}
}
}
1 change: 1 addition & 0 deletions src/Beutl.Controls/Styles.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<ResourceInclude Source="/Styling/PropertyEditors/GradientStopsSlider.axaml" />
<ResourceInclude Source="/Styling/PropertyEditors/LibraryItemPickerFlyoutPresenter.axaml" />
<ResourceInclude Source="/Styling/PropertyEditors/FontFamilyPickerFlyoutPresenter.axaml" />
<ResourceInclude Source="/Styling/PropertyEditors/ExpressionEditorFlyoutPresenter.axaml" />
<ResourceInclude Source="/ColorPicker/ColorPickerStyles.axaml" />
<ResourceInclude Source="/ColorPicker/ColorPickerButtonStyles.axaml" />
</ResourceDictionary.MergedDictionaries>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lang="using:Beutl.Language"
xmlns:local="using:Beutl.Controls.PropertyEditors"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:CompileBindings="True">

<ControlTheme x:Key="{x:Type local:ExpressionEditorFlyoutPresenter}" TargetType="local:ExpressionEditorFlyoutPresenter">
<Setter Property="Width" Value="360" />
<Setter Property="ShowHideButtons" Value="True" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="{DynamicResource FlyoutPresenterBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource FlyoutBorderThemeBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource FlyoutBorderThemeThickness}" />
<Setter Property="Padding" Value="0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="Template">
<ControlTemplate>
<Border Padding="{DynamicResource FlyoutBorderThemePadding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid RowDefinitions="Auto,*,Auto">
<!-- Tab Header and Close Button -->
<Grid Name="DragArea"
Height="40"
VerticalAlignment="Top"
Background="Transparent"
ColumnDefinitions="*,Auto">
<WrapPanel Name="TabLayout" Margin="4,4,0,4">
<RadioButton Name="InputTabButton"
IsChecked="True"
ToolTip.Tip="{x:Static lang:Strings.Expression_Input}">
<ui:SymbolIcon Symbol="Edit" />
</RadioButton>
<RadioButton Name="HelpTabButton"
ToolTip.Tip="{x:Static lang:Strings.Expression_Help}">
<ui:SymbolIcon Symbol="Help" />
</RadioButton>
</WrapPanel>

<Button Name="CloseButton"
Grid.Column="1"
Width="32"
Height="32"
Margin="4"
Padding="0"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Theme="{StaticResource TransparentButton}">
<ui:FontIcon FontFamily="{DynamicResource SymbolThemeFontFamily}" Glyph="&#xE711;" />
</Button>
</Grid>

<!-- Content Area -->
<Grid Name="ContentArea" Grid.Row="1" Margin="8,0,8,8">
<!-- Input Tab -->
<StackPanel Name="InputPanel" Spacing="8">
<TextBlock Text="{x:Static lang:Strings.ExpressionHelp}"
TextWrapping="Wrap"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBox Name="ExpressionTextBox"
AcceptsReturn="True"
MinHeight="100"
MaxHeight="200"
TextWrapping="Wrap"
Watermark="Sin(Time * 2 * PI) * 100"
Text="{TemplateBinding ExpressionText, Mode=TwoWay}"
VerticalContentAlignment="Top" />
<TextBlock Name="ErrorTextBlock"
Text="{TemplateBinding ErrorMessage}"
TextWrapping="Wrap"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
IsVisible="False" />
</StackPanel>

<!-- Help Tab -->
<ScrollViewer Name="HelpPanel" MaxHeight="300" IsVisible="False">
<StackPanel Spacing="12">
<!-- Variables Section -->
<StackPanel Spacing="4">
<TextBlock Text="{x:Static lang:Strings.Expression_Variables}"
FontWeight="Bold" />
<TextBlock Text="{x:Static lang:Strings.Expression_Variables_Description}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
</StackPanel>

<!-- Functions Section -->
<StackPanel Spacing="4">
<TextBlock Text="{x:Static lang:Strings.Expression_Functions}"
FontWeight="Bold" />
<StackPanel Spacing="4">
<TextBlock Text="{x:Static lang:Strings.Expression_Functions_Trigonometric}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
<TextBlock Text="{x:Static lang:Strings.Expression_Functions_Math}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
<TextBlock Text="{x:Static lang:Strings.Expression_Functions_Interpolation}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
<TextBlock Text="{x:Static lang:Strings.Expression_Functions_Utility}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
</StackPanel>
</StackPanel>

<!-- GetProperty Section -->
<StackPanel Spacing="4">
<TextBlock Text="GetProperty" FontWeight="Bold" />
<TextBlock Text="{x:Static lang:Strings.Expression_GetProperty_Description}"
TextWrapping="Wrap"
FontFamily="Consolas, Menlo, monospace"
FontSize="11" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>

<!-- Accept/Dismiss Buttons -->
<Border Grid.Row="2"
BorderBrush="{DynamicResource PickerFlyoutPresenterDivider}"
BorderThickness="0,1,0,0">
<Panel Name="AcceptDismissContainer"
Height="{DynamicResource PickerAcceptDismissRegionHeight}"
IsVisible="False">
<Grid ColumnDefinitions="*,*">
<Button Name="AcceptButton"
Margin="4,4,2,4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Theme="{StaticResource FlyoutAcceptDismiss}">
<ui:SymbolIcon FontSize="18" Symbol="Checkmark" />
</Button>
<Button Name="DismissButton"
Grid.Column="1"
Margin="2,4,4,4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Theme="{StaticResource FlyoutAcceptDismiss}">
<ui:SymbolIcon FontSize="16" Symbol="Dismiss" />
</Button>
</Grid>
</Panel>
</Border>
</Grid>
</Border>
</ControlTemplate>
</Setter>

<Style Selector="^:acceptdismiss /template/ Panel#AcceptDismissContainer">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:acceptdismiss /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="False" />
</Style>

<Style Selector="^:has-error /template/ TextBlock#ErrorTextBlock">
<Setter Property="IsVisible" Value="True" />
</Style>

<!-- Help tab visibility -->
<Style Selector="^:help-tab /template/ StackPanel#InputPanel">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:help-tab /template/ ScrollViewer#HelpPanel">
<Setter Property="IsVisible" Value="True" />
</Style>

<!-- Tab button styles -->
<Style Selector="^ /template/ WrapPanel#TabLayout">
<Style Selector="^ > RadioButton">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="Theme" Value="{StaticResource ColorPickerTypeTransparentToggleButtonStyle}" />

<Style Selector="^ > ui|FontIcon">
<Setter Property="FontFamily" Value="{DynamicResource SymbolThemeFontFamily}" />
</Style>
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>
8 changes: 6 additions & 2 deletions src/Beutl.Core/CoreObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static IObservable<T> GetObservable<T>(this ICoreObject obj, CoreProperty
foreach (CoreProperty prop in props)
{
object? inner = obj.GetValue(prop);
if (inner != null && hashSet.Add(inner))
if (inner != null && !hashSet.Contains(inner))
{
if (predicate(inner))
{
Expand All @@ -65,6 +65,8 @@ public static IObservable<T> GetObservable<T>(this ICoreObject obj, CoreProperty
{
return match;
}

hashSet.Add(inner);
}
}

Expand All @@ -75,7 +77,7 @@ public static IObservable<T> GetObservable<T>(this ICoreObject obj, CoreProperty
{
foreach (IHierarchical item in hierarchical.HierarchicalChildren)
{
if (hashSet.Add(item))
if (!hashSet.Contains(item))
{
if (predicate(item))
{
Expand All @@ -85,6 +87,8 @@ public static IObservable<T> GetObservable<T>(this ICoreObject obj, CoreProperty
{
return match;
}

hashSet.Add(item);
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/Beutl.Core/Reactive/ObservableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace Beutl.Reactive;

public static class ObservableExtensions
{
extension(Observable)
{
public static IObservable<T> ReturnThenNever<T>(T value)
{
return Observable.Create<T>(observer =>
{
observer.OnNext(value);
return Disposable.Empty;
});
}
}
}
1 change: 1 addition & 0 deletions src/Beutl.Engine/Beutl.Engine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@
<PackageReference Include="Vortice.XAudio2" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/Beutl.Engine/Converters/ColorConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Beutl.Animation;
using Beutl.Graphics.Rendering;
using Beutl.Media;

namespace Beutl.Converters;
Expand Down Expand Up @@ -111,7 +112,7 @@ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type source
}
else if (value is SolidColorBrush solidColorBrush)
{
return solidColorBrush.Color.GetValue(TimeSpan.Zero);
return solidColorBrush.Color.GetValue(RenderContext.Default);
}
else if (value is SolidColorBrush.Resource solidColorBrushResource)
{
Expand Down
Loading
Loading