Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,17 @@ El proyecto `EasyWindowsApplication.Generators` contiene `EasyBehaviorGenerator`
}
```
3. **Valida** duplicados en tiempo de compilación (`EAWIN001`: "Name '{0}' is already used...").
4. **Genera propiedades `internal static`** en la clase envolvente del `Behavior` (cuando se usa un lambda `.Behavior(bh => ...)`), para acceso directo sin el prefijo `bh.`.
4. **Valida el gate `UseWinApi()`** en tiempo de compilación (`EAWIN002`): si se declara un control (`View<T>` cuyo tipo hereda de `IControl` de `Win32ControlsModule`) sin activar `UseWinApi()`, emite un error y **no genera** ningún accessor de controles. Las ventanas no se gatean.
5. **Genera propiedades `internal static`** en la clase envolvente del `Behavior` (cuando se usa un lambda `.Behavior(bh => ...)`), para acceso directo sin el prefijo `bh.`.

### Detección de candidatos

- **Nombre**: invocaciones `.Name("string")` dentro de lambdas `View<T>()` / `Window()` / `AlternativeWindow()`.
- **Behavior**: invocaciones `.Behavior(bh => ...)` con un lambda (no method-group). Si se pasa un method-group, el enclosing-type no se detecta, pero las extensiones globales sí se generan.
- **`UseWinApi()`**: invocaciones `.UseWinApi()` (0 args) cuya verificación semántica confirme el símbolo `ISettingsBuilder.UseWinApi` (solo existe dentro de `.Resources(...).Setting(...)`).
- **Control**: invocaciones `View<T>(lambda)` cuyo `T` hereda de `EasyWindowsApplication.Win32ControlsModule.Frontend.IControl`.

> **Naming**: referenciar un control desde `Behavior` requiere `.Name()` explícito; sin `.Name()` no se genera accessor (el control sigue funcionando en el Layout por HWND). El auto-naming fue evaluado y **rechazado** (deriva semántica silenciosa al editar el Layout).

---

Expand All @@ -140,6 +145,8 @@ Los proyectos de plantilla incluyen un `Target` de MSBuild (`GenerateAppIcon`) q

> **Nota**: SkiaSharp es dependencia de *build-time* solo (no se incluye en la app final). El developer puede reemplazar el procesador implementando `IEasyImageProcessor`.

> **Alineación de versiones**: el proyecto temporal `IcoGen` debe referenciar la **misma** versión de `Svg.Skia` que el framework (`EasyWindowsApplication.csproj` usa `*` flotante). La línea se declara como `Version="%2A"` en los tres `.csproj` con el target (`Sample`, `EasyWinApp`, `SimpleEasyWinApp`): `%2A` es el escapado MSBuild de `*`, necesario porque un `*` literal en un `Include` de ItemGroup se interpreta como *wildcard* y MSBuild descarta el item. El `IcoGen.csproj` generado recibe `Version="*"`, de modo que ambos resuelven al mismo `Svg.Skia` en el mismo build. Si se *pinea* el framework, hay que actualizar también las 3 líneas del `IcoGen`; un desajuste produce `System.IO.FileNotFoundException` al ejecutar `IcoGen` (código `-532462766`).

---

## Native AOT
Expand All @@ -158,6 +165,7 @@ Documenta aquí las decisiones de arquitectura relevantes. Usa ADRs (Architectur
- **Controles como interfaces** (`IButton`, `ILabel`, ...) — permite que el Source Generator genere tipos débilesmente acoplados y facilita testing/mocking.
- **MasterRouter** — centraliza el bucle de mensajes (`WndProc` → `DefWindowProcW`) y despacha eventos tipados (`WM.COMMAND` → `Click`).
- **ControlAccess** — punto de acceso público para resolver controles por nombre desde el Behavior (usado por las propiedades generadas). Vive en `Share/Infrastructure` (técnico, `[EditorBrowsable(Never)]`). `SetController` se cablea en `Application.Behavior(...)`.
- **`UseWinApi()` como bifurcación explícita** — elegir controles nativos por HWND (comctl32) frente a los futuros controles GDI/GDI+ personalizados. Es un gate **compile-time** (`EAWIN002`), no un flag decorativo: el consumidor es el Source Generator, que decide qué accessors existen. Con los módulos MVU futuros, lo que reciba el lambda de `.Behavior(...)` dependerá del mismo flag.

---

Expand Down Expand Up @@ -235,7 +243,7 @@ Setup manual (una sola vez):

### Notas para mantenedores

- **`Svg.Skia` usa `*`** en `EasyWindowsApplication.csproj`. Si un build empieza a fallar tras una actualización de dependencias, considera *pinnear* la versión.
- **`Svg.Skia` usa `*`** en `EasyWindowsApplication.csproj`. Si un build empieza a fallar tras una actualización de dependencias, considera *pinnear* la versión — en ese caso actualiza también las 3 líneas del proyecto `IcoGen` (ver [Pipeline de iconos SVG→ICO](#pipeline-de-iconos-svg-ico)).
- **Tags de GitHub**: `v{x.y.z}` (estable) o `v{x.y.z}-preview`. Si el workflow falla al crear el tag por duplicado, elimínalo manualmente (`git push origin --delete v{tag}` o desde la interfaz) y vuelve a disparar el workflow.

---
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* **UX simplificada** — 4 bloques secuenciales (`Resources → Layout → Behavior → Initialize`) que reducen el esfuerzo de cableado y aceleran la entrega temprana.
* **Fluent API declarativa** — El IntelliSense te guía de Resources a Layout a Behavior a Initialize, impidiendo equivocarte de orden.
* **Source Generators** — Acceso tipado a controles por su nombre. Si te equivocas, falla en compilación, no en runtime.
* **Válvula de escape Win32** — `UseWinApi()` habilita el uso del módulo de controles Win32 nativos de alto rendimiento (`Win32ControlsModule`) cuando lo necesites.
* **Controles Win32 nativos** — `UseWinApi()` es el *gate* obligatorio para declarar controles de `Win32ControlsModule` en `Layout`: sin él el compilador lo rechaza (EAWIN002); con él, el generador emite los accessors tipados.

## Un vistazo al código

Expand All @@ -24,6 +24,7 @@ using EasyWindowsApplication.WindowingModule.Frontend;
int counter = 0;

WindowsApplication
.Resources(rd => rd.Setting(st => st.UseWinApi()))
.Layout(ly => ly
.Window(iw => iw
.Name("MainWindow")
Expand Down Expand Up @@ -119,7 +120,7 @@ El framework incluye **dos plantillas** oficiales:
La fase `Resources` registra todo lo que tu app necesita antes de dibujar:

* **Assets** — imágenes, iconos (`.svg` → `.ico` multi-resolución) y fuentes.
* **Settings** — configuración embebida y persistencia (`UseWinApi`, `AppConfigFile`).
* **Settings** — configuración embebida y persistencia (`UseWinApi`, `AppConfigFile`). `UseWinApi` es el *gate* obligatorio para usar controles Win32 nativos.
* **Services** — contenedor de inyección de dependencias.

```csharp
Expand Down Expand Up @@ -159,7 +160,7 @@ EasyWindowsApplication/src/

