Compare commits

1 Commits
master ... qwen

Author SHA1 Message Date
912daa6069 - add: sort think 2026-04-12 19:15:09 +03:00
23 changed files with 1440 additions and 830 deletions

View File

@@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="InternalSortMethods.Android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise">
<file url="file://$PROJECT_DIR$/InternalSortMethods/ViewModels/MainViewModel.cs" charset="UTF-8" />
</component>
</project>

View File

@@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="androidx.annotation.annotation-jvm">
<CLASSES>
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.annotation.jvm/1.9.1.7/jar/androidx.annotation.annotation-jvm.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

13
.qwen/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(mkdir *)",
"Bash(dotnet *)",
"Bash(powershell *)",
"Bash(del *)",
"Bash(rmdir *)",
"Bash(findstr *)"
]
},
"$version": 3
}

7
.qwen/settings.json.orig Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(mkdir *)"
]
}
}

View File

@@ -10,34 +10,52 @@
<Application.Styles>
<FluentTheme />
<Style Selector="Window">
<!-- Liquid Glass стили встроены напрямую -->
<Style Selector="Button.glass-btn">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#0F0F1A" Offset="0"/>
<GradientStop Color="#1A1A2E" Offset="0.5"/>
<GradientStop Color="#16213E" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#ffffff15" Offset="0.0"/>
<GradientStop Color="#ffffff0a" Offset="1.0"/>
</LinearGradientBrush>
</Setter>
<Setter Property="Foreground" Value="#f0f0f5"/>
<Setter Property="BorderBrush">
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#ffffff25" Offset="0.0"/>
<GradientStop Color="#ffffff0d" Offset="1.0"/>
</LinearGradientBrush>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="16,10"/>
<Style Selector="^:disabled">
<Setter Property="Opacity" Value="0.4"/>
</Style>
</Style>
<Style Selector="ComboBox">
<Setter Property="Background" Value="#20FFFFFF"/>
<Style Selector="Button.accent-btn">
<Setter Property="Background">
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#7c5cfc" Offset="0.0"/>
<GradientStop Color="#6d28d9" Offset="1.0"/>
</LinearGradientBrush>
</Setter>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#40FFFFFF"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="MinHeight" Value="44"/>
</Style>
<Style Selector="ComboBox:pointerover /template/ Border#NormalRectangle">
<Setter Property="Background" Value="#30FFFFFF"/>
<Setter Property="BorderBrush" Value="#60FFFFFF"/>
</Style>
<Style Selector="Slider">
<Setter Property="MinHeight" Value="44"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="16,10"/>
<Style Selector="^:pointerover">
<Setter Property="Opacity" Value="0.9"/>
</Style>
<Style Selector="^:pressed">
<Setter Property="Opacity" Value="0.8"/>
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity" Value="0.4"/>
</Style>
</Style>
</Application.Styles>
</Application>
</Application>

View File

@@ -1,150 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace InternalSortMethods.Controls;
public class ArrayVisualizer : Control
{
public static readonly StyledProperty<ObservableCollection<Models.ArrayItemModel>?> ItemsProperty =
AvaloniaProperty.Register<ArrayVisualizer, ObservableCollection<Models.ArrayItemModel>?>(nameof(Items));
public ObservableCollection<Models.ArrayItemModel>? Items
{
get => GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
static ArrayVisualizer()
{
ItemsProperty.Changed.AddClassHandler<ArrayVisualizer>((x, e) => x.OnItemsChanged(e));
}
public ArrayVisualizer()
{
ClipToBounds = true;
}
private void OnItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ObservableCollection<Models.ArrayItemModel> oldCollection)
{
oldCollection.CollectionChanged -= OnCollectionChanged;
foreach (var item in oldCollection)
item.PropertyChanged -= OnItemPropertyChanged;
}
if (e.NewValue is ObservableCollection<Models.ArrayItemModel> newCollection)
{
newCollection.CollectionChanged += OnCollectionChanged;
foreach (var item in newCollection)
item.PropertyChanged += OnItemPropertyChanged;
}
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Models.ArrayItemModel item in e.NewItems)
item.PropertyChanged += OnItemPropertyChanged;
}
if (e.OldItems != null)
{
foreach (Models.ArrayItemModel item in e.OldItems)
item.PropertyChanged -= OnItemPropertyChanged;
}
InvalidateVisual();
}
private void OnItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs? e)
{
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
if (Items == null || Items.Count == 0 || Bounds.Width <= 0 || Bounds.Height <= 0)
return;
double width = Bounds.Width;
double height = Bounds.Height;
int count = Items.Count;
double barWidth = (width - (count - 1) * 2) / count;
double maxValue = 100.0;
var backgroundBrush = new SolidColorBrush(Color.Parse("#1E1E2E"));
context.DrawRectangle(backgroundBrush, null, new Rect(0, 0, width, height));
for (int i = 0; i < count; i++)
{
var item = Items[i];
double barHeight = (item.Value / maxValue) * height * 0.9;
double x = i * (barWidth + 2);
double y = height - barHeight;
var brush = GetBrushForState(item.State);
context.DrawRectangle(brush, null, new Rect(x, y, barWidth, barHeight));
}
}
private IBrush GetBrushForState(Models.ArrayItemState state)
{
return state switch
{
Models.ArrayItemState.Comparing => new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 1, RelativeUnit.Absolute),
EndPoint = new RelativePoint(0, 0, RelativeUnit.Absolute),
GradientStops =
[
new GradientStop(Color.Parse("#FF6B6B"), 0),
new GradientStop(Color.Parse("#FF8E8E"), 1)
]
},
Models.ArrayItemState.Swapping => new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 1, RelativeUnit.Absolute),
EndPoint = new RelativePoint(0, 0, RelativeUnit.Absolute),
GradientStops =
[
new GradientStop(Color.Parse("#FFE66D"), 0),
new GradientStop(Color.Parse("#FFB347"), 1)
]
},
Models.ArrayItemState.Sorted => new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 1, RelativeUnit.Absolute),
EndPoint = new RelativePoint(0, 0, RelativeUnit.Absolute),
GradientStops =
[
new GradientStop(Color.Parse("#4ADE80"), 0),
new GradientStop(Color.Parse("#22C55E"), 1)
]
},
Models.ArrayItemState.Pivot => new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 1, RelativeUnit.Absolute),
EndPoint = new RelativePoint(0, 0, RelativeUnit.Absolute),
GradientStops =
[
new GradientStop(Color.Parse("#C084FC"), 0),
new GradientStop(Color.Parse("#A855F7"), 1)
]
},
_ => new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 1, RelativeUnit.Absolute),
EndPoint = new RelativePoint(0, 0, RelativeUnit.Absolute),
GradientStops =
[
new GradientStop(Color.Parse("#6366F1"), 0),
new GradientStop(Color.Parse("#4F46E5"), 1)
]
}
};
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using InternalSortMethods.ViewModels;
namespace InternalSortMethods.Converters;
/// <summary>
/// Конвертирует SortItemState в соответствующий цвет столбца.
/// Используется в DataTemplate для визуализации.
/// </summary>
public class StateToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not SortItemState state)
return Colors.Transparent;
return state switch
{
SortItemState.Default => Color.Parse("#818cf8"), // Индиго
SortItemState.Comparing => Color.Parse("#fbbf24"), // Жёлтый
SortItemState.Swapping => Color.Parse("#f87171"), // Красный
SortItemState.Writing => Color.Parse("#22d3ee"), // Голубой
SortItemState.Pivot => Color.Parse("#c084fc"), // Фиолетовый
SortItemState.Sorted => Color.Parse("#34d399"), // Зелёный
_ => Color.Parse("#818cf8")
};
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,73 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Сортировка пузырьком (Bubble Sort).
/// Последовательно сравнивает соседние элементы и меняет их, если они стоят в неверном порядке.
/// Сложность: O(n²) по времени, O(1) по памяти.
/// </summary>
public sealed class BubbleSort : SortAlgorithm
{
public override string DisplayName => "Bubble Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
bool swapped = false;
for (int j = 0; j < n - i - 1; j++)
{
cancellationToken.ThrowIfCancellationRequested();
// Шаг сравнения
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [j, j + 1],
ArrayState = (int[])arr.Clone()
};
if (arr[j] > arr[j + 1])
{
// Перестановка
(arr[j], arr[j + 1]) = (arr[j + 1], arr[j]);
swapped = true;
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [j, j + 1],
ArrayState = (int[])arr.Clone()
};
}
}
// Элемент n-i-1 встал на своё место
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [n - i - 1],
ArrayState = (int[])arr.Clone()
};
if (!swapped) break; // Массив отсортирован
}
// Помечаем все элементы как отсортированные
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = Enumerable.Range(0, n).ToArray(),
ArrayState = (int[])arr.Clone()
};
}
}

