Skip to content

Commit

Permalink
Load XAML in YAML (#1075)
Browse files Browse the repository at this point in the history
Add support for loading XAML styles via YAML.
  • Loading branch information
dalyIsaac authored Nov 12, 2024
1 parent e0f11b8 commit 275b519
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 14 deletions.
24 changes: 20 additions & 4 deletions docs/configure/core/styling.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Styling

## XAML Styling

XAML resources can be loaded via the `styles` key in the YAML/JSON configuration. Each path should be a path to a XAML file. Paths must be absolute (e.g. `C:\Users\user\.whim\resources\styles.xaml`) or relative to the `.whim` directory (e.g. `resources\styles.xaml`).

For example:

```yaml
styles:
user_dictionaries:
- resources/styles.xaml
```
[!INCLUDE [Styling](../../_includes/core/styling.md)]
## Backdrops
Different Whim windows can support custom backdrops. They will generally be associated with a `backdrop` key in the YAML/JSON configuration. The following backdrops are available:
Expand All @@ -16,13 +30,15 @@ Different Whim windows can support custom backdrops. They will generally be asso
| `mica` | An opaque, dynamic material that incorpoates theme and the desktop wallpaper. Mica has better performance than Acrylic. | [Mica material](https://learn.microsoft.com/en-us/windows/apps/design/style/mica) |
| `mica_alt` | A variant of Mica with stronger tinting of the user's background color. | [Mica alt material](https://learn.microsoft.com/en-us/windows/apps/design/style/mica) |

### Configuration
### Backdrops Configuration

| Property | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | The type of backdrop to use. |
| `always_show_backdrop` | By default, WinUI will disable the backdrop when the window loses focus. Whim overrides this setting. Set this to false to disable the backdrop when the window loses focus. |

## XAML Styling

Loading XAML styles via YAML/JSON is being tracked in [this GitHub issue](https://github.com/dalyIsaac/Whim/issues/1064). In the meantime, it is available [via C# scripting](../../script/core/styling.md).
```yaml
backdrop:
type: acrylic
always_show_backdrop: true
```
170 changes: 170 additions & 0 deletions src/Whim.Yaml.Tests/YamlLoader/YamlLoader_LoadStylesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using NSubstitute;
using Whim.TestUtils;
using Xunit;

namespace Whim.Yaml.Tests;

public class YamlLoader_LoadStylesTests
{
public static TheoryData<string, bool> ValidStylesConfig =>
new()
{
{
"""
styles:
user_dictionaries:
- "path/to/dict1.xaml"
- "path/to/dict2.xaml"
""",
true
},
{
"""
{
"styles": {
"user_dictionaries": [
"path/to/dict1.xaml",
"path/to/dict2.xaml"
]
}
}
""",
false
},
};

[Theory]
[MemberAutoSubstituteData<YamlLoaderCustomization>(nameof(ValidStylesConfig))]
public void Load_ValidStyles_AddsUserDictionaries(string config, bool isYaml, IContext ctx)
{
// Given
YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml);
ctx.FileManager.FileExists(Arg.Is<string>(p => p.StartsWith("path"))).Returns(true);

// When
bool result = YamlLoader.Load(ctx, showErrorWindow: false);

// Then
Assert.True(result);
ctx.ResourceManager.Received(2).AddUserDictionary(Arg.Any<string>());
}

[Theory]
[MemberAutoSubstituteData<YamlLoaderCustomization>(nameof(ValidStylesConfig))]
public void Load_FallbackStyles_AddsUserDictionaries(string config, bool isYaml, IContext ctx)
{
// Given
YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml);
ctx.FileManager.WhimDir.Returns("C:\\Users\\username\\.whim");
ctx.FileManager.FileExists(Arg.Is<string>(p => p.StartsWith("path"))).Returns(false);
ctx.FileManager.FileExists(Arg.Is<string>(p => p.StartsWith("C:\\Users\\username\\.whim"))).Returns(true);

// When
bool result = YamlLoader.Load(ctx, showErrorWindow: false);

// Then
Assert.True(result);
ctx.ResourceManager.Received(2).AddUserDictionary(Arg.Any<string>());
}

public static TheoryData<string, bool> NoStylesConfig =>
new()
{
{
"""
styles:
user_dictionaries: []
""",
true
},
{
"""
{
"styles": {
"user_dictionaries": []
}
}
""",
false
},
{
"""
styles: {}
""",
true
},
{
"""
{
"styles": {}
}
""",
false
},
};

public static TheoryData<string, bool> InvalidPathsStylesConfig =>
new()
{
{
"""
styles:
user_dictionaries:
- "the path to nowhere"
""",
true
},
{
"""
{
"styles": {
"user_dictionaries": [
"the path to nowhere"
]
}
}
""",
false
},
};

public static TheoryData<string, bool> InvalidStylesConfig =>
new()
{
{
"""
styles:
user_dictionaries: "path/to/dict.xaml"
""",
true
},
{
"""
{
"styles": {
"user_dictionaries": "path/to/dict.xaml"
}
}
""",
false
},
};

[Theory]
[MemberAutoSubstituteData<YamlLoaderCustomization>(nameof(NoStylesConfig))]
[MemberAutoSubstituteData<YamlLoaderCustomization>(nameof(InvalidPathsStylesConfig))]
[MemberAutoSubstituteData<YamlLoaderCustomization>(nameof(InvalidStylesConfig))]
public void Load_DoesNotAddUserDictionaries(string config, bool isYaml, IContext ctx)
{
// Given
YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml);
ctx.FileManager.FileExists(Arg.Is<string>(p => !p.Contains("yaml") && !p.Contains("json"))).Returns(false);

// When
bool result = YamlLoader.Load(ctx, showErrorWindow: false);

// Then
Assert.True(result);
ctx.ResourceManager.DidNotReceive().AddUserDictionary(Arg.Any<string>());
}
}
36 changes: 33 additions & 3 deletions src/Whim.Yaml/ErrorWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.UI.Xaml;