El **Source Generator** analiza tu `Layout` en tiempo de compilación: cada `.Name("BtnGuardar")` dentro de un `View<T>` o `Window` genera una propiedad tipada (`bh.BtnGuardar`) en `Behavior`. Al compilar, ya no necesitas localizar controles por string — si el nombre no coincide, el compilador te lo dice. En runtime, el `MasterRouter` centraliza el bucle de mensajes de Win32 y despacha eventos tipados (`Click`, etc.) de forma automatica.

Para usar los controles nativos del módulo Win32, activa `UseWinApi()` en Resources:
Para usar los controles nativos del módulo Win32, activa `UseWinApi()` en Resources. **Es obligatorio**: si declaras un control (`View<T>`) sin él, el compilador lo rechaza con `EAWIN002` y no se genera ningún accessor:

```csharp
WindowsApplication
Expand All @@ -169,7 +170,7 @@ WindowsApplication
.Initialize();
```

> **Nota**: el acceso de bajo nivel al HWND/WndProc (`WithWin32State`) es plomería interna del framework; se cableará automáticamente según el patrón MVU cuando `UseWinApi()` esté activo.
> **Nota**: el acceso de bajo nivel al HWND/WndProc es plomería interna del framework. Con los futuros módulos MVU, lo que reciba el lambda de `.Behavior(...)` dependerá de que `UseWinApi()` esté activo — resuelto en compile-time por el Source Generator.

## ¿Desarrollas el framework?

Expand Down
111 changes: 109 additions & 2 deletions src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public sealed class EasyBehaviorGenerator : IIncrementalGenerator
DiagnosticSeverity.Error,
isEnabledByDefault: true);

