Skip to content

Commit e0a1de9

Browse files
authored
feat: Introduced an Avalonia launcher [WIP] application for managing and running Stride Community Toolkit examples (#308)
* test: Avalonia launcher added * fix: Fixing runtime error * chore: NuGet packages bumped
1 parent e16faa9 commit e0a1de9

File tree

8 files changed

+480
-1
lines changed

8 files changed

+480
-1
lines changed

Stride.CommunityToolkit.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Microsoft Visual Studio Solution File, Format Version 12.00
22
# Visual Studio Version 18
3-
VisualStudioVersion = 18.0.11010.61
3+
VisualStudioVersion = 18.0.11116.177 d18.0
44
MinimumVisualStudioVersion = 10.0.40219.1
55
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.CommunityToolkit", "src\Stride.CommunityToolkit\Stride.CommunityToolkit.csproj", "{BE6D2173-AC41-40BB-9F3C-D0AF85B7FE86}"
66
EndProject
@@ -146,6 +146,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example01_Basic3DScene_DPI_
146146
EndProject
147147
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.CommunityToolkit.Linux", "src\Stride.CommunityToolkit.Linux\Stride.CommunityToolkit.Linux.csproj", "{AB244060-9A1F-40B1-9043-02FBED6E94E4}"
148148
EndProject
149+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.CommunityToolkit.Examples.Launcher", "src\Stride.CommunityToolkit.Examples.Launcher\Stride.CommunityToolkit.Examples.Launcher.csproj", "{AFB1F085-B907-4C81-8DA3-C039ACC41A34}"
150+
EndProject
149151
Global
150152
GlobalSection(SolutionConfigurationPlatforms) = preSolution
151153
Debug|Any CPU = Debug|Any CPU
@@ -403,6 +405,10 @@ Global
403405
{AB244060-9A1F-40B1-9043-02FBED6E94E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
404406
{AB244060-9A1F-40B1-9043-02FBED6E94E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
405407
{AB244060-9A1F-40B1-9043-02FBED6E94E4}.Release|Any CPU.Build.0 = Release|Any CPU
408+
{AFB1F085-B907-4C81-8DA3-C039ACC41A34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
409+
{AFB1F085-B907-4C81-8DA3-C039ACC41A34}.Debug|Any CPU.Build.0 = Debug|Any CPU
410+
{AFB1F085-B907-4C81-8DA3-C039ACC41A34}.Release|Any CPU.ActiveCfg = Release|Any CPU
411+
{AFB1F085-B907-4C81-8DA3-C039ACC41A34}.Release|Any CPU.Build.0 = Release|Any CPU
406412
EndGlobalSection
407413
GlobalSection(SolutionProperties) = preSolution
408414
HideSolutionNode = FALSE
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Application xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
x:Class="Stride.CommunityToolkit.Examples.Launcher.App"
4+
RequestedThemeVariant="Default">
5+
<Application.Styles>
6+
<FluentTheme />
7+
</Application.Styles>
8+
</Application>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Avalonia;
2+
using Avalonia.Controls.ApplicationLifetimes;
3+
using Avalonia.Markup.Xaml;
4+
5+
namespace Stride.CommunityToolkit.Examples.Launcher;
6+
7+
public sealed partial class App : Application
8+
{
9+
public override void Initialize() => AvaloniaXamlLoader.Load(this);
10+
11+
public override void OnFrameworkInitializationCompleted()
12+
{
13+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
14+
desktop.MainWindow = new MainWindow();
15+
16+
base.OnFrameworkInitializationCompleted();
17+
}
18+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<Window xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
x:Class="Stride.CommunityToolkit.Examples.Launcher.MainWindow"
4+
Width="1200" Height="700"
5+
Title="Stride Community Toolkit - Examples Launcher">
6+
<Grid ColumnDefinitions="2*,3*" RowDefinitions="Auto,*">
7+
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" Margin="8">
8+
<TextBox x:Name="SearchBox" Width="320" Watermark="Search examples..." />
9+
<Button x:Name="BtnRun" Content="▶ Run" Margin="8,0,0,0" Background="#28a745" Foreground="White" Padding="12,6" />
10+
<Button x:Name="BtnStop" Content="⏹ Stop" Margin="8,0,0,0" Background="#dc3545" Foreground="White" Padding="12,6" />
11+
<Button x:Name="BtnOpenFolder" Content="📁 Open Folder" Margin="8,0,0,0" Padding="12,6" />
12+
<Button x:Name="BtnCopyCmd" Content="📋 Copy Command" Margin="8,0,0,0" Padding="12,6" />
13+
</StackPanel>
14+
15+
<Border Grid.Row="1" Margin="8" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4">
16+
<ScrollViewer VerticalScrollBarVisibility="Auto">
17+
<ListBox x:Name="ExamplesList" SelectionMode="Single" />
18+
</ScrollViewer>
19+
</Border>
20+
21+
<Grid Grid.Column="1" Grid.Row="1" Margin="8" RowDefinitions="Auto,*,Auto">
22+
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
23+
<TextBlock Text="Output Log" FontWeight="Bold" VerticalAlignment="Center" />
24+
<Button x:Name="BtnClearLog" Content="Clear" Margin="8,0,0,0" Padding="8,4" />
25+
</StackPanel>
26+
27+
<Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="6">
28+
<ScrollViewer VerticalScrollBarVisibility="Auto">
29+
<TextBlock x:Name="LogPanel" FontFamily="Consolas,monospace" TextWrapping="Wrap" FontSize="12" />
30+
</ScrollViewer>
31+
</Border>
32+
33+
<TextBlock Grid.Row="2" Margin="0,6,0,0" Opacity="0.7" FontSize="11" TextWrapping="Wrap"
34+
Text="💡 Tip: Shader/effect warnings are filtered by default. Set SHOW_WARNINGS=1 to see all warnings."/>
35+
</Grid>
36+
</Grid>
37+
</Window>
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Threading;
3+
using Stride.CommunityToolkit.Examples.Core;
4+
using System.Collections.ObjectModel;
5+
using System.Diagnostics;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
9+
namespace Stride.CommunityToolkit.Examples.Launcher;
10+
11+
public partial class MainWindow : Window
12+
{
13+
private readonly ObservableCollection<ExampleListItem> _examples = [];
14+
private readonly List<ExampleProjectMeta> _all = [];
15+
private Process? _running;
16+
private CancellationTokenSource? _cts;
17+
18+
private static readonly Regex GenericWarning = new(@"\bwarning\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
19+
private static readonly Regex ShaderWarning = new(@"\b(effect|shader|hlsl|fx|mixin|compiler)\b.*\bwarning\b|\bwarning\b.*\b(effect|shader|hlsl|fx|mixin|compiler)\b",
20+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
21+
22+
public MainWindow()
23+
{
24+
InitializeComponent();
25+
26+
ExamplesList.ItemsSource = _examples;
27+
28+
LoadExamples();
29+
30+
SearchBox.PropertyChanged += (s, e) =>
31+
{
32+
if (e.Property.Name == nameof(TextBox.Text))
33+
Filter(SearchBox.Text);
34+
};
35+
36+
BtnRun.Click += async (_, __) => await RunSelectedAsync();
37+
BtnStop.Click += (_, __) => StopRunning();
38+
BtnOpenFolder.Click += (_, __) => OpenFolder();
39+
BtnCopyCmd.Click += (_, __) => CopyCommand();
40+
BtnClearLog.Click += (_, __) => LogPanel.Text = string.Empty;
41+
}
42+
43+
private void LoadExamples()
44+
{
45+
var provider = new ExampleProvider();
46+
var examples = provider.GetExamples()
47+
.Where(e => e.Title != Constants.Quit && e.Title != Constants.Clear)
48+
.Select(e => new ExampleProjectMeta(e.Id, e.Title, GetProjectPath(e), GetOrder(e), e.Category))
49+
.ToList();
50+
51+
_all.AddRange(examples);
52+
foreach (var e in _all)
53+
_examples.Add(new ExampleListItem(e));
54+
}
55+
56+
private static string GetProjectPath(Example example)
57+
{
58+
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
59+
var examplesRoot = FindExamplesRoot(baseDir) ?? Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "examples", "code-only"));
60+
61+
var projectName = example.ProjectName ?? example.Title.Replace(" ", "_");
62+
var patterns = new[] { "*.csproj", "*.fsproj", "*.vbproj" };
63+
64+
foreach (var pattern in patterns)
65+
{
66+
var files = Directory.EnumerateFiles(examplesRoot, pattern, SearchOption.AllDirectories)
67+
.Where(f => Path.GetFileNameWithoutExtension(f).Contains(projectName, StringComparison.OrdinalIgnoreCase))
68+
.ToList();
69+
70+
if (files.Count > 0) return files[0];
71+
}
72+
73+
return string.Empty;
74+
}
75+
76+
private static string? FindExamplesRoot(string baseDir)
77+
{
78+
var dir = baseDir;
79+
for (int i = 0; i < 8 && !string.IsNullOrEmpty(dir); i++)
80+
{
81+
var candidate = Path.Combine(dir, "examples", "code-only");
82+
if (Directory.Exists(candidate))
83+
return candidate;
84+
85+
dir = Path.GetDirectoryName(dir?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
86+
}
87+
return null;
88+
}
89+
90+
private static int? GetOrder(Example example)
91+
{
92+
if (example.Category == Constants.BasicExample) return 1;
93+
if (example.Category == Constants.AdvanceExample) return 2;
94+
return 3;
95+
}
96+
97+
private void Filter(string? text)
98+
{
99+
text ??= string.Empty;
100+
text = text.Trim();
101+
102+
_examples.Clear();
103+
foreach (var e in _all)
104+
{
105+
if (text.Length == 0 ||
106+
e.Title.Contains(text, StringComparison.OrdinalIgnoreCase) ||
107+
e.Id.Contains(text, StringComparison.OrdinalIgnoreCase) ||
108+
(e.Category?.Contains(text, StringComparison.OrdinalIgnoreCase) ?? false))
109+
{
110+
_examples.Add(new ExampleListItem(e));
111+
}
112+
}
113+
}
114+
115+
private ExampleProjectMeta? Current
116+
{
117+
get
118+
{
119+
var item = ExamplesList.SelectedItem as ExampleListItem;
120+
return item?.Meta;
121+
}
122+
}
123+
124+
private async Task RunSelectedAsync()
125+
{
126+
var meta = Current;
127+
if (meta is null)
128+
{
129+
AppendLine("⚠️ Please select an example to run.");
130+
return;
131+
}
132+
133+
if (string.IsNullOrEmpty(meta.ProjectFile) || !File.Exists(meta.ProjectFile))
134+
{
135+
AppendLine($"❌ Project file not found: {meta.ProjectFile}");
136+
return;
137+
}
138+
139+
StopRunning();
140+
141+
LogPanel.Text = string.Empty;
142+
AppendLine($"▶️ Starting: {meta.Title}");
143+
AppendLine($"📁 Project: {meta.ProjectFile}");
144+
AppendLine(new string('-', 80));
145+
146+
_cts = new CancellationTokenSource();
147+
148+
var psi = new ProcessStartInfo
149+
{
150+
FileName = "dotnet",
151+
Arguments = $"run --project \"{meta.ProjectFile}\"",
152+
WorkingDirectory = Path.GetDirectoryName(meta.ProjectFile) ?? Environment.CurrentDirectory,
153+
UseShellExecute = false,
154+
RedirectStandardOutput = true,
155+
RedirectStandardError = true,
156+
CreateNoWindow = true
157+
};
158+
159+
_running = new Process { StartInfo = psi, EnableRaisingEvents = true };
160+
var process = _running;
161+
162+
try
163+
{
164+
process.Start();
165+
var readOut = Task.Run(() => ReadLinesAsync(process.StandardOutput, isError: false, _cts.Token));
166+
var readErr = Task.Run(() => ReadLinesAsync(process.StandardError, isError: true, _cts.Token));
167+
await Task.WhenAll(readOut, readErr);
168+
169+
process.WaitForExit();
170+
var exitCode = process.ExitCode;
171+
AppendLine($"✅ Process exited with code: {exitCode}");
172+
}
173+
catch (Exception ex)
174+
{
175+
AppendLine($"❌ Error: {ex.Message}");
176+
StopRunning();
177+
}
178+
}
179+
180+
private void StopRunning()
181+
{
182+
try
183+
{
184+
_cts?.Cancel();
185+
if (_running is { HasExited: false })
186+
{
187+
AppendLine("⏹️ Stopping process...");
188+
_running.Kill(entireProcessTree: true);
189+
_running.WaitForExit(2000);
190+
AppendLine("✅ Process stopped.");
191+
}
192+
}
193+
catch (Exception ex)
194+
{
195+
AppendLine($"⚠️ Error stopping process: {ex.Message}");
196+
}
197+
finally
198+
{
199+
_running?.Dispose();
200+
_running = null;
201+
_cts?.Dispose();
202+
_cts = null;
203+
}
204+
}
205+
206+
private async Task ReadLinesAsync(StreamReader reader, bool isError, CancellationToken ct)
207+
{
208+
while (!ct.IsCancellationRequested)
209+
{
210+
string? line;
211+
try { line = await reader.ReadLineAsync(); }
212+
catch { break; }
213+
if (line is null) break;
214+
215+
if (ShouldSuppress(line)) continue;
216+
217+
AppendLine(line, isError);
218+
}
219+
}
220+
221+
private static bool ShouldSuppress(string line)
222+
{
223+
var showAll = string.Equals(Environment.GetEnvironmentVariable("SHOW_WARNINGS"), "1", StringComparison.OrdinalIgnoreCase)
224+
|| string.Equals(Environment.GetEnvironmentVariable("SHOW_WARNINGS"), "true", StringComparison.OrdinalIgnoreCase);
225+
if (showAll) return false;
226+
227+
if (!GenericWarning.IsMatch(line)) return false;
228+
return ShaderWarning.IsMatch(line);
229+
}
230+
231+
private void AppendLine(string text, bool isError = false)
232+
{
233+
Dispatcher.UIThread.Post(() =>
234+
{
235+
var sb = new StringBuilder(LogPanel.Text ?? string.Empty);
236+
if (sb.Length > 0) sb.AppendLine();
237+
if (isError) sb.Append("❌ ");
238+
sb.Append(text);
239+
LogPanel.Text = sb.ToString();
240+
});
241+
}
242+
243+
private void OpenFolder()
244+
{
245+
var meta = Current;
246+
if (meta is null) return;
247+
var dir = Path.GetDirectoryName(meta.ProjectFile);
248+
if (dir is null || !Directory.Exists(dir))
249+
{
250+
AppendLine("⚠️ Folder not found.");
251+
return;
252+
}
253+
254+
try
255+
{
256+
Process.Start(new ProcessStartInfo { FileName = dir, UseShellExecute = true });
257+
}
258+
catch (Exception ex)
259+
{
260+
AppendLine($"❌ Error opening folder: {ex.Message}");
261+
}
262+
}
263+
264+
private void CopyCommand()
265+
{
266+
var meta = Current;
267+
if (meta is null) return;
268+
var cmd = $"dotnet run --project \"{meta.ProjectFile}\"";
269+
270+
try
271+
{
272+
Clipboard?.SetTextAsync(cmd);
273+
AppendLine($"📋 Copied to clipboard: {cmd}");
274+
}
275+
catch (Exception ex)
276+
{
277+
AppendLine($"❌ Error copying to clipboard: {ex.Message}");
278+
}
279+
}
280+
281+
private class ExampleListItem(ExampleProjectMeta meta)
282+
{
283+
public ExampleProjectMeta Meta { get; } = meta;
284+
285+
public override string ToString()
286+
{
287+
var cat = !string.IsNullOrEmpty(meta.Category) ? $"[{meta.Category}] " : "";
288+
return $"{cat}{meta.Title} ({meta.Id})";
289+
}
290+
}
291+
}

0 commit comments

Comments
 (0)