diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad2055c..a35f0c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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` 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()` / `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(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). --- @@ -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 @@ -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. --- @@ -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. --- diff --git a/README.md b/README.md index 9237953..2308226 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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") @@ -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 @@ -159,7 +160,7 @@ EasyWindowsApplication/src/ El **Source Generator** analiza tu `Layout` en tiempo de compilación: cada `.Name("BtnGuardar")` dentro de un `View` 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`) sin él, el compilador lo rechaza con `EAWIN002` y no se genera ningún accessor: ```csharp WindowsApplication @@ -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? diff --git a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs index 5680fff..b1f4fa8 100644 --- a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs +++ b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs @@ -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( @@ -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(); @@ -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) { @@ -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) { @@ -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; diff --git a/src/EasyWindowsApplication/CoreModule/Backend/Application.cs b/src/EasyWindowsApplication/CoreModule/Backend/Application.cs index 60d1427..3f1e10f 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/Application.cs +++ b/src/EasyWindowsApplication/CoreModule/Backend/Application.cs @@ -63,9 +63,6 @@ public void Initialize() RegisterMain(window); } - if (BehaviorBuilder.Win32Configurator is not null) - BehaviorBuilder.Win32Configurator(new Win32StateImpl(_registry)); - Procedures.RunMessageLoop(); } @@ -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(string name) where T : View - { - var control = _registry.GetByName(name) - ?? throw new InvalidOperationException($"Control '{name}' not found in the layout."); - return (T)control; - } } \ No newline at end of file diff --git a/src/EasyWindowsApplication/CoreModule/Backend/ResourcesDictionaryImpl.cs b/src/EasyWindowsApplication/CoreModule/Backend/ResourcesDictionaryImpl.cs index b2aa786..11e69dd 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/ResourcesDictionaryImpl.cs +++ b/src/EasyWindowsApplication/CoreModule/Backend/ResourcesDictionaryImpl.cs @@ -22,12 +22,10 @@ public IResourcesDictionary Services(Action configure) internal sealed class SettingsBuilderImpl : ISettingsBuilder { - internal bool IsWinApiEnabled { get; private set; } internal AppConfigFileConfig AppConfig { get; } = new(); public ISettingsBuilder UseWinApi() { - IsWinApiEnabled = true; return this; } diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs b/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs index 820cd81..300834b 100644 --- a/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs +++ b/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs @@ -5,11 +5,8 @@ namespace EasyWindowsApplication.CoreModule.Frontend; internal sealed class BehaviorBuilderImpl : IBehaviorBuilder, ControlAccess.IBehaviorServicesController { - internal Action? Win32Configurator { get; private set; } internal HandleRegistry? Registry { get; set; } - internal void SetWin32Configurator(Action configure) => Win32Configurator = configure; - T ControlAccess.IBehaviorServicesController.Get(string name) => (T)Registry!.GetByName(name)!; diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs b/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs deleted file mode 100644 index 4fba86f..0000000 --- a/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs +++ /dev/null @@ -1,8 +0,0 @@ -using EasyWindowsApplication.Share.Infrastructure; - -namespace EasyWindowsApplication.CoreModule.Frontend; - -internal interface IWin32State -{ - T Get(string name) where T : View; -} diff --git a/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj b/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj index 1f7f1ac..c986b96 100644 --- a/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj +++ b/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj @@ -51,7 +51,7 @@ <_IcoProjLine Include=" <Nullable>enable</Nullable>" /> <_IcoProjLine Include=" </PropertyGroup>" /> <_IcoProjLine Include=" <ItemGroup>" /> - <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="5.1.1" />" /> + <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="%2A" />" /> <_IcoProjLine Include=" <ProjectReference Include="$(_EasyWAProj)" />" /> <_IcoProjLine Include=" </ItemGroup>" /> <_IcoProjLine Include="</Project>" /> diff --git a/src/ProjectTemplates/SimpleEasyWinApp/Program.cs b/src/ProjectTemplates/SimpleEasyWinApp/Program.cs index abb24d2..d5f5044 100644 --- a/src/ProjectTemplates/SimpleEasyWinApp/Program.cs +++ b/src/ProjectTemplates/SimpleEasyWinApp/Program.cs @@ -5,6 +5,7 @@ int counter = 0; WindowsApplication + .Resources(rd => rd.Setting(st => st.UseWinApi())) .Layout(ly => ly .Window(iw => iw .Name("MainWindow") diff --git a/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj b/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj index 9d08de1..9b1a1a5 100644 --- a/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj +++ b/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj @@ -51,7 +51,7 @@ <_IcoProjLine Include=" <Nullable>enable</Nullable>" /> <_IcoProjLine Include=" </PropertyGroup>" /> <_IcoProjLine Include=" <ItemGroup>" /> - <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="5.1.1" />" /> + <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="%2A" />" /> <_IcoProjLine Include=" <ProjectReference Include="$(_EasyWAProj)" />" /> <_IcoProjLine Include=" </ItemGroup>" /> <_IcoProjLine Include="</Project>" /> diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index 333b816..2e5e305 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -2,10 +2,10 @@ 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") @@ -13,12 +13,14 @@ .Dimensions(420, 280) .Position(WindowPositionOnScreen.Center) .Content(c => c - .Children(ch => ch - .View(btn => btn + .Children(ch => + { + ch.View(btn => btn .Name("BtnIncrement") .Text("Click me") - ) - ) + ); + ch.View(lb => lb.Name("LbShowIncrement").Text("0 Increment")); + }) ) ) ) @@ -27,7 +29,7 @@ bh.BtnIncrement.OnClick(() => { counter++; - bh.BtnIncrement.Text = $"Click: {counter}"; + bh.LbShowIncrement.Text = $"{counter} Increment"; }); }) .Initialize(); diff --git a/src/Sample/Sample.csproj b/src/Sample/Sample.csproj index 5772094..d76f94f 100644 --- a/src/Sample/Sample.csproj +++ b/src/Sample/Sample.csproj @@ -51,7 +51,7 @@ <_IcoProjLine Include=" <Nullable>enable</Nullable>" /> <_IcoProjLine Include=" </PropertyGroup>" /> <_IcoProjLine Include=" <ItemGroup>" /> - <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="5.1.1" />" /> + <_IcoProjLine Include=" <PackageReference Include="Svg.Skia" Version="%2A" />" /> <_IcoProjLine Include=" <ProjectReference Include="$(_EasyWAProj)" />" /> <_IcoProjLine Include=" </ItemGroup>" /> <_IcoProjLine Include="</Project>" /> diff --git a/src/Sample/Sample.ico b/src/Sample/Sample.ico index fae7f03..0f483cf 100644 Binary files a/src/Sample/Sample.ico and b/src/Sample/Sample.ico differ