private static readonly DiagnosticDescriptor UseWinApiRequiredRule = new(
id: "EAWIN002",
title: "UseWinApi required",
messageFormat: "UseWinApi() is required to use Win32 controls in Layout. Add .Resources(s => s.Setting(x => x.UseWinApi())).",
category: "EasyWindowsApplication",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var namedItems = context.SyntaxProvider.CreateSyntaxProvider(
Expand All @@ -37,9 +45,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

var enclosingType = behaviorCalls.Collect();

context.RegisterSourceOutput(collected.Combine(enclosingType), static (spc, pair) =>
var useWinApi = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => IsUseWinApiCandidate(node),
transform: static (ctx, _) => IsUseWinApiSymbol(ctx))
.Where(static enabled => enabled)
.Collect()
.Select(static (items, _) => !items.IsDefaultOrEmpty);

var controlItems = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => IsViewControlCandidate(node),
transform: static (ctx, _) => ExtractControlLocation(ctx))
.Where(static location => location is not null);

var controlLocations = controlItems.Collect();

context.RegisterSourceOutput(
collected.Combine(enclosingType).Combine(useWinApi).Combine(controlLocations),
static (spc, quadruple) =>
{
var (items, enclosing) = pair;
var (((items, enclosing), usesWinApi), controls) = quadruple;

if (!usesWinApi && !controls.IsDefaultOrEmpty)
{
foreach (var location in controls)
spc.ReportDiagnostic(Diagnostic.Create(UseWinApiRequiredRule, location));
}

if (items.IsDefaultOrEmpty) return;

var seen = new HashSet<string>();
Expand Down Expand Up @@ -74,6 +105,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

foreach (var item in unique)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
Expand Down Expand Up @@ -113,6 +145,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

foreach (var item in unique)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
Expand Down Expand Up @@ -226,6 +259,80 @@ private static bool IsBehaviorCandidate(SyntaxNode node)
return true;
}

private static bool IsUseWinApiCandidate(SyntaxNode node)
{
if (node is not InvocationExpressionSyntax inv)
return false;
if (inv.Expression is not MemberAccessExpressionSyntax ma)
return false;
if (ma.Name.Identifier.Text != "UseWinApi")
return false;
if (inv.ArgumentList.Arguments.Count != 0)
return false;
return true;
}

private static bool IsUseWinApiSymbol(GeneratorSyntaxContext context)
{
var inv = (InvocationExpressionSyntax)context.Node;
if (inv.Expression is not MemberAccessExpressionSyntax ma)
return false;
var symbol = context.SemanticModel.GetSymbolInfo(ma).Symbol;
return symbol is IMethodSymbol method
&& method.Name == "UseWinApi"
&& method.ContainingType is { } containingType
&& containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
== "global::EasyWindowsApplication.CoreModule.Frontend.ISettingsBuilder";
}

private static bool IsViewControlCandidate(SyntaxNode node)
{
if (node is not InvocationExpressionSyntax inv)
return false;
if (inv.Expression is not MemberAccessExpressionSyntax ma)
return false;
if (ma.Name is not GenericNameSyntax generic)
return false;
if (generic.Identifier.Text != "View")
return false;
if (generic.TypeArgumentList.Arguments.Count != 1)
return false;
if (inv.ArgumentList.Arguments.Count != 1)
return false;
if (inv.ArgumentList.Arguments[0].Expression is not LambdaExpressionSyntax)
return false;
return true;
}

private static Location ExtractControlLocation(GeneratorSyntaxContext context)
{
var inv = (InvocationExpressionSyntax)context.Node;
if (inv.Expression is not MemberAccessExpressionSyntax ma)
return null;
if (ma.Name is not GenericNameSyntax generic)
return null;

var typeSymbol = context.SemanticModel.GetTypeInfo(generic.TypeArgumentList.Arguments[0]).Type;
if (typeSymbol is null)
return null;

var iControl = context.SemanticModel.Compilation.GetTypeByMetadataName(
"EasyWindowsApplication.Win32ControlsModule.Frontend.IControl");
if (iControl is null)
return null;

if (typeSymbol.Equals(iControl, SymbolEqualityComparer.Default))
return inv.GetLocation();

foreach (var iface in typeSymbol.AllInterfaces)
{
if (iface.Equals(iControl, SymbolEqualityComparer.Default))
return inv.GetLocation();
}

return null;
}

private static EnclosingTypeInfo ExtractEnclosingType(GeneratorSyntaxContext context)
{
var current = context.Node.Parent;
Expand Down
16 changes: 0 additions & 16 deletions src/EasyWindowsApplication/CoreModule/Backend/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ public void Initialize()
RegisterMain(window);
}

if (BehaviorBuilder.Win32Configurator is not null)
BehaviorBuilder.Win32Configurator(new Win32StateImpl(_registry));

Procedures.RunMessageLoop();
}

Expand All @@ -91,17 +88,4 @@ private void RegisterAlternative(WindowModel window)
_registry.RegisterWindow(new WindowingModule.Backend.AlternativeWindowImpl(
0, 0, window.Name, window.Title, window.Width, window.Height, window.Position));
}
}