View File

@@ -0,0 +1,121 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Пирамидальная сортировка (Heap Sort).
/// Строит максимальную кучу, затем извлекает корень и восстанавливает кучу.
/// Сложность: O(n log n) по времени, O(1) по памяти.
/// </summary>
public sealed class HeapSort : SortAlgorithm
{
public override string DisplayName => "Heap Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
int n = arr.Length;
// Построение кучи (heapify)
for (int i = n / 2 - 1; i >= 0; i--)
{
foreach (var step in Heapify(arr, n, i, cancellationToken))
yield return step;
}
// Извлечение элементов из кучи
for (int i = n - 1; i > 0; i--)
{
cancellationToken.ThrowIfCancellationRequested();
// Перемещаем корень (максимум) в конец
(arr[0], arr[i]) = (arr[i], arr[0]);
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [0, i],
ArrayState = (int[])arr.Clone()
};
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [i],
ArrayState = (int[])arr.Clone()
};
// Восстановление кучи
foreach (var step in Heapify(arr, i, 0, cancellationToken))
yield return step;
}
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [0],
ArrayState = (int[])arr.Clone()
};
}
/// <summary>
/// Восстанавливает свойство максимальной кучи для поддерева с корнем в index.
/// </summary>
private IEnumerable<SortStep> Heapify(int[] arr, int n, int index, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
int largest = index;
int left = 2 * index + 1;
int right = 2 * index + 2;
// Сравнение с левым потомком
if (left < n)
{
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [largest, left],
ArrayState = (int[])arr.Clone()
};
if (arr[left] > arr[largest])
largest = left;
}
// Сравнение с правым потомком
if (right < n)
{
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [largest, right],
ArrayState = (int[])arr.Clone()
};
if (arr[right] > arr[largest])
largest = right;
}
// Перестановка, если корень не максимум
if (largest != index)
{
(arr[index], arr[largest]) = (arr[largest], arr[index]);
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [index, largest],
ArrayState = (int[])arr.Clone()
};
// Рекурсивный heapify для затронутого поддерева
foreach (var step in Heapify(arr, n, largest, ct))
yield return step;
}
}
}

View File

@@ -0,0 +1,88 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Сортировка вставками (Insertion Sort).
/// Вставляет каждый элемент в уже отсортированную часть массива.
/// Сложность: O(n²) по времени, O(1) по памяти.
/// </summary>
public sealed class InsertionSort : SortAlgorithm
{
public override string DisplayName => "Insertion Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
int n = arr.Length;
// Первый элемент считается отсортированным
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [0],
ArrayState = (int[])arr.Clone()
};
for (int i = 1; i < n; i++)
{
int key = arr[i];
int j = i - 1;
// Подсвечиваем вставляемый элемент
yield return new SortStep
{
Type = SortStepType.Pivot,
Indices = [i],
ArrayState = (int[])arr.Clone()
};
while (j >= 0 && arr[j] > key)
{
cancellationToken.ThrowIfCancellationRequested();
// Сравнение
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [j, i],
ArrayState = (int[])arr.Clone()
};
// Сдвиг элемента вправо
arr[j + 1] = arr[j];
yield return new SortStep
{
Type = SortStepType.Write,
Indices = [j + 1],
ArrayState = (int[])arr.Clone()
};
j--;
}
// Вставка ключа на правильную позицию
arr[j + 1] = key;
yield return new SortStep
{
Type = SortStepType.Write,
Indices = [j + 1],
ArrayState = (int[])arr.Clone()
};
// Элементы до i включительно отсортированы
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = Enumerable.Range(0, i + 1).ToArray(),
ArrayState = (int[])arr.Clone()
};
}
}
}

View File

@@ -0,0 +1,140 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Сортировка слиянием (Merge Sort).
/// Рекурсивно делит массив пополам, сортирует части и сливает их.
/// Сложность: O(n log n) по времени, O(n) по памяти.
/// </summary>
public sealed class MergeSort : SortAlgorithm
{
public override string DisplayName => "Merge Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
var steps = MergeSortRecursive(arr, 0, arr.Length - 1, cancellationToken).ToList();
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = Enumerable.Range(0, arr.Length).ToArray(),
ArrayState = (int[])arr.Clone()
};
}
private IEnumerable<SortStep> MergeSortRecursive(int[] arr, int left, int right, CancellationToken ct)
{
if (left >= right)
{
if (left == right)
{
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [left],
ArrayState = (int[])arr.Clone()
};
}
yield break;
}
ct.ThrowIfCancellationRequested();
int mid = left + (right - left) / 2;
foreach (var step in MergeSortRecursive(arr, left, mid, ct))
yield return step;
foreach (var step in MergeSortRecursive(arr, mid + 1, right, ct))
yield return step;
foreach (var step in Merge(arr, left, mid, right, ct))
yield return step;
}
private IEnumerable<SortStep> Merge(int[] arr, int left, int mid, int right, CancellationToken ct)
{
int n1 = mid - left + 1;
int n2 = right - mid;
var L = new int[n1];
var R = new int[n2];
Array.Copy(arr, left, L, 0, n1);
Array.Copy(arr, mid + 1, R, 0, n2);
int i = 0, j = 0, k = left;
while (i < n1 && j < n2)
{
ct.ThrowIfCancellationRequested();
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [left + i, mid + 1 + j],
ArrayState = (int[])arr.Clone()
};
if (L[i] <= R[j])
{
arr[k] = L[i];
i++;
}
else
{
arr[k] = R[j];
j++;
}
yield return new SortStep
{
Type = SortStepType.Write,
Indices = [k],
ArrayState = (int[])arr.Clone()
};
k++;
}
while (i < n1)
{
ct.ThrowIfCancellationRequested();
arr[k] = L[i];
yield return new SortStep
{
Type = SortStepType.Write,
Indices = [k],
ArrayState = (int[])arr.Clone()
};
i++;
k++;
}
while (j < n2)
{
ct.ThrowIfCancellationRequested();
arr[k] = R[j];
yield return new SortStep
{
Type = SortStepType.Write,
Indices = [k],
ArrayState = (int[])arr.Clone()
};
j++;
k++;
}
}
}

View File

