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
45 changes: 26 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,22 @@ El framework está organizado en módulos bajo `src/EasyWindowsApplication/`:

| Módulo | Capa | Responsabilidad |
|---|---|---|
| `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 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`) |
| `Core` | `internal` (100%) | Punto de entrada (`WindowsApplication` → `Share/IApplicationLayoutPhase`), `Application`, `MasterRouter` (trampolín HWND-based), `HandleRegistry` (ConcurrentDictionary HWND→Router), `ControlBase`, `IconGenerator` |
| `Core/Windowing` | `internal` | `WindowImpl`, `AlternativeWindowImpl`, `Procedures` (CreateMainWindow/CreateAlternativeWindow con `[UnmanagedCallersOnly]`), `UserControl` |
| `Core/LayoutEngine` | `internal` | `LayoutEngine`, `ILayoutBuilder`, layouts (Grid, Stack, Dock), `ChildrenBuilderImpl` (usa `ControlActivatorRegistry`), `ViewBuilderImpl` |
| `Common` | `internal` | `ControlActivatorRegistry` + `IControlActivator` (registry genérico `Register<T>/Create<T>`, `EnsureInitialized()` + `ControlActivatorGenerator` `IIncrementalGenerator` con `InternalsVisibleTo`, MEF compile-time) — rompe ciclo `Common ↔ Core` sin `ModuleInitializer` (`CA2255`) |
| `Share` | `public` | API usuario: `IWindow`, `IAlternativeWindow`, `IWindowConfig`, `IChildrenBuilder` (dual overload `Action<View<T>>` + `Func<View<T>,View<T>>`), `View<T>` struct readonly (`Name/Margin/Width/Height/Background/Dock/Grid/OnClick/Text`), `LayoutLength`, `GridDefinitions`, `IResourcesDictionary`, etc. |
| `Share/Infrastructure` | `public` técnico | `[EditorBrowsable(Never)]`: `ControlAccess` (punto usado por Source Generator), `ViewBase<TSelf>` base para controles custom (ej. `IpAddress`) |
| `Win32ControlsModule` | Frontend `public` / Backend `internal` | Interfaces (`IButton`, `ILabel` …) heredando `IControl` + Backend (`Button`, `Label` → `Core.ControlBase`) + `Win32ControlActivator` (`IControlActivator`, `internal`, auto-registrado vía `ControlActivatorGenerator` + `EnsureInitialized()` en `Core/Application`) |
| `Generators` | `Analyzer` | `EasyBehaviorGenerator` (FQN `Share` + `GetTypeByMetadataName`) + `ControlActivatorGenerator` (descubre `IControlActivator` en `Compilation`, genera `ControlActivatorRegistrations.g.cs`, `InternalsVisibleTo`) |

### Principios de organización