internal sealed class Win32StateImpl : IWin32State
{
private readonly HandleRegistry _registry;
internal Win32StateImpl(HandleRegistry registry) => _registry = registry;

public T Get<T>(string name) where T : View<T>
{
var control = _registry.GetByName(name)
?? throw new InvalidOperationException($"Control '{name}' not found in the layout.");
return (T)control;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ public IResourcesDictionary Services(Action<IServicesBuilder> configure)

internal sealed class SettingsBuilderImpl : ISettingsBuilder
{
internal bool IsWinApiEnabled { get; private set; }
internal AppConfigFileConfig AppConfig { get; } = new();

public ISettingsBuilder UseWinApi()
{
IsWinApiEnabled = true;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ namespace EasyWindowsApplication.CoreModule.Frontend;

internal sealed class BehaviorBuilderImpl : IBehaviorBuilder, ControlAccess.IBehaviorServicesController
{
internal Action<IWin32State>? Win32Configurator { get; private set; }
internal HandleRegistry? Registry { get; set; }

internal void SetWin32Configurator(Action<IWin32State> configure) => Win32Configurator = configure;

T ControlAccess.IBehaviorServicesController.Get<T>(string name)
=> (T)Registry!.GetByName(name)!;

Expand Down
8 changes: 0 additions & 8 deletions src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<_IcoProjLine Include=" &lt;Nullable&gt;enable&lt;/Nullable&gt;" />
<_IcoProjLine Include=" &lt;/PropertyGroup&gt;" />
<_IcoProjLine Include=" &lt;ItemGroup&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;5.1.1&quot; /&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;%2A&quot; /&gt;" />
<_IcoProjLine Include=" &lt;ProjectReference Include=&quot;$(_EasyWAProj)&quot; /&gt;" />
<_IcoProjLine Include=" &lt;/ItemGroup&gt;" />
<_IcoProjLine Include="&lt;/Project&gt;" />
Expand Down
1 change: 1 addition & 0 deletions src/ProjectTemplates/SimpleEasyWinApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
int counter = 0;

WindowsApplication
.Resources(rd => rd.Setting(st => st.UseWinApi()))
.Layout(ly => ly
.Window(iw => iw
.Name("MainWindow")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<_IcoProjLine Include=" &lt;Nullable&gt;enable&lt;/Nullable&gt;" />
<_IcoProjLine Include=" &lt;/PropertyGroup&gt;" />
<_IcoProjLine Include=" &lt;ItemGroup&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;5.1.1&quot; /&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;%2A&quot; /&gt;" />
<_IcoProjLine Include=" &lt;ProjectReference Include=&quot;$(_EasyWAProj)&quot; /&gt;" />
<_IcoProjLine Include=" &lt;/ItemGroup&gt;" />
<_IcoProjLine Include="&lt;/Project&gt;" />
Expand Down
14 changes: 8 additions & 6 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@
using EasyWindowsApplication.Win32ControlsModule.Frontend;
using EasyWindowsApplication.WindowingModule.Frontend;


int counter = 0;

WindowsApplication
.Resources(rd => rd.Setting(st => st.UseWinApi()))
.Layout(ly => ly
.Window(iw => iw
.Name("MainWindow")
.Title("Easy Win App")
.Dimensions(420, 280)
.Position(WindowPositionOnScreen.Center)
.Content(c => c
.Children(ch => ch
.View<IButton>(btn => btn
.Children(ch =>
{
ch.View<IButton>(btn => btn
.Name("BtnIncrement")
.Text("Click me")
)
)
);
ch.View<ILabel>(lb => lb.Name("LbShowIncrement").Text("0 Increment"));
})
)
)
)
Expand All @@ -27,7 +29,7 @@
bh.BtnIncrement.OnClick(() =>
{
counter++;
bh.BtnIncrement.Text = $"Click: {counter}";
bh.LbShowIncrement.Text = $"{counter} Increment";
});
})
.Initialize();
2 changes: 1 addition & 1 deletion src/Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<_IcoProjLine Include=" &lt;Nullable&gt;enable&lt;/Nullable&gt;" />
<_IcoProjLine Include=" &lt;/PropertyGroup&gt;" />
<_IcoProjLine Include=" &lt;ItemGroup&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;5.1.1&quot; /&gt;" />
<_IcoProjLine Include=" &lt;PackageReference Include=&quot;Svg.Skia&quot; Version=&quot;%2A&quot; /&gt;" />
<_IcoProjLine Include=" &lt;ProjectReference Include=&quot;$(_EasyWAProj)&quot; /&gt;" />
<_IcoProjLine Include=" &lt;/ItemGroup&gt;" />
<_IcoProjLine Include="&lt;/Project&gt;" />
Expand Down
Binary file modified src/Sample/Sample.ico
Binary file not shown.
Loading