@@ -0,0 +1,154 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Быстрая сортировка (Quick Sort).
/// Разделяет массив по опорному элементу (pivot) и рекурсивно сортирует части.
/// Сложность: O(n log n) в среднем, O(n²) в худшем случае.
/// </summary>
public sealed class QuickSort : SortAlgorithm
{
public override string DisplayName => "Quick Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
var steps = QuickSortRecursive(arr, 0, arr.Length - 1, cancellationToken).ToList();
// Помечаем все элементы как отсортированные в конце
steps.Add(new SortStep
{
Type = SortStepType.Sorted,
Indices = Enumerable.Range(0, arr.Length).ToArray(),
ArrayState = (int[])arr.Clone()
});
return steps;
}
private IEnumerable<SortStep> QuickSortRecursive(int[] arr, int low, int high, CancellationToken ct)
{
if (low < high)
{
ct.ThrowIfCancellationRequested();
// Разбиение и получение опорного индекса
foreach (var step in PartitionSteps(arr, low, high, ct))
{
yield return step;
}
// Определяем позицию pivot после разбиения
int pivotIdx = FindPivotIndex(arr, low, high);
// Рекурсивная сортировка левой и правой частей
foreach (var step in QuickSortRecursive(arr, low, pivotIdx - 1, ct))
yield return step;
foreach (var step in QuickSortRecursive(arr, pivotIdx + 1, high, ct))
yield return step;
}
else if (low == high)
{
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [low],
ArrayState = (int[])arr.Clone()
};
}
}
private int FindPivotIndex(int[] arr, int low, int high)
{
// После partition pivot стоит на правильном месте
// Ищем элемент, который уже на своём месте
for (int i = low; i <= high; i++)
{
bool isPivot = true;
for (int j = low; j < i; j++)
{
if (arr[j] > arr[i]) { isPivot = false; break; }
}
for (int j = i + 1; j <= high; j++)
{
if (arr[j] < arr[i]) { isPivot = false; break; }
}
if (isPivot) return i;
}
return low;
}
/// <summary>
/// Генерирует шаги для операции разбиения (partition) с визуализацией.
/// </summary>
private IEnumerable<SortStep> PartitionSteps(int[] arr, int low, int high, CancellationToken ct)
{
var temp = (int[])arr.Clone();
int pivot = arr[high];
int i = low - 1;
yield return new SortStep
{
Type = SortStepType.Pivot,
Indices = [high],
ArrayState = (int[])temp.Clone()
};
for (int j = low; j < high; j++)
{
ct.ThrowIfCancellationRequested();
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [j, high],
ArrayState = (int[])temp.Clone()
};
if (temp[j] <= pivot)
{
i++;
if (i != j)
{
(temp[i], temp[j]) = (temp[j], temp[i]);
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [i, j],
ArrayState = (int[])temp.Clone()
};
}
}
}
if (i + 1 != high)
{
(temp[i + 1], temp[high]) = (temp[high], temp[i + 1]);
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [i + 1, high],
ArrayState = (int[])temp.Clone()
};
}
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [i + 1],
ArrayState = (int[])temp.Clone()
};
// Копируем результат в оригинальный массив
for (int k = low; k <= high; k++)
arr[k] = temp[k];
}
}

View File

@@ -0,0 +1,83 @@
using InternalSortMethods.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Сортировка выбором (Selection Sort).
/// Находит минимальный элемент в неотсортированной части и ставит его на позицию.
/// Сложность: O(n²) по времени, O(1) по памяти.
/// </summary>
public sealed class SelectionSort : SortAlgorithm
{
public override string DisplayName => "Selection Sort";
public override IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default)
{
var arr = (int[])array.Clone();
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
int minIdx = i;
// Подсвечиваем текущий минимум
yield return new SortStep
{
Type = SortStepType.Pivot,
Indices = [minIdx],
ArrayState = (int[])arr.Clone()
};
for (int j = i + 1; j < n; j++)
{
cancellationToken.ThrowIfCancellationRequested();
// Сравнение с текущим минимумом
yield return new SortStep
{
Type = SortStepType.Compare,
Indices = [minIdx, j],
ArrayState = (int[])arr.Clone()
};
if (arr[j] < arr[minIdx])
{
minIdx = j;
}
}
// Перестановка минимума на позицию i
if (minIdx != i)
{
(arr[i], arr[minIdx]) = (arr[minIdx], arr[i]);
yield return new SortStep
{
Type = SortStepType.Swap,
Indices = [i, minIdx],
ArrayState = (int[])arr.Clone()
};
}
// Элемент i встал на своё место
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [i],
ArrayState = (int[])arr.Clone()
};
}
// Последний элемент автоматически отсортирован
yield return new SortStep
{
Type = SortStepType.Sorted,
Indices = [n - 1],
ArrayState = (int[])arr.Clone()
};
}
}

View File

@@ -0,0 +1,27 @@
using InternalSortMethods.Models;
using System.Collections.Generic;
using System.Threading;
namespace InternalSortMethods.Models;
/// <summary>
/// Базовый класс для всех алгоритмов сортировки.
/// Генерирует последовательность шагов (SortStep), которые ViewModel выполняет
/// с задержкой для анимации. Это позволяет реализовать паузу, шаг вперёд и отмену
/// без блокировки UI-потока.
/// </summary>
public abstract class SortAlgorithm
{
/// <summary>Отображаемое название алгоритма.</summary>
public abstract string DisplayName { get; }
/// <summary>
/// Генерирует шаги сортировки для заданного массива.
/// Каждый шаг содержит копию состояния массива после операции,
/// что позволяет визуализировать процесс пошагово.
/// </summary>
/// <param name="array">Массив для сортировки.</param>
/// <param name="cancellationToken">Токен отмены для прерывания генерации шагов.</param>
/// <returns>Последовательность шагов сортировки.</returns>
public abstract IEnumerable<SortStep> GenerateSteps(int[] array, CancellationToken cancellationToken = default);
}

View File

@@ -1,38 +0,0 @@
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
namespace InternalSortMethods.Models;
public static class SortAlgorithmsProvider
{
public static IReadOnlyList<SortingAlgorithm> Algorithms { get; } =
[
new BubbleSort(),
new SelectionSort(),
new InsertionSort(),
new QuickSort(),
new MergeSort(),
new HeapSort()
];
}
public partial class ArrayItemModel : ObservableObject
{
[ObservableProperty]
private int _value;
[ObservableProperty]
private int _index;
[ObservableProperty]
private ArrayItemState _state = ArrayItemState.Normal;
}
public enum ArrayItemState
{
Normal,
Comparing,
Swapping,
Sorted,
Pivot
}

View File

@@ -1,22 +1,43 @@
using System.Collections.Generic;
using System;
namespace InternalSortMethods.Models;
public enum SortActionType
/// <summary>
/// Представляет один шаг выполнения алгоритма сортировки.
/// Используется для пошаговой визуализации с анимацией.
/// </summary>
public sealed class SortStep
{
Compare,
Swap,
Set,
MarkSorted,
MarkPivot
/// <summary>Тип операции шага.</summary>
public SortStepType Type { get; init; }
/// <summary>Индексы элементов, участвующих в операции (сравнение или перестановка).</summary>
public int[] Indices { get; init; } = [];
/// <summary>
/// Состояние массива после выполнения данного шага.
/// Копия создаётся для того, чтобы UI мог отобразить актуальное состояние.
/// </summary>
public int[] ArrayState { get; init; } = [];
}
public record SortStep(int Index1, int Index2, SortActionType Action, int[]? ArraySnapshot = null);
public record SortState(int[] Array, List<SortStep> Steps, int CurrentStep, bool IsComplete);
public class SortingResult
/// <summary>
/// Типы операций шага сортировки для визуальной подсветки.
/// </summary>
public enum SortStepType
{
public required int[] SortedArray { get; init; }
public required List<SortStep> Steps { get; init; } = [];
}
/// <summary>Сравнение двух элементов (подсвечиваем жёлтым).</summary>
Compare,
/// <summary>Перестановка двух элементов (подсвечиваем красным).</summary>
Swap,
/// <summary>Запись/вставка элемента на позицию (подсвечиваем голубым).</summary>
Write,
/// <summary>Элемент определён как опорный (pivot) — для QuickSort.</summary>
Pivot,
/// <summary>Элемент окончательно стоит на своём месте.</summary>
Sorted
}

View File

