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
23 changes: 16 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ El framework está organizado en módulos bajo `src/EasyWindowsApplication/`:
|---|---|---|
| `CoreModule` | Backend + Frontend | Punto de entrada (`WindowsApplication`), fases del Fluent API, Resources, Behavior, MasterRouter, HandleRegistry |
| `LayoutModule` | Backend + Frontend | `ILayoutBuilder`, layouts (Grid, Stack, Dock), `IContentBuilder`, `IChildrenBuilder`, `IViewBuilder` |
| `Share` | Frontend (público) | Tipos públicos compartidos: `Color`, `Thickness`, `IContentBuilder`, `LayoutLength`, `LayoutOptions` |
| `Share` | Frontend (público) | Tipos públicos de la API de usuario: `Color`, `Thickness`, `IContentBuilder`, `LayoutLength`, `LayoutOptions` |
| `Share/Infrastructure` | Frontend (público técnico) | Tipos `public` **por razones técnicas** con `[EditorBrowsable(Never)]`: `ControlAccess`, `IconGenerator`, `ControlBase`, `View<T>` |
| `Win32ControlsModule` | Backend + Frontend | Interfaces de controles (`IButton`, `ILabel`, `IEdit`, ...) y sus implementaciones Win32 |
| `WindowingModule` | Backend + Frontend | Ventanas (`IWindow`, `IAlternativeWindow`), posicionamiento (`WindowPositionOnScreen`) |
| `BuildSupport` | Frontend (público) | `IconGenerator` — pipeline SVG→ICO en build-time |

### Principios de organización

- **`Common`** — código compartido entre módulos, siempre `internal`.
- **`Share`** — código compartido entre módulos que también es **público** para consumidores del framework.
- **`Common`** — código compartido entre módulos, pero de acceso **`internal`** (no visible para consumidores).
- **`Share`** — código compartido entre módulos, pero de acceso **`public`** (expuesto a consumidores del framework).
- **`Share/Infrastructure`** — código compartido entre módulos, `public` **por razones técnicas** (lo alcanzan el código generado, los targets de build o la herencia interna), pero **NO** parte de la API de usuario → obligatorio `[EditorBrowsable(EditorBrowsableState.Never)]`. Si un archivo ahí no lo tiene, es un bug.
- **`CoreModule/Frontend`** — interfaces de las fases del Fluent API (`IApplicationLayoutPhase` → `IApplicationPostLayoutPhase` → `IApplicationPostBehaviorPhase`) que guían por IntelliSense el orden correcto.
- **`Backend`** — implementaciones `internal` de esas interfaces.

Expand Down Expand Up @@ -118,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 @@ -139,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 @@ -156,7 +164,8 @@ Documenta aquí las decisiones de arquitectura relevantes. Usa ADRs (Architectur
- **Fluent API con interfaces de fase** — El IntelliSense guía el orden `Resources → Layout → Behavior → Initialize` mediante interfaces (`IApplicationLayoutPhase`, `IApplicationPostLayoutPhase`, `IApplicationPostBehaviorPhase`).
- **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). <!-- TODO: verificar que SetController se cablee en Initialize() -->
- **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 @@ -234,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
16 changes: 8 additions & 8 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()` + `WithWin32State` para acceso directo al HWND y WndProc 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 All @@ -145,9 +146,9 @@ EasyWindowsApplication/src/
│ ├── CoreModule/ # Fases del Fluent API, Resources, Behavior
│ ├── LayoutModule/ # ILayoutBuilder, Grid, Stack layouts
│ ├── Share/ # Tipos públicos (Color, Thickness, IContentBuilder)
│ │ └── Infrastructure/ # Tipos públicos técnicos ([EditorBrowsable(Never)])
│ ├── Win32ControlsModule/ # IButton, ILabel, IEdit, ICheckBox, ...
│ ├── WindowingModule/ # IWindow, WindowPositionOnScreen
│ ├── BuildSupport/ # IconGenerator (pipeline SVG→ICO en build-time)
│ └── WindowsApplication.cs # Punto de entrada del Fluent API
├── EasyWindowsApplication.Generators/ # Source Generator (EasyBehaviorGenerator)
└── ProjectTemplates/
Expand All @@ -159,19 +160,18 @@ 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 acceso total al HWND y WndProc, activa `UseWinApi()` en Resources y usa `WithWin32State`:
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
.Resources(r => r.Setting(s => s.UseWinApi()))
.Layout(...)
.Behavior(b => b.WithWin32State(ctrl => {
var btn = ctrl.Get<IButton>("BtnGuardar");
btn.OnMessage(WM.COMMAND, (w, l) => { /* tu código Win32 puro */ });
}))
.Behavior(bh => { ... })
.Initialize();
```

> **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?

Toda la información para compilar desde el código fuente, entender la arquitectura de módulos, el pipeline de generación de iconos y las decisiones de diseño se encuentran en [`CONTRIBUTING.md`](CONTRIBUTING.md) y en la **Wiki del repositorio**.
119 changes: 113 additions & 6 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,17 +105,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

foreach (var item in unique)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
sb.AppendLine($" public {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.ControlAccess");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .GetWindow<{item.TypeSymbol}>(\"{item.Name}\");");
}
else
{
sb.AppendLine($" public {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.ControlAccess");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");");
}
sb.AppendLine();
Expand Down Expand Up @@ -113,17 +145,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

foreach (var item in unique)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
sb.AppendLine($" internal static {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.ControlAccess");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .GetWindow<{item.TypeSymbol}>(\"{item.Name}\");");
}
else
{
sb.AppendLine($" internal static {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.ControlAccess");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");");
}
sb.AppendLine();
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
Loading
Loading