- **`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.
- **`Common`** — código compartido `internal` (no visible para consumidores). Contiene `ControlActivatorRegistry` (evita reflexión, factorías genéricas).
- **`Share`** — API `public` para consumidores. Todo lo que devuelve `WindowsApplication` (`IApplicationLayoutPhase` etc.) vive aquí.
- **`Share/Infrastructure`** — `public` por razones técnicas (`[EditorBrowsable(Never)]`). Solo `ControlAccess` y `ViewBase<TSelf>` permanecen aquí; `ControlBase`/`IconGenerator` migraron a `Core/`.
- **`Core/**`** — 100% `internal`. Nada `public` aquí (si aparece `public` es bug, debe ser `internal` o moverse a `Share`). Implementaciones puras.
- **`Win32ControlsModule/Frontend`** — `public` interfaces controles; `Backend` — `internal` implementaciones que heredan `Core.ControlBase` y se registran vía `IControlActivator`.

---

Expand Down Expand Up @@ -118,10 +120,10 @@ Resources/

## Source Generators

El proyecto `EasyWindowsApplication.Generators` contiene `EasyBehaviorGenerator`, un `IIncrementalGenerator` que:
El proyecto `EasyWindowsApplication.Generators` contiene `EasyBehaviorGenerator`, un `IIncrementalGenerator` que (FQN actualizados a `EasyWindowsApplication.Share.*` + robustez `GetTypeByMetadataName`):

1. **Detecta** llamadas `.Name("...")`** dentro de lambdas `View<T>(...)` y `Window(...)` en el `Layout`.
2. **Genera** `EasyBehaviorExtensions` — un *extension property* para cada nombre, tipado con la interfaz del control:
1. **Detecta** llamadas `.Name("...")`** dentro de lambdas `View<T>(...)` (ahora `View<T>` es `Share/View<T>` struct) y `Window(...)` / `AlternativeWindow(...)` en el `Layout`.
2. **Genera** `EasyBehaviorExtensions` — un *extension property* para cada nombre, tipado con la interfaz del control (FQN `global::EasyWindowsApplication.Share.IWindow` / `IAlternativeWindow` / `global::EasyWindowsApplication.Share.IBehaviorBuilder`):
```csharp
// Auto-generado (ejemplo)
extension(IBehaviorBuilder bh) {
Expand All @@ -141,6 +143,9 @@ El proyecto `EasyWindowsApplication.Generators` contiene `EasyBehaviorGenerator`

> **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).

- **Robustez FQN**: `ISettingsBuilder` se resuelve vía `Compilation.GetTypeByMetadataName("EasyWindowsApplication.Share.ISettingsBuilder")` + `SymbolEqualityComparer.Default` (no string comparison). Si `GetTypeByMetadataName` retorna `null`, el gate `UseWinApi` no se activa.
- **View<T>**: el generador detecta `View<T>` genérico con `T : IControl`; `IWindow`/`IAlternativeWindow` usan FQN `Share` (antes `WindowingModule.Frontend`).

---

## Pipeline de iconos SVG→ICO
Expand Down Expand Up @@ -171,11 +176,13 @@ Los proyectos de plantilla incluyen un `Target` de MSBuild (`GenerateAppIcon`) q

Documenta aquí las decisiones de arquitectura relevantes. Usa ADRs (Architecture Decision Records) en `docs/adr/`.

- **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). 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.
- **Fluent API con interfaces de fase** — El IntelliSense guía el orden `Resources → Layout → Behavior → Initialize` mediante interfaces (`IApplicationLayoutPhase`, `IApplicationPostLayoutPhase`, `IApplicationPostBehaviorPhase`) ahora en `Share/` (antes `CoreModule/Frontend`). `WindowsApplication` devuelve `IApplicationLayoutPhase` público.
- **Controles como interfaces** (`IButton`, `ILabel`, ...) — permite que el Source Generator genere tipos débilesmente acoplados y facilita testing/mocking. `Button`/`Label` heredan `Core.ControlBase` (antes `Share.Infrastructure.ControlBase`).
- **MasterRouter** — centraliza el bucle de mensajes con trampolín `[UnmanagedCallersOnly(CallConvs=[Stdcall])]` + lookup HWND-based (`HandleRegistry.ConcurrentDictionary<HWND,MasterRouter>`), `try/catch` que evita tumbar proceso, despacha eventos tipados (`WM.COMMAND` → `Click`). El delegado estático sobrescrito fue eliminado (evita GC).
- **ControlAccess / ControlActivatorRegistry / ControlActivatorGenerator** — `ControlAccess` sigue en `Share/Infrastructure` (usado por `EasyBehaviorGenerator`). `ControlActivatorRegistry` en `Common/` (genérico `Register<T>/Create<T>`, `Shared` + `EnsureInitialized()` + `partial RegisterGeneratedActivators()`) rompe acoplamiento `Core → Win32ControlsModule` sin `ModuleInitializer` (`CA2255` en `Library`). `ControlActivatorGenerator` (`IIncrementalGenerator` en `Generators`, descubre `IControlActivator` en `Compilation` vía `GetTypeByMetadataName` + `AllInterfaces`, genera `ControlActivatorRegistrations.g.cs` con `new Win32ControlActivator().RegisterActivators(Shared)` y futuros plugins; usa `<InternalsVisibleTo Include="EasyWindowsApplication.Generators"/>` para mantener `internal`.
- **View<T> struct** — `Share/View<T>` readonly struct `where T : class, IControl` con `Instance` + fluents (`Name/Margin/Width/Height/Background/Dock/Grid/OnClick/Text`). Dual overload en `IChildrenBuilder`: `View<T>(Action<View<T>>)` + `View<T>(Func<View<T>,View<T>>)` instanciando `View<T>(control)` vía `ControlActivatorRegistry`.
- **`UseWinApi()` como bifurcación explícita** — gate **compile-time** (`EAWIN002`), consumidor Source Generator decide accessors. `IconGenerator` migró `Share/Infrastructure` → `Core/` (ahora `Core.IconGenerator`, IcoGen usa `EasyWindowsApplication.Core`).
- **AlternativeWindow** — ahora funcional: `Procedures.CreateAlternativeWindow` registra clase `WS_POPUP|CAPTION|SYSMENU` y `HandleRegistry.RegisterRouter(hwnd, router)`; `Application.RegisterAlternative` crea hwnd real, aplica BackgroundBrush y `Show()`.

---

Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ Crear una aplicación nativa nunca fue tan limpio. Todo se divide en 4 bloques l

```csharp
using EasyWindowsApplication;
using EasyWindowsApplication.Share;
using EasyWindowsApplication.Win32ControlsModule.Frontend;
using EasyWindowsApplication.WindowingModule.Frontend;

int counter = 0;

Expand All @@ -43,7 +43,7 @@ WindowsApplication
)
.Behavior(bh =>
{
var btn = bh.BtnGuardar; // tipado por Source Generator
var btn = bh.BtnGuardar; // tipado por Source Generator (View<T> wrapper)
btn.OnClick(() =>
{
counter++;
Expand Down Expand Up @@ -152,22 +152,23 @@ WindowsApplication
```
EasyWindowsApplication/src/
├── EasyWindowsApplication/ # Framework principal
│ ├── 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
│ └── WindowsApplication.cs # Punto de entrada del Fluent API
├── EasyWindowsApplication.Generators/ # Source Generator (EasyBehaviorGenerator)
│ ├── Core/ # Fases Fluent API, Resources, Behavior, MasterRouter (100% internal)
│ │ ├── Windowing/ # WindowImpl, AlternativeWindowImpl, Procedures (HWND-based WndProc)
│ │ └── LayoutEngine/ # ILayoutBuilder, Grid/Stack/Dock layouts, ViewBuilder
│ ├── Common/ # ControlActivatorRegistry + ControlActivatorGenerator (IIncrementalGenerator, InternalsVisibleTo)
│ ├── Share/ # API pública usuario (IWindow, ILayoutBuilder, View<T> struct, Color, Thickness)
│ │ └── Infrastructure/ # Técnicos [EditorBrowsable(Never)]: ControlAccess
│ ├── Win32ControlsModule/ # IButton, ILabel... + Backend (Button, Label → Core.ControlBase)
│ └── WindowsApplication.cs # Punto de entrada Fluent API (devuelve Share/IApplicationLayoutPhase)
├── EasyWindowsApplication.Generators/ # Source Generator (FQN Share.* + GetTypeByMetadataName)
└── ProjectTemplates/
├── EasyWinApp/ # Plantilla `dotnet new easywinapp`
└── SimpleEasyWinApp/ # Plantilla `dotnet new simpleeasywinapp`
```

## Cómo funciona

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.
El **Source Generator** `EasyBehaviorGenerator` analiza tu `Layout` en tiempo de compilación: cada `.Name("BtnGuardar")` dentro de un `View<T>` (struct `View<T>` wrapper con `Action<View<T>>` + `Func<View<T>,View<T>>`) o `Window` genera una propiedad tipada (`bh.BtnGuardar`) en `Behavior`. En runtime, el `MasterRouter` usa un trampolín `[UnmanagedCallersOnly]` con lookup HWND-based (`HandleRegistry.ConcurrentDictionary`) y despacha eventos tipados (`Click`, etc.); `ControlActivatorRegistry` en `Common/` (poblado en `Application.Initialize()` vía `ControlActivatorGenerator` — `IIncrementalGenerator` con `InternalsVisibleTo`, MEF compile-time) elimina el acoplamiento `Core → Win32Controls` sin `ModuleInitializer` (`CA2255`).

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:

Expand Down
97 changes: 97 additions & 0 deletions src/EasyWindowsApplication.Generators/ControlActivatorGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EasyWindowsApplication.Generators;

[Generator(LanguageNames.CSharp)]
public sealed class ControlActivatorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(context.CompilationProvider, (spc, compilation) =>
{
// Solo generar en el assembly que define ControlActivatorRegistry (EasyWindowsApplication)
var registryType = compilation.Assembly.GetTypeByMetadataName("EasyWindowsApplication.Common.ControlActivatorRegistry");
if (registryType is null) return;

var activatorInterface = compilation.GetTypeByMetadataName("EasyWindowsApplication.Common.IControlActivator");
if (activatorInterface is null) return;

var candidates = new List<INamedTypeSymbol>();

// Recorre tipos del assembly principal (EasyWindowsApplication) — incluye Win32ControlActivator y futuros plugins en mismo ensamblado
CollectTypes(compilation.Assembly.GlobalNamespace, activatorInterface, candidates);

var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#pragma warning disable CS0612, CS0618, CS1591, CA2255");
sb.AppendLine("using EasyWindowsApplication.Common;");
sb.AppendLine();
sb.AppendLine("namespace EasyWindowsApplication.Common;");
sb.AppendLine();
sb.AppendLine("internal sealed partial class ControlActivatorRegistry");
sb.AppendLine("{");
sb.AppendLine(" static partial void RegisterGeneratedActivators()");
sb.AppendLine(" {");

// Fallback hardcode para Win32ControlsModule (garantiza registro incluso si escaneo falla)
var hasWin32 = candidates.Any(c => c.Name == "Win32ControlActivator");
if (!hasWin32)
{
// Verificación por nombre si el escaneo por AllInterfaces falló por InternalsVisibleTo timing
sb.AppendLine(" new global::EasyWindowsApplication.Win32ControlsModule.Backend.Win32ControlActivator().RegisterActivators(Shared);");
}

foreach (var type in candidates.Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default).OrderBy(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
{
// Evitar generar la interfaz misma y evitar duplicar Win32 si ya hardcodeado
if (SymbolEqualityComparer.Default.Equals(type, activatorInterface)) continue;
if (type.Name == "Win32ControlActivator" && !hasWin32) continue; // ya emitido arriba
var fqn = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.AppendLine($" new {fqn}().RegisterActivators(Shared);");
}

sb.AppendLine(" }");
sb.AppendLine("}");

spc.AddSource("ControlActivatorRegistrations.g.cs", sb.ToString());
});
}

private static void CollectTypes(INamespaceSymbol ns, INamedTypeSymbol activatorInterface, List<INamedTypeSymbol> candidates)
{
foreach (var member in ns.GetMembers())
{
if (member is INamespaceSymbol nestedNs)
{
CollectTypes(nestedNs, activatorInterface, candidates);
}
else if (member is INamedTypeSymbol type)
{
CollectNestedTypes(type, activatorInterface, candidates);
}
}
}

private static void CollectNestedTypes(INamedTypeSymbol type, INamedTypeSymbol activatorInterface, List<INamedTypeSymbol> candidates)
{
// Incluir el tipo si implementa IControlActivator
foreach (var iface in type.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, activatorInterface))
{
if (!type.IsAbstract)
candidates.Add(type);
break;
}
}

// Recursión en tipos anidados
foreach (var nested in type.GetTypeMembers())
{
CollectNestedTypes(nested, activatorInterface, candidates);
}
}
}
Loading
Loading