@@ -1,277 +0,0 @@
using System.Collections.Generic;
namespace InternalSortMethods.Models;
public abstract class SortingAlgorithm
{
public abstract string Name { get; }
public abstract string NameRu { get; }
protected List<SortStep> Steps { get; } = [];
protected int[] _array = [];
public SortingResult Sort(int[] array)
{
_array = (int[])array.Clone();
Steps.Clear();
OnSort();
return new SortingResult
{
SortedArray = _array,
Steps = [.. Steps]
};
}
protected void Compare(int i, int j)
{
Steps.Add(new SortStep(i, j, SortActionType.Compare));
}
protected void Swap(int i, int j)
{
(_array[i], _array[j]) = (_array[j], _array[i]);
Steps.Add(new SortStep(i, j, SortActionType.Swap, (int[])_array.Clone()));
}
protected void SetValue(int index, int value)
{
_array[index] = value;
Steps.Add(new SortStep(index, -1, SortActionType.Set, (int[])_array.Clone()));
}
protected void MarkSorted(int index)
{
Steps.Add(new SortStep(index, -1, SortActionType.MarkSorted));
}
protected void MarkPivot(int index)
{
Steps.Add(new SortStep(index, -1, SortActionType.MarkPivot));
}
protected abstract void OnSort();
}
public class BubbleSort : SortingAlgorithm
{
public override string Name => "Bubble Sort";
public override string NameRu => "Пузырьковая";
protected override void OnSort()
{
int n = _array.Length;
for (int i = 0; i < n - 1; i++)
{
bool swapped = false;
for (int j = 0; j < n - i - 1; j++)
{
Compare(j, j + 1);
if (_array[j] > _array[j + 1])
{
Swap(j, j + 1);
swapped = true;
}
}
MarkSorted(n - i - 1);
if (!swapped) break;
}
MarkSorted(0);
}
}
public class SelectionSort : SortingAlgorithm
{
public override string Name => "Selection Sort";
public override string NameRu => "Выбором";
protected override void OnSort()
{
int n = _array.Length;
for (int i = 0; i < n - 1; i++)
{
int minIdx = i;
for (int j = i + 1; j < n; j++)
{
Compare(j, minIdx);
if (_array[j] < _array[minIdx])
minIdx = j;
}
if (minIdx != i)
Swap(i, minIdx);
MarkSorted(i);
}
MarkSorted(n - 1);
}
}
public class InsertionSort : SortingAlgorithm
{
public override string Name => "Insertion Sort";
public override string NameRu => "Вставками";
protected override void OnSort()
{
int n = _array.Length;
for (int i = 1; i < n; i++)
{
int key = _array[i];
int j = i - 1;
Compare(j, i);
while (j >= 0 && _array[j] > key)
{
SetValue(j + 1, _array[j]);
j--;
if (j >= 0) Compare(j, i);
}
SetValue(j + 1, key);
}
MarkSorted(0);
}
}
public class QuickSort : SortingAlgorithm
{
public override string Name => "Quick Sort";
public override string NameRu => "Быстрая";
protected override void OnSort()
{
QuickSortRecursive(0, _array.Length - 1);
MarkSorted(0);
}
private void QuickSortRecursive(int low, int high)
{
if (low < high)
{
int pi = Partition(low, high);
QuickSortRecursive(low, pi - 1);
QuickSortRecursive(pi + 1, high);
}
}
private int Partition(int low, int high)
{
int pivot = _array[high];
MarkPivot(high);
int i = low - 1;
for (int j = low; j < high; j++)
{
Compare(j, high);
if (_array[j] < pivot)
{
i++;
if (i != j) Swap(i, j);
}
}
if (i + 1 != high) Swap(i + 1, high);
MarkSorted(i + 1);
return i + 1;
}
}
public class MergeSort : SortingAlgorithm
{
public override string Name => "Merge Sort";
public override string NameRu => "Слиянием";
protected override void OnSort()
{
MergeSortRecursive(0, _array.Length - 1);
MarkSorted(0);
}
private void MergeSortRecursive(int left, int right)
{
if (left < right)
{
int mid = left + (right - left) / 2;
MergeSortRecursive(left, mid);
MergeSortRecursive(mid + 1, right);
Merge(left, mid, right);
}
}
private void Merge(int left, int mid, int right)
{
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for (int x = 0; x < n1; x++) leftArr[x] = _array[left + x];
for (int x = 0; x < n2; x++) rightArr[x] = _array[mid + 1 + x];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2)
{
Compare(left + i, mid + 1 + j);
if (leftArr[i] <= rightArr[j])
{
SetValue(k, leftArr[i]);
i++;
}
else
{
SetValue(k, rightArr[j]);
j++;
}
k++;
}
while (i < n1)
{
SetValue(k, leftArr[i]);
i++; k++;
}
while (j < n2)
{
SetValue(k, rightArr[j]);
j++; k++;
}
}
}
public class HeapSort : SortingAlgorithm
{
public override string Name => "Heap Sort";
public override string NameRu => "Кучей";
protected override void OnSort()
{
int n = _array.Length;
for (int i = n / 2 - 1; i >= 0; i--)
Heapify(n, i);
for (int i = n - 1; i > 0; i--)
{
Swap(0, i);
MarkSorted(i);
Heapify(i, 0);
}
MarkSorted(0);
}
private void Heapify(int heapSize, int root)
{
int largest = root;
int left = 2 * root + 1;
int right = 2 * root + 2;
if (left < heapSize)
{
Compare(left, largest);
if (_array[left] > _array[largest])
largest = left;
}
if (right < heapSize)
{
Compare(right, largest);
if (_array[right] > _array[largest])
largest = right;
}
if (largest != root)
{
Swap(root, largest);
Heapify(heapSize, largest);
}
}
}

View File

