Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .jules/forge.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,11 @@
- Systematically removed `OnDataContextChanged` overrides in `Border`, `DialogBox`, `Table`, `TuiWindow`, and `Grid`.
- Removed explicit `DataContext` local value setters when assigning child elements (e.g., `PushOverlay` in `TuiWindow.cs`, `Content` setter in `DialogBox.cs`).
- Delegated context propagation fully to `UIElement.OnPropertyChanged` which naturally handles `IsInherited` property traversal, matching the exact WPF hierarchical structure and eliminating false-positive `HasLocalValue` states on visual children.

## 2024-06-25 - Dependency Object Style Precedence Integration
**Observation:** Discovered a core parity deficit where the `DependencyObject` property system lacked support for `Style` values and strict XAML value precedence (`Local > Trigger > Style > Inherited > Default`). Previously, `Style` property evaluation mechanics and their specific impact on triggers/local overrides were absent.
**Strategic Action:**
- Created the `Style` class encompassing `Setter` and `TriggerBase` collections.
- Integrated `StyleProperty` into the `UIElement` foundation.
- Restructured `DependencyObject.GetValue()` and assignment logic to deterministically prioritize `_localValues`, `_triggerValues`, and `_styleValues` dictionaries, resolving property collisions according to standard WPF rules.
- Added comprehensive testing in `StyleTests.cs` to guarantee exact behavioral mapping under concurrent state mutations.
128 changes: 128 additions & 0 deletions src/Tedd.TUI.Tests/StyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using Xunit;
using Tedd.TUI;

namespace Tedd.TUI.Tests;

public class StyleTests
{
private class TestControl : Control
{
}

[Fact]
public void Style_Setter_AppliesValue()
{
var control = new TestControl();
var style = new Style();
style.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Red));

control.Style = style;

Assert.Equal(TuiColor.Red, control.Foreground);
}

[Fact]
public void LocalValue_Overrides_StyleSetter()
{
var control = new TestControl();
control.Foreground = TuiColor.Blue; // Local value

var style = new Style();
style.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Red));

control.Style = style;

// Local value should take precedence
Assert.Equal(TuiColor.Blue, control.Foreground);
}

[Fact]
public void ClearingLocalValue_Restores_StyleSetter()
{
var control = new TestControl();
control.Foreground = TuiColor.Blue; // Local value

var style = new Style();
style.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Red));

control.Style = style;

// Local value should take precedence
Assert.Equal(TuiColor.Blue, control.Foreground);

// Clear local value
control.ClearValue(UIElement.ForegroundProperty);

// Style value should take over
Assert.Equal(TuiColor.Red, control.Foreground);
}

[Fact]
public void Style_Trigger_Overrides_StyleSetter()
{
var control = new TestControl();

var style = new Style();
style.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Red));

var trigger = new Trigger
{
Property = UIElement.IsFocusedProperty,
Value = true
};
trigger.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Yellow));
style.Triggers.Add(trigger);

control.Style = style;

// Initial state, no trigger
Assert.Equal(TuiColor.Red, control.Foreground);

// Activate trigger
control.IsFocused = true;

// Trigger should take precedence over style setter
Assert.Equal(TuiColor.Yellow, control.Foreground);

// Deactivate trigger
control.IsFocused = false;

// Should restore to style setter
Assert.Equal(TuiColor.Red, control.Foreground);
}

