diff --git a/.jules/forge.md b/.jules/forge.md index c75ffb0..2524da6 100644 --- a/.jules/forge.md +++ b/.jules/forge.md @@ -92,3 +92,6 @@ ## 2026-03-09 - ControlTemplate Trigger Integration **Observation:** The TUI framework lacked standard visual state infrastructure within `ControlTemplate`. There was an absence of WPF parity regarding `Trigger` mechanisms, which dynamically evaluate dependency properties and apply visual state updates (via `Setter`) without imperative event wiring. **Strategic Action:** Integrated WPF visual state triggers by establishing the `TriggerBase`, `Trigger`, and `Setter` object model. Extended `ControlTemplate` with a `Triggers` collection. Augmented `Control.OnPropertyChanged` to intercept dependency property mutations, evaluate active trigger conditions (`EvaluateTriggers`), dynamically inject setter values when conditions are met, and automatically revert to original local/inherited property states (`DependencyProperty.UnsetValue`) when conditions fail. +## 2026-03-09 - ListBox WPF Isomorphism Parity +**Observation:** The TUI framework's `ListBox` component lacked architectural parity with standard WPF models. It directly implemented scrolling, item measurement, and rendering (`Render`, `MeasureOverride`, `ArrangeOverride`) instead of utilizing the `ItemsControl` templating paradigms (`ControlTemplate`, `ItemsPresenter`, `ScrollViewer`, and generated containers). +**Strategic Action:** Refactored `ListBox` to use a `ControlTemplate` wrapping an `ItemsPresenter` within a `ScrollViewer`. Created the `ListBoxItem` component (inheriting from `ContentControl`) to act as the generated item container. Implemented `Selected`/`Unselected` bubbling routed events on `ListBoxItem` to decouple selection synchronization, allowing `ListBox` to map UI interactions directly to its `SelectedIndex` property, establishing strict XAML structural isomorphism. diff --git a/src/Tedd.TUI.Tests/ListBoxMeasureTests.cs b/src/Tedd.TUI.Tests/ListBoxMeasureTests.cs index aa119bc..cca2f86 100644 --- a/src/Tedd.TUI.Tests/ListBoxMeasureTests.cs +++ b/src/Tedd.TUI.Tests/ListBoxMeasureTests.cs @@ -14,6 +14,7 @@ public void Measure_FixedHeight_UsesFixedHeight() listBox.Height = 10; listBox.Items.Add("Item 1"); + listBox.ApplyTemplate(); listBox.Measure(new Size(100, 100)); Assert.Equal(10, listBox.DesiredSize.Height); @@ -28,6 +29,7 @@ public void Measure_AutoHeight_UsesItemCount() listBox.Items.Add("Item 2"); listBox.Items.Add("Item 3"); + listBox.ApplyTemplate(); // Available size is large enough listBox.Measure(new Size(100, 100)); @@ -45,6 +47,7 @@ public void Measure_AutoHeight_ConstrainedByAvailableSize() listBox.Items.Add($"Item {i}"); } + listBox.ApplyTemplate(); // Available size is smaller (5) listBox.Measure(new Size(100, 5)); @@ -59,6 +62,7 @@ public void Measure_FixedWidth_UsesFixedWidth() listBox.Width = 20; listBox.Items.Add("Long Item Name"); + listBox.ApplyTemplate(); listBox.Measure(new Size(100, 100)); Assert.Equal(20, listBox.DesiredSize.Width); @@ -72,11 +76,14 @@ public void Measure_AutoWidth_UsesMaxItemWidth() listBox.Items.Add("Short"); listBox.Items.Add("Long Item Name"); // Length 14 + listBox.ApplyTemplate(); // Available size is large enough listBox.Measure(new Size(100, 100)); - // Expect width to accommodate "Long Item Name" (14) - Assert.Equal(14, listBox.DesiredSize.Width); + // ScrollViewer is set to VerticalScrollBarVisibility = true. + // This adds 1 to the width for the ScrollBar. + // Expect width to accommodate "Long Item Name" (14) + ScrollBar (1) = 15 + Assert.Equal(15, listBox.DesiredSize.Width); } [Fact] @@ -90,12 +97,14 @@ public void Measure_AutoWidth_WithScrollbar() listBox.Items.Add("Item"); // Length 4 } + listBox.ApplyTemplate(); // Available height 5, so scrollbar needed. listBox.Measure(new Size(100, 5)); // Height should be 5. Assert.Equal(5, listBox.DesiredSize.Height); + // ScrollViewer has VerticalScrollBarVisibility = true, so ScrollBar is always 1 // Width should be 4 (Item) + 1 (ScrollBar) = 5. Assert.Equal(5, listBox.DesiredSize.Width); } diff --git a/src/Tedd.TUI.Tests/ListBoxTests.cs b/src/Tedd.TUI.Tests/ListBoxTests.cs index f048693..bf11e75 100644 --- a/src/Tedd.TUI.Tests/ListBoxTests.cs +++ b/src/Tedd.TUI.Tests/ListBoxTests.cs @@ -80,6 +80,66 @@ public void Render_UsesItemTemplate_WhenSet() Assert.Equal("Bob", row1); } + [Fact] + public void OnGotFocus_UpdatesSelectedItemVisualState() + { + // Arrange + var window = new TuiWindow(); + var listBox = new ListBox(); + window.Content = listBox; + listBox.Items.Add("Item 1"); + listBox.Items.Add("Item 2"); + listBox.SelectedIndex = 0; + listBox.Measure(new Size(20, 5)); + listBox.Arrange(new Rect(0, 0, 20, 5)); + + var container = listBox.ItemsPanelRoot?.Children[0] as ListBoxItem; + Assert.NotNull(container); + Assert.True(container.IsSelected); + + // Before focus, item should show unfocused selection colors + listBox.OnLostFocus(); + var unfocusedBg = container.Background; + var unfocusedFg = container.Foreground; + + // Act: ListBox gains focus + listBox.OnGotFocus(); + + // Assert: selected item should now show focused selection colors + Assert.NotEqual(unfocusedBg, container.Background); + Assert.NotEqual(unfocusedFg, container.Foreground); + Assert.Equal(listBox.FocusedSelectionBackground, container.Background); + Assert.Equal(listBox.FocusedSelectionForeground, container.Foreground); + } + + [Fact] + public void OnLostFocus_UpdatesSelectedItemVisualState() + { + // Arrange + var window = new TuiWindow(); + var listBox = new ListBox(); + window.Content = listBox; + listBox.Items.Add("Item 1"); + listBox.Items.Add("Item 2"); + listBox.SelectedIndex = 0; + listBox.Measure(new Size(20, 5)); + listBox.Arrange(new Rect(0, 0, 20, 5)); + + var container = listBox.ItemsPanelRoot?.Children[0] as ListBoxItem; + Assert.NotNull(container); + + // First give focus so item shows focused selection colors + listBox.OnGotFocus(); + Assert.Equal(listBox.FocusedSelectionBackground, container.Background); + + // Act: ListBox loses focus + listBox.OnLostFocus(); + + // Assert: selected item should now show unfocused selection colors (ShowSelection=true by default) + Assert.Equal(listBox.SelectionBackground, container.Background); + Assert.Equal(listBox.SelectionForeground, container.Foreground); + } + [Fact] public void Render_FallsBackToGetItemText_WhenNoTemplate() { @@ -101,4 +161,119 @@ public void Render_FallsBackToGetItemText_WhenNoTemplate() Assert.Equal("Hello", row0); Assert.Equal("World", row1); } + + [Fact] + public void ItemIsSelected_ViaIsSelectedProperty_UpdatesListBoxSelectedIndex() + { + var listBox = new ListBox(); + listBox.Items.Add("Item 1"); + listBox.Items.Add("Item 2"); + listBox.Items.Add("Item 3"); + + // ItemsPanelRoot is populated after items are added (via OnItemsCollectionChanged → PopulatePanel) + Assert.NotNull(listBox.ItemsPanelRoot); + var container1 = listBox.ItemsPanelRoot!.Children[1] as ListBoxItem; + Assert.NotNull(container1); + + // Act: set IsSelected = true on the second container; this raises SelectedEvent which bubbles to ListBox + container1!.IsSelected = true; + + // Assert: SelectedIndex should be updated to match the container's position + Assert.Equal(1, listBox.SelectedIndex); + } + + [Fact] + public void ItemSelectedEvent_RaisedOnContainer_UpdatesListBoxSelectedIndex() + { + var listBox = new ListBox(); + listBox.Items.Add("A"); + listBox.Items.Add("B"); + listBox.Items.Add("C"); + + Assert.NotNull(listBox.ItemsPanelRoot); + var container2 = listBox.ItemsPanelRoot!.Children[2] as ListBoxItem; + Assert.NotNull(container2); + + // Act: raise SelectedEvent directly on the third container + container2!.RaiseEvent(new RoutedEventArgs(ListBoxItem.SelectedEvent, container2)); + + // Assert: SelectedIndex should be updated to 2 + Assert.Equal(2, listBox.SelectedIndex); + } + + [Fact] + public void SelectedIndex_Change_Updates_ContainersIsSelected() + { + var listBox = new ListBox(); + listBox.Items.Add("Alpha"); + listBox.Items.Add("Beta"); + listBox.Items.Add("Gamma"); + + Assert.NotNull(listBox.ItemsPanelRoot); + + // Act: select the second item + listBox.SelectedIndex = 1; + + var container0 = listBox.ItemsPanelRoot!.Children[0] as ListBoxItem; + var container1 = listBox.ItemsPanelRoot!.Children[1] as ListBoxItem; + var container2 = listBox.ItemsPanelRoot!.Children[2] as ListBoxItem; + + Assert.NotNull(container0); + Assert.NotNull(container1); + Assert.NotNull(container2); + + Assert.False(container0!.IsSelected); + Assert.True(container1!.IsSelected); + Assert.False(container2!.IsSelected); + } + + [Fact] + public void SelectedIndex_Change_Deselects_PreviousContainer() + { + var listBox = new ListBox(); + listBox.Items.Add("X"); + listBox.Items.Add("Y"); + listBox.Items.Add("Z"); + + Assert.NotNull(listBox.ItemsPanelRoot); + + // Select the first item + listBox.SelectedIndex = 0; + var container0 = listBox.ItemsPanelRoot!.Children[0] as ListBoxItem; + var container1 = listBox.ItemsPanelRoot!.Children[1] as ListBoxItem; + Assert.NotNull(container0); + Assert.NotNull(container1); + Assert.True(container0!.IsSelected); + Assert.False(container1!.IsSelected); + + // Act: change selection to the second item + listBox.SelectedIndex = 1; + + // Previous container should be deselected, new one selected + Assert.False(container0.IsSelected); + Assert.True(container1.IsSelected); + } + + [Fact] + public void SelectedIndex_MinusOne_Deselects_AllContainers() + { + var listBox = new ListBox(); + listBox.Items.Add("One"); + listBox.Items.Add("Two"); + + Assert.NotNull(listBox.ItemsPanelRoot); + + listBox.SelectedIndex = 0; + var container0 = listBox.ItemsPanelRoot!.Children[0] as ListBoxItem; + var container1 = listBox.ItemsPanelRoot!.Children[1] as ListBoxItem; + Assert.NotNull(container0); + Assert.NotNull(container1); + Assert.True(container0!.IsSelected); + + // Act: clear selection + listBox.SelectedIndex = -1; + + Assert.False(container0.IsSelected); + Assert.False(container1!.IsSelected); + } } diff --git a/src/Tedd.TUI/ListBox.cs b/src/Tedd.TUI/ListBox.cs index 580dcbb..255289f 100644 --- a/src/Tedd.TUI/ListBox.cs +++ b/src/Tedd.TUI/ListBox.cs @@ -5,26 +5,42 @@ namespace Tedd.TUI; public class ListBox : Selector { - private readonly ScrollBar _scrollBar; - public ListBox() { Focusable = true; - _scrollBar = new ScrollBar() - { - Orientation = Orientation.Vertical, - Width = 1 - }; - _scrollBar.Parent = this; - _scrollBar.ValueChanged += OnScroll; Foreground = ConsoleColor.Gray; + + Template = new ControlTemplate(parent => + { + var sv = new ScrollViewer + { + VerticalScrollBarVisibility = true, + HorizontalScrollBarVisibility = false // TUI ListBox usually does not scroll horizontally by default + }; + + var ip = new ItemsPresenter(); + ip.TemplatedParent = parent; + + sv.Content = ip; + + return sv; + }); + + // Listen to SelectedEvent from children to update selection + AddHandler(ListBoxItem.SelectedEvent, new RoutedEventHandler(OnItemSelected)); } - private void OnScroll(object? sender, EventArgs e) + private void OnItemSelected(object? sender, RoutedEventArgs e) { - _scrollOffset = _scrollBar.Value; - Invalidate(); + if (e.OriginalSource is ListBoxItem item) + { + int index = ItemsPanelRoot?.Children.IndexOf(item) ?? -1; + if (index >= 0 && index != SelectedIndex) + { + SelectedIndex = index; + } + } } /// @@ -71,213 +87,85 @@ public ConsoleColor FocusedSelectionBackground set => SetValue(FocusedSelectionBackgroundProperty, value); } - private int _scrollOffset = 0; - - public override int VisualChildrenCount => 1; - public override UIElement GetVisualChild(int index) + protected internal override bool IsItemItsOwnContainerOverride(object item) { - if (index == 0) return _scrollBar; - throw new ArgumentOutOfRangeException(nameof(index)); + return item is ListBoxItem; } - protected override Size MeasureOverride(Size availableSize) + protected internal override UIElement GetContainerForItemOverride() { - // 1. Calculate Height - int h; - if (Height >= 0) - { - h = Height; - } - else - { - // Auto Height - h = Items.Count; - // Constrain to available space - if (h > availableSize.Height) h = availableSize.Height; - } - - // 2. Determine if ScrollBar is needed - bool showScroll = Items.Count > h; - - // 3. Configure ScrollBar - if (showScroll) - { - _scrollBar.Measure(new Size(1, h)); - _scrollBar.Maximum = Math.Max(0, Items.Count - h); - _scrollBar.ViewportSize = h; - _scrollBar.Value = _scrollOffset; - _scrollBar.Visibility = true; - } - else - { - _scrollBar.Visibility = false; - } - - // 4. Calculate Width - int w; - if (Width >= 0) - { - w = Width; - } - else - { - // Auto Width - int maxLen = 0; - foreach (var item in Items) - { - var s = GetItemText(item); - if (!string.IsNullOrEmpty(s)) - { - if (s.Length > maxLen) maxLen = s.Length; - } - } - w = maxLen; - if (showScroll) w++; - - // Constrain - if (w > availableSize.Width) w = availableSize.Width; - } - - return new Size(w, h); + return new ListBoxItem(); } - protected override void ArrangeOverride(Size finalSize) + protected internal override void PrepareContainerForItemOverride(UIElement element, object item) { - if (_scrollBar.Visibility) + base.PrepareContainerForItemOverride(element, item); + if (element is ListBoxItem lbi) { - _scrollBar.Arrange(new Rect(finalSize.Width - 1, 0, 1, finalSize.Height)); - } - } - - public override void Render(VirtualBuffer buffer, int offsetX, int offsetY) - { - int x = RenderSize.X + offsetX; - int y = RenderSize.Y + offsetY; - int w = RenderSize.Width; - int h = RenderSize.Height; - - // Draw items. - // If ScrollBar visible, effective width is w - 1 - int effectiveW = _scrollBar.Visibility ? w - 1 : w; - - // Ensure scroll offset is valid - if (_scrollOffset > Items.Count - h) _scrollOffset = Math.Max(0, Items.Count - h); - _scrollBar.Value = _scrollOffset; // Sync if clamped - - for (int i = 0; i < h; i++) - { - int itemIndex = i + _scrollOffset; - - // Clear line - for (int dx = 0; dx < effectiveW; dx++) + // Set content correctly based on ItemTemplate or fallback + if (ItemTemplate != null) { - var pixelBg = Background ?? buffer.GetPixel(x + dx, y + i).Background; - buffer.SetPixel(x + dx, y + i, ' ', ConsoleColor.White, pixelBg); + lbi.ContentTemplate = ItemTemplate; + lbi.Content = item; } - - if (itemIndex < Items.Count) + else if (item is UIElement uiElement) { - bool isSelected = (itemIndex == SelectedIndex); - var bg = Background ?? buffer.GetPixel(x, y + i).Background; - var fg = Foreground; - if (isSelected) - { - if (IsFocused) - { - // Focused: selected item is blue - bg = FocusedSelectionBackground; - fg = FocusedSelectionForeground; - } - else if (ShowSelection) - { - // Not focused but ShowSelection enabled: inverted black/white - bg = SelectionBackground; - fg = SelectionForeground; - } - // else: ShowSelection is false and not focused, use default colors - } + // Preserve UIElement items as content so they render and interact correctly + lbi.Content = uiElement; + } + else + { + lbi.Content = GetItemText(item); + } - if (ItemTemplate != null) - { - // Fill row with selection background first, then render template content on top - for (int dx = 0; dx < effectiveW; dx++) - { - buffer.SetPixel(x + dx, y + i, ' ', fg, bg); - } - var container = GetContainerForItemCore(); - PrepareContainerForItemOverride(container, Items[itemIndex]); - container.Measure(new Size(effectiveW, 1)); - container.Arrange(new Rect(0, 0, effectiveW, 1)); - container.Render(buffer, x, y + i); - } - else - { - string content = GetItemText(Items[itemIndex]); - if (content.Length > effectiveW) content = content.Substring(0, effectiveW); - - for (int dx = 0; dx < content.Length; dx++) - { - buffer.SetPixel(x + dx, y + i, content[dx], fg, bg); - } - // Fill rest of line with bg - for (int dx = content.Length; dx < effectiveW; dx++) - { - buffer.SetPixel(x + dx, y + i, ' ', fg, bg); - } - } + // Sync IsSelected + int index = Items.IndexOf(item); + if (index == SelectedIndex) + { + lbi.IsSelected = true; + } + else + { + lbi.IsSelected = false; } } + } - if (_scrollBar.Visibility) - { - _scrollBar.Render(buffer, x, y); - } + public override void OnGotFocus() + { + base.OnGotFocus(); + NotifyContainersVisualStateChanged(); } - public override void OnMouseDown(MouseEventArgs e) + public override void OnLostFocus() { - base.OnMouseDown(e); - Focus(); + base.OnLostFocus(); + NotifyContainersVisualStateChanged(); + } - // Check if ScrollBar hit - if (_scrollBar.Visibility && e.X >= RenderSize.Width - 1) + private void NotifyContainersVisualStateChanged() + { + if (ItemsPanelRoot != null) { - // Pass to ScrollBar. - // We need to pass local coordinates to ScrollBar. - // ScrollBar is at (Width-1, 0). - // So localX = e.X - (Width-1) = 0 usually. - - var sbArgs = new MouseEventArgs + for (int i = 0; i < ItemsPanelRoot.Children.Count; i++) { - X = e.X - (RenderSize.Width - 1), - Y = e.Y, - Handled = false - }; - _scrollBar.OnMouseDown(sbArgs); - e.Handled = true; - return; - } - - // e.Y is already local relative to this control - int itemIndex = e.Y + _scrollOffset; - - if (itemIndex >= 0 && itemIndex < Items.Count) - { - SelectedIndex = itemIndex; - // SelectionChanged is raised by base.SelectedIndex setter + if (ItemsPanelRoot.Children[i] is ListBoxItem lbi) + lbi.UpdateVisualState(); + } } - e.Handled = true; } public override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); + if (e.Handled) return; + if (e.Key == ConsoleKey.UpArrow) { if (SelectedIndex > 0) { SelectedIndex--; - EnsureVisible(SelectedIndex); + EnsureItemVisible(SelectedIndex); } e.Handled = true; } @@ -286,7 +174,7 @@ public override void OnKeyDown(KeyEventArgs e) if (SelectedIndex < Items.Count - 1) { SelectedIndex++; - EnsureVisible(SelectedIndex); + EnsureItemVisible(SelectedIndex); } e.Handled = true; } @@ -297,17 +185,44 @@ public override void OnKeyDown(KeyEventArgs e) } } - private void EnsureVisible(int index) + private void EnsureItemVisible(int index) { - if (index < _scrollOffset) + // Find ScrollViewer inside the template + if (TemplateRoot is ScrollViewer sv) { - _scrollOffset = index; + // A simple way to scroll into view based on index. + // In WPF, we would call BringIntoView on the item. + // Here, we can just manipulate the scrollviewer. + int offset = sv.VerticalOffset; + int viewport = sv.RenderSize.Height; // Approximate viewport size + + if (index < offset) + { + sv.ScrollToVerticalOffset(index); + } + else if (index >= offset + viewport) + { + sv.ScrollToVerticalOffset(index - viewport + 1); + } } - else if (index >= _scrollOffset + RenderSize.Height) + } + + // In Selector, OnSelectionChanged is fired when SelectedIndex/SelectedItem changes. + // We override it to sync IsSelected to the containers. + protected override void OnSelectionChanged() + { + base.OnSelectionChanged(); + + if (ItemsPanelRoot != null) { - _scrollOffset = index - RenderSize.Height + 1; + for (int i = 0; i < ItemsPanelRoot.Children.Count; i++) + { + if (ItemsPanelRoot.Children[i] is ListBoxItem lbi) + { + lbi.IsSelected = (i == SelectedIndex); + } + } } - _scrollBar.Value = _scrollOffset; Invalidate(); } } diff --git a/src/Tedd.TUI/ListBoxItem.cs b/src/Tedd.TUI/ListBoxItem.cs new file mode 100644 index 0000000..e1fa9bb --- /dev/null +++ b/src/Tedd.TUI/ListBoxItem.cs @@ -0,0 +1,142 @@ +using System; + +namespace Tedd.TUI; + +public class ListBoxItem : ContentControl +{ + public static readonly DependencyProperty IsSelectedProperty = + DependencyProperty.Register("IsSelected", typeof(bool), typeof(ListBoxItem), false); + + public bool IsSelected + { + get => (bool)GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + public static readonly RoutedEvent SelectedEvent = RoutedEvent.Register( + "Selected", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ListBoxItem)); + + public static readonly RoutedEvent UnselectedEvent = RoutedEvent.Register( + "Unselected", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ListBoxItem)); + + public event RoutedEventHandler Selected + { + add => AddHandler(SelectedEvent, value); + remove => RemoveHandler(SelectedEvent, value); + } + + public event RoutedEventHandler Unselected + { + add => AddHandler(UnselectedEvent, value); + remove => RemoveHandler(UnselectedEvent, value); + } + + public ListBoxItem() + { + Focusable = true; + } + + protected override void OnPropertyChanged(DependencyProperty dp) + { + base.OnPropertyChanged(dp); + if (dp == IsSelectedProperty || dp == IsFocusedProperty) + { + UpdateVisualState(); + } + + if (dp == IsSelectedProperty) + { + bool isSelected = (bool)GetValue(IsSelectedProperty); + if (isSelected) + { + RaiseEvent(new RoutedEventArgs(SelectedEvent, this)); + } + else + { + RaiseEvent(new RoutedEventArgs(UnselectedEvent, this)); + } + } + } + + internal void UpdateVisualState() + { + // Try to get colors from Parent ListBox if we are inside one. + // If not, use defaults. + var parentListBox = Parent as ListBox; + + // Sometimes parent is not set yet, so search up the visual tree + if (parentListBox == null) + { + var curr = Parent; + while(curr != null && !(curr is ListBox)) + { + curr = curr.Parent; + } + parentListBox = curr as ListBox; + } + + ConsoleColor selectedBg = parentListBox?.SelectionBackground ?? ConsoleColor.White; + ConsoleColor selectedFg = parentListBox?.SelectionForeground ?? ConsoleColor.Black; + + ConsoleColor focusedSelectedBg = parentListBox?.FocusedSelectionBackground ?? ConsoleColor.Blue; + ConsoleColor focusedSelectedFg = parentListBox?.FocusedSelectionForeground ?? ConsoleColor.White; + + ConsoleColor normalBg = parentListBox?.Background ?? ConsoleColor.Black; + ConsoleColor normalFg = parentListBox?.Foreground ?? ConsoleColor.Gray; + + bool showSelection = parentListBox?.ShowSelection ?? true; + + if (IsSelected) + { + if (IsFocused || (parentListBox != null && parentListBox.IsFocused)) + { + // Note: WPF ListBoxItem is focused when selected usually. + // Or if ListBox is focused. + Background = focusedSelectedBg; + Foreground = focusedSelectedFg; + } + else if (showSelection) + { + Background = selectedBg; + Foreground = selectedFg; + } + else + { + Background = normalBg; + Foreground = normalFg; + } + } + else + { + Background = normalBg; + Foreground = normalFg; + } + } + + public override void OnGotFocus() + { + base.OnGotFocus(); + if (!IsSelected) + { + IsSelected = true; + } + UpdateVisualState(); + } + + public override void OnLostFocus() + { + base.OnLostFocus(); + UpdateVisualState(); + } + + public override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + Focus(); + if (!IsSelected) + { + IsSelected = true; + } + e.Handled = true; + } +}