@@ -1,262 +1,383 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using InternalSortMethods.Models;
namespace InternalSortMethods.ViewModels;
/// <summary>
/// Ãëàâíàÿ ViewModel ïðèëîæåíèÿ.
///
/// Ðàçäåëåíèå îòâåòñòâåííîñòè ïî MVVM:
/// - ViewModel ÍÅ ñîäåðæèò ññûëîê íà UI-ýëåìåíòû (Button, ListBox è ò.ä.)
/// - Âñå äàííûå äëÿ îòîáðàæåíèÿ (Items, IsRunning è ò.ä.) — ýòî ñâîéñòâà ñ INotifyPropertyChanged
/// - Âñå äåéñòâèÿ ïîëüçîâàòåëÿ îáðàáàòûâàþòñÿ ÷åðåç RelayCommand
/// - Àëãîðèòìû ñîðòèðîâêè íàõîäÿòñÿ â îòäåëüíûõ Model-êëàññàõ
///
/// Àñèíõðîííîñòü áåç Thread.Sleep:
/// - Èñïîëüçóåòñÿ await Task.Delay(ms, _cancellationToken) — ýòî ÍÅ áëîêèðóåò UI-ïîòîê
/// - CancellationToken ïîçâîëÿåò ìãíîâåííî îòìåíèòü âûïîëíåíèå
/// - SemaphoreSlim èñïîëüçóåòñÿ äëÿ ïàóçû/ïðîäîëæåíèÿ
/// </summary>
public partial class MainViewModel : ViewModelBase
{
// ===== Êîíñòàíòû =====
private const int MinArraySize = 5;
private const int MaxArraySize = 100;
private const int DefaultValue = 20;
// ===== Äîñòóïíûå àëãîðèòìû =====
private static readonly SortAlgorithm[] Algorithms =
[
new BubbleSort(),
new SelectionSort(),
new InsertionSort(),
new QuickSort(),
new MergeSort(),
new HeapSort()
];
// ===== Ïîëÿ =====
private readonly SemaphoreSlim _pauseSemaphore = new(1, 1);
private CancellationTokenSource? _cancellationTokenSource;
private SortingResult? _currentResult;
private int[] _currentArray = [];
private int _comparisons;
private int _swaps;
// Äëÿ ïîøàãîâîãî âûïîëíåíèÿ
private System.Collections.Generic.IEnumerator<SortStep>? _stepEnumerator;
private System.Collections.Generic.List<SortStep>? _cachedSteps;
private int _currentStepIndex;
private readonly ManualResetEventSlim _pauseEvent = new(true);
[ObservableProperty] private ObservableCollection<ArrayItemModel> _arrayItems = [];
[ObservableProperty] private ObservableCollection<SortingAlgorithm> _algorithms = [];
[ObservableProperty] private SortingAlgorithm? _selectedAlgorithm;
[ObservableProperty] private int _arraySize = 20;
[ObservableProperty] private double _animationSpeed = 500;
// ===== ObservableProperty (CommunityToolkit ãåíåðèðóåò INPC) =====
[ObservableProperty] private ObservableCollection<SortItemViewModel> _items = [];
[ObservableProperty] private int _arraySize = DefaultValue;
[ObservableProperty] private int _selectedAlgorithmIndex;
[ObservableProperty] private int _animationSpeed = 100;
[ObservableProperty] private bool _isRunning;
[ObservableProperty] private bool _isCompleted;
[ObservableProperty] private string _statusText = "Ãîòîâî";
[ObservableProperty] private int _currentStep;
[ObservableProperty] private int _totalSteps;
[ObservableProperty] private int _comparisonsCount;
[ObservableProperty] private int _swapsCount;
[ObservableProperty] private bool _isPaused;
[ObservableProperty] private bool _isComplete;
[ObservableProperty] private string _statusText = "Готов";
public MainViewModel()
{
foreach (var algo in SortAlgorithmsProvider.Algorithms)
Algorithms.Add(algo);
SelectedAlgorithm = Algorithms[0];
GenerateArray();
}
// ===== Âû÷èñëÿåìûå ñâîéñòâà =====
partial void OnSelectedAlgorithmChanged(SortingAlgorithm? value)
{
if (value != null && !IsRunning)
ResetCommand.Execute(null);
}
/// <summary>Ñïèñîê íàçâàíèé àëãîðèòìîâ äëÿ ComboBox.</summary>
public string[] AlgorithmNames => Algorithms.Select(a => a.DisplayName).ToArray();
partial void OnArraySizeChanged(int value)
{
if (!IsRunning)
GenerateArray();
}
/// <summary>Ìîæíî ëè çàïóñòèòü ñîðòèðîâêó.</summary>
public bool CanStart => !IsRunning && Items.Count > 0;
/// <summary>Ìîæíî ëè ïîñòàâèòü íà ïàóçó/ïðîäîëæèòü.</summary>
public bool CanPause => IsRunning && !IsCompleted;
/// <summary>Ìîæíî ëè ñäåëàòü øàã âïåð¸ä (ðàáîòàåò òîëüêî íà ïàóçå).</summary>
public bool CanStepForward => IsRunning && IsPaused && !IsCompleted;
/// <summary>Ìîæíî ëè ñáðîñèòü.</summary>
public bool CanReset => IsRunning || IsCompleted || Items.Count > 0;
// ===== Êîìàíäû =====
/// <summary>
/// Ãåíåðàöèÿ íîâîãî ñëó÷àéíîãî ìàññèâà.
/// </summary>
[RelayCommand]
private void GenerateArray()
{
ArrayItems.Clear();
if (IsRunning) return;
ArraySize = Math.Clamp(ArraySize, MinArraySize, MaxArraySize);
ResetState();
var random = new Random();
int maxVal = 100;
for (int i = 0; i < ArraySize; i++)
{
ArrayItems.Add(new ArrayItemModel
{
Value = random.Next(10, maxVal),
Index = i,
State = ArrayItemState.Normal
});
}
ResetStates();
StatusText = "Массив сгенерирован";
_currentArray = Enumerable.Range(0, ArraySize)
.Select(_ => random.Next(1, 100))
.ToArray();
UpdateItems(_currentArray);
StatusText = $"Ñãåíåðèðîâàí ìàññèâ èç {ArraySize} ýëåìåíòîâ";
}
[RelayCommand(CanExecute = nameof(CanStart))]
private async Task StartAsync()
/// <summary>
/// Çàïóñê ñîðòèðîâêè.
///
/// Àñèíõðîííîñòü áåç Thread.Sleep:
/// - Task.Delay ÍÅ áëîêèðóåò UI-ïîòîê (èñïîëüçóåòñÿ continuation)
/// - CancellationToken ïîçâîëÿåò îòìåíèòü çàäåðæêó
/// </summary>
[RelayCommand]
private async Task StartSortAsync()
{
if (IsComplete || _currentResult == null)
if (IsRunning || Items.Count == 0) return;
var algorithm = Algorithms[SelectedAlgorithmIndex];
ResetState();
IsRunning = true;
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
try
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
IsRunning = true;
IsComplete = false;
StatusText = $"Сортировка: {SelectedAlgorithm?.NameRu}";
var arr = GetCurrentArray();
_currentResult = SelectedAlgorithm!.Sort(arr);
// Ãåíåðèðóåì ÂÑÅ øàãè çàðàíåå äëÿ ïîäñ÷¸òà è êýøèðîâàíèÿ
_cachedSteps = algorithm.GenerateSteps(_currentArray, token).ToList();
TotalSteps = _cachedSteps.Count;
_stepEnumerator = _cachedSteps.GetEnumerator();
_currentStepIndex = 0;
_ = Task.Run(() => ProcessStepsAsync(token), token);
StatusText = $"Ñîðòèðîâêà: {algorithm.DisplayName}...";
// Àñèíõðîííîå âûïîëíåíèå øàãîâ ñ àíèìàöèåé
foreach (var step in _cachedSteps)
{
token.ThrowIfCancellationRequested();
// Æä¸ì ðàçðåøåíèÿ íà ïðîäîëæåíèå (ïàóçà)
await _pauseSemaphore.WaitAsync(token);
_pauseSemaphore.Release();
// Åñëè íà ïàóçå — æä¸ì ñíÿòèÿ
if (IsPaused)
{
await WaitUntilResumed(token);
}
token.ThrowIfCancellationRequested();
ApplyStep(step);
_currentStepIndex++;
CurrentStep = _currentStepIndex;
// Çàäåðæêà äëÿ àíèìàöèè — ÍÅ áëîêèðóåò UI-ïîòîê!
// Task.Delay èñïîëüçóåò ñèñòåìíûé òàéìåð, à íå Thread.Sleep
await Task.Delay(AnimationSpeed, token);
}
IsCompleted = true;
StatusText = $"Ñîðòèðîâêà çàâåðøåíà! Ñðàâíåíèé: {ComparisonsCount}, ïåðåñòàíîâîê: {SwapsCount}";
}
catch (OperationCanceledException)
{
StatusText = "Ñîðòèðîâêà îòìåíåíà";
}
catch (Exception ex)
{
StatusText = $"Îøèáêà: {ex.Message}";
}
finally
{
IsRunning = false;
}
}
/// <summary>
/// Ïàóçà èëè ïðîäîëæåíèå âûïîëíåíèÿ.
///
/// Êàê ðàáîòàåò ïàóçà:
/// - SemaphoreSlim âûñòóïàåò êàê «âîðîòà».
/// - Ïðè ïàóçå ìû «çàáèðàåì» permit (Wait), è öèêë ñîðòèðîâêè áëîêèðóåòñÿ íà WaitAsync.
/// - Ïðè ïðîäîëæåíèè ìû «âîçâðàùàåì» permit (Release), è öèêë ïðîäîëæàåò ðàáîòó.
/// - Äîïîëíèòåëüíî ôëàã IsPaused àêòèâèðóåò îæèäàíèå ÷åðåç WaitUntilResumed.
/// </summary>
[RelayCommand]
private void PauseResume()
{
if (!IsRunning || IsCompleted) return;
IsPaused = !IsPaused;
if (IsPaused)
{
// Çàáèðàåì permit — ñëåäóþùèé WaitAsync çàáëîêèðóåòñÿ
_pauseSemaphore.Wait();
StatusText = "Ïàóçà";
}
else
{
_pauseEvent.Set();
IsPaused = false;
StatusText = $"Продолжено";
// Âîçâðàùàåì permit — ðàçáëîêèðóåì WaitAsync
_pauseSemaphore.Release();
StatusText = "Ïðîäîëæåíèå...";
}
StartCommand.NotifyCanExecuteChanged();
PauseCommand.NotifyCanExecuteChanged();
StepForwardCommand.NotifyCanExecuteChanged();
ResetCommand.NotifyCanExecuteChanged();
}
private bool CanStart() => !IsRunning || IsPaused || IsComplete;
[RelayCommand(CanExecute = nameof(CanPause))]
private void Pause()
/// <summary>
/// Øàã âïåð¸ä — âûïîëíÿåò îäèí øàã ñîðòèðîâêè.
/// Ðàáîòàåò òîëüêî êîãäà ñîðòèðîâêà íà ïàóçå.
/// </summary>
[RelayCommand]
private async Task StepForwardAsync()
{
_pauseEvent.Reset();
IsPaused = true;
StatusText = "Пауза";
StartCommand.NotifyCanExecuteChanged();
PauseCommand.NotifyCanExecuteChanged();
StepForwardCommand.NotifyCanExecuteChanged();
if (!IsRunning || !IsPaused || IsCompleted) return;
if (_currentStepIndex < TotalSteps)
{
// Âðåìåííî ñíèìàåì ñ ïàóçû äëÿ îäíîãî øàãà
IsPaused = false;
_pauseSemaphore.Release();
// Ïðèìåíÿåì ñëåäóþùèé øàã
var step = _cachedSteps![_currentStepIndex];
ApplyStep(step);
_currentStepIndex++;
CurrentStep = _currentStepIndex;
await Task.Delay(50); // Ìèíèìàëüíàÿ çàäåðæêà äëÿ îáíîâëåíèÿ UI
// Ñíîâà ñòàâèì íà ïàóçó
IsPaused = true;
_pauseSemaphore.Wait();
}
}
private bool CanPause() => IsRunning && !IsPaused;
[RelayCommand(CanExecute = nameof(CanStep))]
private async Task StepForward()
{
if (_currentResult == null || _currentStepIndex >= _currentResult.Steps.Count)
return;
_pauseEvent.Reset();
IsPaused = true;
await ApplyStepAsync(_currentResult.Steps[_currentStepIndex]);
_currentStepIndex++;
if (_currentStepIndex >= _currentResult.Steps.Count)
Complete();
StartCommand.NotifyCanExecuteChanged();
PauseCommand.NotifyCanExecuteChanged();
StepForwardCommand.NotifyCanExecuteChanged();
}
private bool CanStep() => IsRunning && _currentResult != null && _currentStepIndex < _currentResult.Steps.Count;
/// <summary>
/// Ñáðîñ — îòìåíÿåò âûïîëíåíèå è âîçâðàùàåò èñõîäíîå ñîñòîÿíèå.
/// </summary>
[RelayCommand]
private void Reset()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = null;
_currentResult = null;
_currentStepIndex = 0;
IsRunning = false;
IsPaused = false;
IsComplete = false;
_pauseEvent.Set();
ResetStates();
StatusText = "Сброшено";
StartCommand.NotifyCanExecuteChanged();
PauseCommand.NotifyCanExecuteChanged();
StepForwardCommand.NotifyCanExecuteChanged();
ResetCommand.NotifyCanExecuteChanged();
CancelRunningOperation();
ResetState();
UpdateItems(_currentArray);
StatusText = "Ãîòîâî";
}
private async Task ProcessStepsAsync(CancellationToken token)
// ===== Âñïîìîãàòåëüíûå ìåòîäû =====
/// <summary>
/// Àñèíõðîííîå îæèäàíèå ñíÿòèÿ ïàóçû.
/// Ïðîâåðÿåì êàæäûå 50ìñ, íå áûëà ëè îòìåíåíà îïåðàöèÿ.
/// Ýòî ÍÅ áëîêèðóåò UI-ïîòîê áëàãîäàðÿ await Task.Delay.
/// </summary>
private async Task WaitUntilResumed(CancellationToken token)
{
try
while (IsPaused)
{
while (_currentStepIndex < _currentResult!.Steps.Count && !token.IsCancellationRequested)
{
_pauseEvent.Wait(token);
if (token.IsCancellationRequested) break;
await ApplyStepAsync(_currentResult.Steps[_currentStepIndex]);
_currentStepIndex++;
await Task.Delay((int)AnimationSpeed, token);
}
if (!token.IsCancellationRequested)
Complete();
token.ThrowIfCancellationRequested();
await Task.Delay(50, token);
}
catch (OperationCanceledException) { }
}
private async Task ApplyStepAsync(SortStep step)
/// <summary>
/// Ïðèìåíÿåò îäèí øàã ñîðòèðîâêè ê Items è îáíîâëÿåò ñòàòèñòèêó.
/// </summary>
private void ApplyStep(SortStep step)
{
await Dispatcher.UIThread.InvokeAsync(() =>
// Ñáðàñûâàåì ñîñòîÿíèÿ âñåõ ýëåìåíòîâ
foreach (var item in Items)
{
for (int i = 0; i < ArrayItems.Count; i++)
item.State = SortItemState.Default;
}
// Ïîäñâå÷èâàåì ó÷àñòâóþùèå ýëåìåíòû
foreach (var idx in step.Indices)
{
if (idx >= 0 && idx < Items.Count)
{
if (ArrayItems[i].State != ArrayItemState.Sorted)
ArrayItems[i].State = ArrayItemState.Normal;
Items[idx].State = step.Type switch
{
SortStepType.Compare => SortItemState.Comparing,
SortStepType.Swap => SortItemState.Swapping,
SortStepType.Write => SortItemState.Writing,
SortStepType.Pivot => SortItemState.Pivot,
SortStepType.Sorted => SortItemState.Sorted,
_ => SortItemState.Default
};
}
}
int i1 = step.Index1;
int i2 = step.Index2;
// Îáíîâëÿåì çíà÷åíèÿ ýëåìåíòîâ (åñëè ìàññèâ èçìåíèëñÿ)
for (int i = 0; i < Math.Min(step.ArrayState.Length, Items.Count); i++)
{
Items[i].Value = step.ArrayState[i];
}
switch (step.Action)
{
case SortActionType.Compare:
if (i1 >= 0 && i1 < ArrayItems.Count)
ArrayItems[i1].State = ArrayItemState.Comparing;
if (i2 >= 0 && i2 < ArrayItems.Count)
ArrayItems[i2].State = ArrayItemState.Comparing;
break;
// Îáíîâëÿåì ñòàòèñòèêó
if (step.Type == SortStepType.Compare)
{
ComparisonsCount++;
_comparisons++;
}
case SortActionType.Swap:
if (i1 >= 0 && i1 < ArrayItems.Count && i2 >= 0 && i2 < ArrayItems.Count)
{
(ArrayItems[i1].Value, ArrayItems[i2].Value) = (ArrayItems[i2].Value, ArrayItems[i1].Value);
ArrayItems[i1].State = ArrayItemState.Swapping;
ArrayItems[i2].State = ArrayItemState.Swapping;
}
break;
if (step.Type == SortStepType.Swap)
{
SwapsCount++;
_swaps++;
}
case SortActionType.Set:
if (i1 >= 0 && i1 < ArrayItems.Count && step.ArraySnapshot != null && i1 < step.ArraySnapshot.Length)
ArrayItems[i1].Value = step.ArraySnapshot[i1];
break;
case SortActionType.MarkSorted:
if (i1 >= 0 && i1 < ArrayItems.Count)
ArrayItems[i1].State = ArrayItemState.Sorted;
break;
case SortActionType.MarkPivot:
if (i1 >= 0 && i1 < ArrayItems.Count)
ArrayItems[i1].State = ArrayItemState.Pivot;
break;
}
});
// Îáíîâëÿåì òåêóùèé ìàññèâ
_currentArray = (int[])step.ArrayState.Clone();
}
private void Complete()
private void UpdateItems(int[] array)
{
Items.Clear();
foreach (var value in array)
{
Items.Add(new SortItemViewModel(value));
}
}
private void ResetState()
{
IsRunning = false;
IsComplete = true;
Dispatcher.UIThread.Post(() =>
IsCompleted = false;
IsPaused = false;
CurrentStep = 0;
TotalSteps = 0;
ComparisonsCount = 0;
SwapsCount = 0;
_comparisons = 0;
_swaps = 0;
_stepEnumerator = null;
_cachedSteps = null;
_currentStepIndex = 0;
// Ñáðîñ ñîñòîÿíèé ýëåìåíòîâ
foreach (var item in Items)
{
foreach (var item in ArrayItems)
item.State = ArrayItemState.Sorted;
StatusText = "Сортировка завершена!";
StartCommand.NotifyCanExecuteChanged();
PauseCommand.NotifyCanExecuteChanged();
StepForwardCommand.NotifyCanExecuteChanged();
ResetCommand.NotifyCanExecuteChanged();
});
item.State = SortItemState.Default;
}
// Ñáðîñ ñåìàôîðà
if (_pauseSemaphore.CurrentCount == 0)
{
_pauseSemaphore.Release();
}
}
private int[] GetCurrentArray()
private void CancelRunningOperation()
{
int[] arr = new int[ArrayItems.Count];
for (int i = 0; i < ArrayItems.Count; i++)
arr[i] = ArrayItems[i].Value;
return arr;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
// Îñâîáîæäàåì ñåìàôîð, åñëè îïåðàöèÿ íà ïàóçå
if (IsPaused)
{
IsPaused = false;
if (_pauseSemaphore.CurrentCount == 0)
{
_pauseSemaphore.Release();
}
}
}
private void ResetStates()
// ===== ×àñòè÷íûå ìåòîäû äëÿ ObservableProperty =====
partial void OnArraySizeChanged(int value)
{
foreach (var item in ArrayItems)
item.State = ArrayItemState.Normal;
if (!IsRunning)
{
GenerateArray();
}
}
}
}