[Fact]
public void LocalValue_Overrides_StyleTrigger()
{
var control = new TestControl();

var style = new Style();
var trigger = new Trigger
{
Property = UIElement.IsFocusedProperty,
Value = true
};
trigger.Setters.Add(new Setter(UIElement.ForegroundProperty, TuiColor.Yellow));
style.Triggers.Add(trigger);

control.Style = style;

// Activate trigger
control.IsFocused = true;

Assert.Equal(TuiColor.Yellow, control.Foreground);

// Set local value while trigger is active
control.Foreground = TuiColor.Blue;

// Local value overrides trigger
Assert.Equal(TuiColor.Blue, control.Foreground);

// Deactivate trigger
control.IsFocused = false;

// Local value persists
Assert.Equal(TuiColor.Blue, control.Foreground);
}
}
80 changes: 78 additions & 2 deletions src/Tedd.TUI/DependencyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,32 +39,58 @@ public class DependencyObject : INotifyPropertyChanged
{
private readonly Dictionary<DependencyProperty, object> _localValues = new();
private readonly Dictionary<DependencyProperty, object> _triggerValues = new();
private readonly Dictionary<DependencyProperty, object> _styleValues = new();
private readonly Dictionary<DependencyProperty, object> _styleTriggerValues = new();

// Tracks properties for which SetValue was called while a trigger was active.
// GetValue returns the local value for these, giving it precedence over the trigger.
// Clearing the local value (ClearValue) removes the property from this set so that
// the still-active trigger value is re-exposed instead of falling back to inherited/default.
private readonly HashSet<DependencyProperty> _localOverridesActiveTrigger = new();
private readonly HashSet<DependencyProperty> _localOverridesActiveStyleTrigger = new();

protected virtual DependencyObject? InheritanceParent => null;

public event PropertyChangedEventHandler? PropertyChanged;

public object? GetValue(DependencyProperty dp)
{
// A local value explicitly set while a trigger is active takes highest precedence.
// Local explicitly set overrides an active trigger.
if (_localOverridesActiveTrigger.Contains(dp) && _localValues.TryGetValue(dp, out var overrideValue))
{
return overrideValue;
}
// Active trigger values take precedence over pre-existing local values.

// Active template trigger values take precedence over everything else
if (_triggerValues.TryGetValue(dp, out var triggerValue))
{
return triggerValue;
}

// Local explicit values set while an active style trigger is present override it.
if (_localOverridesActiveStyleTrigger.Contains(dp) && _localValues.TryGetValue(dp, out var styleOverrideValue))
{
return styleOverrideValue;
}

// Active style trigger values take precedence over regular local values
if (_styleTriggerValues.TryGetValue(dp, out var styleTriggerValue))
{
return styleTriggerValue;
}

// Local explicitly set values
if (_localValues.TryGetValue(dp, out var localValue))
{
return localValue;
}

// Style explicitly set values
if (_styleValues.TryGetValue(dp, out var styleValue))
{
return styleValue;
}

if (dp.IsInherited && InheritanceParent != null)
{
return InheritanceParent.GetValue(dp);
Expand Down Expand Up @@ -95,6 +121,10 @@ public void SetValue(DependencyProperty dp, object? value)
{
_localOverridesActiveTrigger.Add(dp);
}
if (_styleTriggerValues.ContainsKey(dp))
{
_localOverridesActiveStyleTrigger.Add(dp);
}

OnPropertyChanged(dp);
}
Expand All @@ -107,6 +137,7 @@ public void ClearValue(DependencyProperty dp)
// Even when no local value was present, removing the override flag can expose a
// different effective value (the trigger's), so treat it as a change.
changed = changed || (_localOverridesActiveTrigger.Remove(dp) && _triggerValues.ContainsKey(dp));
changed = changed || (_localOverridesActiveStyleTrigger.Remove(dp) && _styleTriggerValues.ContainsKey(dp));
if (changed)
{
OnPropertyChanged(dp);
Expand Down Expand Up @@ -157,6 +188,51 @@ internal void ClearTriggerValue(DependencyProperty dp)
}
}

internal void SetStyleValue(DependencyProperty dp, object? value)
{
value = CoerceLegacyColor(dp, value);

if (value != null && !dp.PropertyType.IsInstanceOfType(value))
{
throw new ArgumentException($"Value of type {value.GetType()} is not assignable to property {dp.Name} of type {dp.PropertyType}");
}

_styleValues[dp] = value ?? null!;
OnPropertyChanged(dp);
}

internal void ClearAllStyleValues()
{
var keys = new List<DependencyProperty>(_styleValues.Keys);
_styleValues.Clear();
foreach (var key in keys)
{
OnPropertyChanged(key);
}
}

internal void SetStyleTriggerValue(DependencyProperty dp, object? value)
{
value = CoerceLegacyColor(dp, value);

if (value != null && !dp.PropertyType.IsInstanceOfType(value))
{
throw new ArgumentException($"Value of type {value.GetType()} is not assignable to property {dp.Name} of type {dp.PropertyType}");
}

_styleTriggerValues[dp] = value ?? null!;
OnPropertyChanged(dp);
}

internal void ClearStyleTriggerValue(DependencyProperty dp)
{
if (_styleTriggerValues.Remove(dp))
{
_localOverridesActiveStyleTrigger.Remove(dp);
OnPropertyChanged(dp);
}
}

protected virtual void OnPropertyChanged(DependencyProperty dp)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(dp.Name));
Expand Down
20 changes: 20 additions & 0 deletions src/Tedd.TUI/Style.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;

namespace Tedd.TUI;

public class Style
{
public Type? TargetType { get; set; }
public List<Setter> Setters { get; } = new List<Setter>();
public List<TriggerBase> Triggers { get; } = new List<TriggerBase>();

public Style()
{
}

public Style(Type targetType)
{
TargetType = targetType;
}
}
Loading
Loading