namespace Whim.Yaml;

/// <summary>
/// Exposes YAML errors encountered during parsing to the user.
/// </summary>
public sealed partial class ErrorWindow : Microsoft.UI.Xaml.Window, IDisposable
public sealed partial class ErrorWindow : Microsoft.UI.Xaml.Window, IDisposable, INotifyPropertyChanged
{
private readonly IContext _ctx;
private readonly WindowBackdropController _backdropController;
private string _message = string.Empty;

/// <summary>
/// The errors.
/// </summary>
public string Message { get; }
public string Message
{
get => _message;
private set
{
if (_message != value)
{
_message = value;
OnPropertyChanged();
}
}
}

/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;

private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

/// <summary>
/// Exposes YAML errors encountered during parsing to the user.
Expand Down Expand Up @@ -46,6 +67,15 @@ private void Quit_Click(object sender, RoutedEventArgs e)
_ctx.Exit(new ExitEventArgs() { Reason = ExitReason.User, Message = Message });
}

/// <summary>
/// Appends additional text to the error message.
/// </summary>
/// <param name="text">The text to append.</param>
public void AppendMessage(string text)
{
Message = Message + Environment.NewLine + text;
}

/// <inheritdoc/>
public void Dispose()
{
Expand Down
76 changes: 69 additions & 7 deletions src/Whim.Yaml/YamlLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class YamlLoader
{
private const string JsonConfigFileName = "whim.config.json";
private const string YamlConfigFileName = "whim.config.yaml";
private static ErrorWindow? _errorWindow;

/// <summary>
/// Loads and applies the declarative configuration from a JSON or YAML file.
Expand All @@ -30,16 +31,14 @@ public static bool Load(IContext ctx, bool showErrorWindow = true)
return false;
}

if (showErrorWindow)
{
ValidateConfig(ctx, schema);
}
ValidateConfig(ctx, schema, showErrorWindow);

UpdateWorkspaces(ctx, schema);

UpdateKeybinds(ctx, schema);
UpdateFilters(ctx, schema);
UpdateRouters(ctx, schema);
UpdateStyles(ctx, schema, showErrorWindow);

YamlPluginLoader.LoadPlugins(ctx, schema);
YamlLayoutEngineLoader.UpdateLayoutEngines(ctx, schema);
Expand Down Expand Up @@ -76,7 +75,7 @@ public static bool Load(IContext ctx, bool showErrorWindow = true)
return null;
}

private static void ValidateConfig(IContext ctx, Schema schema)
private static void ValidateConfig(IContext ctx, Schema schema, bool showErrorWindow)
{
ValidationContext result = schema.Validate(ValidationContext.ValidContext, ValidationLevel.Detailed);
if (result.IsValid)
Expand Down Expand Up @@ -106,8 +105,23 @@ private static void ValidateConfig(IContext ctx, Schema schema)
Logger.Error("Configuration file is not valid.");
Logger.Error(errors);

using ErrorWindow window = new(ctx, errors);
window.Activate();
if (showErrorWindow)
{
ShowError(ctx, errors);
}
}

private static void ShowError(IContext ctx, string errors)
{
if (_errorWindow == null)
{
_errorWindow = new(ctx, errors);
_errorWindow.Activate();
}
else
{
_errorWindow.AppendMessage(errors);
}
}

private static void UpdateWorkspaces(IContext ctx, Schema schema)
Expand Down Expand Up @@ -265,4 +279,52 @@ private static void UpdateRouters(IContext ctx, Schema schema)
}
}
}

private static void UpdateStyles(IContext ctx, Schema schema, bool showErrorWindow)
{
if (!schema.Styles.IsValid())
{
Logger.Debug("Styles config is not valid.");
return;
}

if (schema.Styles.UserDictionaries.AsOptional() is not { } userDictionaries)
{
Logger.Debug("No styles found.");
return;
}

foreach (var userDictionary in userDictionaries)
{
if (GetUserDictionaryPath(ctx, (string)userDictionary, showErrorWindow) is not string filePath)
{
continue;
}

ctx.ResourceManager.AddUserDictionary(filePath);
}
}

private static string? GetUserDictionaryPath(IContext ctx, string filePath, bool showErrorWindow)
{
if (ctx.FileManager.FileExists(filePath))
{
return filePath;
}

string relativePath = Path.Combine(ctx.FileManager.WhimDir, filePath);
if (!ctx.FileManager.FileExists(relativePath))
{
string error = $"User dictionary not found: {filePath}";
Logger.Error(error);

if (showErrorWindow)
{
ShowError(ctx, error);
}
return null;
}

return relativePath;
}
}
Loading

0 comments on commit 275b519

Please sign in to comment.