View File

@@ -0,0 +1,62 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace InternalSortMethods.ViewModels;
/// <summary>
/// ViewModel для одного элемента массива в визуализации.
/// Содержит значение и текущее состояние подсветки (цвет).
/// </summary>
public partial class SortItemViewModel : ObservableObject
{
private int _value;
private SortItemState _state = SortItemState.Default;
public SortItemViewModel(int value)
{
_value = value;
}
/// <summary>Значение элемента (высота столбца).</summary>
public int Value
{
get => _value;
set => SetProperty(ref _value, value);
}
/// <summary>
/// Текущее состояние элемента для визуальной подсветки.
/// Изменяется при каждом шаге сортировки.
/// </summary>
public SortItemState State
{
get => _state;
set => SetProperty(ref _state, value);
}
/// <summary>Относительная высота для привязки к UI (0.0 1.0).</summary>
public double NormalizedHeight { get; set; }
}
/// <summary>
/// Состояния элемента для цветовой подсветки.
/// </summary>
public enum SortItemState
{
/// <summary>Обычное состояние (нейтральный цвет).</summary>
Default,
/// <summary>Элемент сравнивается с другим (жёлтый/оранжевый).</summary>
Comparing,
/// <summary>Элемент участвует в перестановке (красный).</summary>
Swapping,
/// <summary>Элемент записан/вставлен (синий/голубой).</summary>
Writing,
/// <summary>Опорный элемент — pivot для QuickSort (фиолетовый).</summary>
Pivot,
/// <summary>Элемент окончательно отсортирован (зелёный).</summary>
Sorted
}

View File

@@ -3,110 +3,238 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:InternalSortMethods.ViewModels"
xmlns:controls="using:InternalSortMethods.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
xmlns:converters="using:InternalSortMethods.Converters"
x:Class="InternalSortMethods.Views.MainView"
x:DataType="vm:MainViewModel">
x:DataType="vm:MainViewModel"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="700">
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<!-- ===== Конвертеры ===== -->
<UserControl.Resources>
<ControlTheme x:Key="GlassButton" TargetType="Button">
<Setter Property="Background" Value="#40666EF1"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="16,12"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="MinHeight" Value="48"/>
<Setter Property="MinWidth" Value="80"/>
</ControlTheme>
<converters:StateToColorConverter x:Key="StateToColor"/>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*,Auto">
<Border Grid.Row="0" Background="#20000000" CornerRadius="16" Margin="16,16,16,8" Padding="16">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,*,Auto">
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="12">
<TextBlock Text="Алгоритм сортировки" FontSize="12" Foreground="#AAFFFFFF"/>
<ComboBox ItemsSource="{Binding Algorithms}"
SelectedItem="{Binding SelectedAlgorithm}"
MinWidth="180" MinHeight="44"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding NameRu}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Основной фон (градиент) -->
<Panel>
<Panel.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#0f0c29" Offset="0.0"/>
<GradientStop Color="#302b63" Offset="0.5"/>
<GradientStop Color="#24243e" Offset="1.0"/>
</LinearGradientBrush>
</Panel.Background>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<Grid RowDefinitions="Auto,*,Auto" Margin="16">
<!-- ===== Заголовок ===== -->
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,16">
<TextBlock Text="Методы внутренней сортировки"
FontSize="24" FontWeight="Bold"
Foreground="#f0f0f5"
HorizontalAlignment="Center"/>
<TextBlock Text="Визуализация алгоритмов сортировки"
FontSize="13"
Foreground="#a0a0b0"
HorizontalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Spacing="12" Margin="16,0">
<TextBlock Text="Размер массива" FontSize="12" Foreground="#AAFFFFFF"/>
<Grid ColumnDefinitions="*,Auto">
<Slider Grid.Column="0" Minimum="5" Maximum="50" Value="{Binding ArraySize}"
TickFrequency="5" IsSnapToTickEnabled="True" MinHeight="44"/>
<TextBlock Grid.Column="1" Text="{Binding ArraySize}" Width="30" Margin="8,0"
VerticalAlignment="Center" Foreground="White"/>
</Grid>
</StackPanel>
<!-- ===== Область визуализации ===== -->
<Border Grid.Row="1" Margin="0,0,0,16" MinHeight="250"
CornerRadius="16" Padding="16"
BoxShadow="0 8 32 0 #00000050">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#121212" Offset="0.0"/>
<GradientStop Color="#131313" Offset="1.0"/>
</LinearGradientBrush>
</Border.Background>
<Button Grid.Row="0" Grid.Column="2" Content="Новый массив"
Command="{Binding GenerateArrayCommand}"
Theme="{StaticResource GlassButton}"
VerticalAlignment="Bottom" MinWidth="120"/>
<DockPanel>
<!-- Статус и прогресс -->
<StackPanel DockPanel.Dock="Top" Spacing="8" Margin="0,0,0,12">
<ProgressBar Value="{Binding CurrentStep}"
Maximum="{Binding TotalSteps}"
Height="8" CornerRadius="4">
<ProgressBar.Background>
<SolidColorBrush Color="#ffffff15"/>
</ProgressBar.Background>
<ProgressBar.Foreground>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,0%">
<GradientStop Color="#7c5cfc" Offset="0.0"/>
<GradientStop Color="#6d28d9" Offset="1.0"/>
</LinearGradientBrush>
</ProgressBar.Foreground>
</ProgressBar>
<StackPanel Grid.Row="1" Grid.ColumnSpan="3" Spacing="12" Margin="0,16,0,0">
<TextBlock Text="Скорость анимации" FontSize="12" Foreground="#AAFFFFFF"/>
<Grid ColumnDefinitions="*,Auto">
<Slider Grid.Column="0" Minimum="50" Maximum="1000" Value="{Binding AnimationSpeed}"
TickFrequency="50" IsSnapToTickEnabled="True" MinHeight="44"/>
<TextBlock Grid.Column="1" Text="{Binding AnimationSpeed, StringFormat={}{0:0}} мс" Width="50" Margin="8,0"
VerticalAlignment="Center" Foreground="White"/>
</Grid>
</StackPanel>
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0"
Text="{Binding StatusText}"
FontSize="13" Foreground="#a0a0b0"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12"
VerticalAlignment="Center">
<TextBlock FontSize="12" Foreground="#fbbf24"
Text="{Binding ComparisonsCount, StringFormat='⚡ {0}'}"/>
<TextBlock FontSize="12" Foreground="#f87171"
Text="{Binding SwapsCount, StringFormat='⇄ {0}'}"/>
</StackPanel>
<TextBlock Grid.Column="2" FontSize="12" Foreground="#818cf8"
VerticalAlignment="Center" Margin="12,0,0,0">
<TextBlock.Text>
<MultiBinding StringFormat="Шаг {0}/{1}">
<Binding Path="CurrentStep"/>
<Binding Path="TotalSteps"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</StackPanel>
<!-- Столбчатая диаграмма -->
<Viewbox Stretch="Uniform" StretchDirection="Both"
VerticalAlignment="Bottom">
<ItemsControl ItemsSource="{Binding Items}"
MinHeight="200" MaxHeight="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Spacing="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SortItemViewModel">
<!-- Столбец: высота = Value, цвет = по State -->
<Border Width="14"
Height="{Binding Value}"
VerticalAlignment="Bottom"
CornerRadius="3,3,0,0"
BoxShadow="0 2 4 0 #00000040">
<Border.Background>
<SolidColorBrush Color="{Binding State, Converter={StaticResource StateToColor}}"/>
</Border.Background>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
</DockPanel>
</Border>
<!-- ===== Панель управления ===== -->
<Border Grid.Row="2" CornerRadius="16" Padding="16"
BoxShadow="0 8 32 0 #00000050">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#121212" Offset="0.0"/>
<GradientStop Color="#131313" Offset="1.0"/>
</LinearGradientBrush>
</Border.Background>
<StackPanel Spacing="16">
<!-- Алгоритм + Размер массива -->
<Grid ColumnDefinitions="*,*" ColumnSpacing="16">
<StackPanel Grid.Column="0" Spacing="6">
<TextBlock Text="Алгоритм" FontSize="11" Foreground="#a0a0b0"
FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding AlgorithmNames}"
SelectedIndex="{Binding SelectedAlgorithmIndex}"
CornerRadius="12" MinHeight="48" FontSize="14">
<ComboBox.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#121212" Offset="0.0"/>
<GradientStop Color="#131313" Offset="1.0"/>
</LinearGradientBrush>
</ComboBox.Background>
<ComboBox.Foreground>
<SolidColorBrush Color="#f0f0f5"/>
</ComboBox.Foreground>
</ComboBox>
</StackPanel>
<StackPanel Grid.Column="1" Spacing="6">
<TextBlock Text="Размер массива" FontSize="11" Foreground="#a0a0b0"
FontWeight="SemiBold"/>
<Grid ColumnDefinitions="*,Auto">
<Slider Grid.Column="0"
Value="{Binding ArraySize}"
Minimum="5" Maximum="80"
TickFrequency="1" IsSnapToTickEnabled="True"
MinHeight="48"/>
<TextBlock Grid.Column="1"
Text="{Binding ArraySize}"
FontSize="16" FontWeight="Bold"
Foreground="#f0f0f5"
VerticalAlignment="Center"
Margin="12,0,0,0" MinWidth="32"
TextAlignment="Center"/>
</Grid>
</StackPanel>
</Grid>
<!-- Скорость -->
<StackPanel Spacing="6">
<Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" Text="Скорость"
FontSize="11" Foreground="#a0a0b0"
FontWeight="SemiBold" VerticalAlignment="Center"/>
<Slider Grid.Column="1"
Value="{Binding AnimationSpeed}"
Minimum="1" Maximum="500"
TickFrequency="10" IsSnapToTickEnabled="True"
MinHeight="48"/>
<TextBlock Grid.Column="2"
Text="{Binding AnimationSpeed, StringFormat={}{0} мс}"
FontSize="13" Foreground="#a0a0b0"
VerticalAlignment="Center" MinWidth="60"
TextAlignment="Right" Margin="12,0,0,0"/>
</Grid>
</StackPanel>
<!-- Кнопки -->
<Grid ColumnDefinitions="*,*,Auto,Auto,Auto" ColumnSpacing="8">
<Button Grid.Column="0" Content="Генерация"
Classes="glass-btn"
Command="{Binding GenerateArrayCommand}"
CornerRadius="12" MinHeight="48"
HorizontalAlignment="Stretch"/>
<Button Grid.Column="1" Content="▶ Старт"
Classes="accent-btn"
Command="{Binding StartSortCommand}"
IsEnabled="{Binding CanStart}"
CornerRadius="12" MinHeight="48"
HorizontalAlignment="Stretch"/>
<Button Grid.Column="2" Content="⏸"
Classes="glass-btn"
Command="{Binding PauseResumeCommand}"
IsEnabled="{Binding CanPause}"
CornerRadius="12" MinHeight="48" MinWidth="52"
Padding="12,10" FontSize="18"/>
<Button Grid.Column="3" Content="⏭"
Classes="glass-btn"
Command="{Binding StepForwardCommand}"
IsEnabled="{Binding CanStepForward}"
CornerRadius="12" MinHeight="48" MinWidth="52"
Padding="12,10" FontSize="18"/>
<Button Grid.Column="4" Content="↺"
Classes="glass-btn"
Command="{Binding ResetCommand}"
IsEnabled="{Binding CanReset}"
CornerRadius="12" MinHeight="48" MinWidth="52"
Padding="12,10" FontSize="18"/>
</Grid>
</StackPanel>
</Border>
</Grid>
</Border>
<Border Grid.Row="1" Background="#15FFFFFF" CornerRadius="16" Margin="16,8" Padding="16">
<controls:ArrayVisualizer Items="{Binding ArrayItems}"/>
</Border>
<Border Grid.Row="2" Background="#20000000" CornerRadius="16" Margin="16,8,16,16" Padding="16">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,*,*,*,*">
<TextBlock Grid.Row="0" Grid.ColumnSpan="5" Text="{Binding StatusText}"
FontSize="16" FontWeight="SemiBold" Foreground="White"
HorizontalAlignment="Center" Margin="0,0,0,16"/>
<Button Grid.Row="1" Grid.Column="0" Content="Старт"
Command="{Binding StartCommand}"
Theme="{StaticResource GlassButton}"
HorizontalAlignment="Stretch" Margin="0,0,8,0"/>
<Button Grid.Row="1" Grid.Column="1" Content="Пауза"
Command="{Binding PauseCommand}"
Theme="{StaticResource GlassButton}"
HorizontalAlignment="Stretch" Margin="0,0,8,0"/>
<Button Grid.Row="1" Grid.Column="2" Content="Шаг"
Command="{Binding StepForwardCommand}"
Theme="{StaticResource GlassButton}"
HorizontalAlignment="Stretch" Margin="0,0,8,0"/>
<Button Grid.Row="1" Grid.Column="3" Content="Сброс"
Command="{Binding ResetCommand}"
Theme="{StaticResource GlassButton}"
HorizontalAlignment="Stretch" Margin="0,0,8,0"/>
<StackPanel Grid.Row="1" Grid.Column="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Ellipse Width="12" Height="12" Fill="#4ADE80" Margin="0,0,8,0"
IsVisible="{Binding IsComplete}"/>
<Ellipse Width="12" Height="12" Fill="#FFE66D" Margin="0,0,8,0"
IsVisible="{Binding IsPaused}"/>
<Ellipse Width="12" Height="12" Fill="#6366F1"
IsVisible="{Binding IsRunning}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</UserControl>
</ScrollViewer>
</Panel>
</UserControl>