diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee1a035..a35f0c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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` | | `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. @@ -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` 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). --- @@ -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 @@ -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). +- **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. --- @@ -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. --- diff --git a/README.md b/README.md index 0a99f17..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()` + `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 @@ -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 @@ -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/ @@ -159,19 +160,18 @@ 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 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`) 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("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**. diff --git a/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs b/src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs index f795edc..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,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(); @@ -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(); @@ -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 a19f37a..3f1e10f 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/Application.cs +++ b/src/EasyWindowsApplication/CoreModule/Backend/Application.cs @@ -2,6 +2,7 @@ using EasyWindowsApplication.LayoutModule.Backend; using EasyWindowsApplication.LayoutModule.Frontend; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Share.Infrastructure; using EasyWindowsApplication.WindowingModule.Frontend; namespace EasyWindowsApplication.CoreModule.Backend; @@ -38,6 +39,7 @@ public IApplicationPostLayoutPhase Layout(Action configure) public IApplicationPostBehaviorPhase Behavior(Action configure) { BehaviorBuilder.Registry = _registry; + ControlAccess.SetController(BehaviorBuilder); configure(BehaviorBuilder); return this; } @@ -61,11 +63,6 @@ public void Initialize() RegisterMain(window); } - BehaviorBuilder.ApplyPending(_registry); - - 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/HandleRegistry.cs b/src/EasyWindowsApplication/CoreModule/Backend/HandleRegistry.cs index 5cebbbc..c25b5e0 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/HandleRegistry.cs +++ b/src/EasyWindowsApplication/CoreModule/Backend/HandleRegistry.cs @@ -33,4 +33,7 @@ internal void RegisterWindow(IBaseWindow window) internal T? GetWindow(string name) where T : class, IBaseWindow => _windowByName.TryGetValue(name, out var w) ? w as T : null; + + internal IBaseWindow? GetWindow(string name) + => _windowByName.TryGetValue(name, out var w) ? w : null; } diff --git a/src/EasyWindowsApplication/CoreModule/Backend/RECT.cs b/src/EasyWindowsApplication/CoreModule/Backend/RECT.cs index 67f425a..cc6ac0a 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/RECT.cs +++ b/src/EasyWindowsApplication/CoreModule/Backend/RECT.cs @@ -3,7 +3,7 @@ namespace EasyWindowsApplication.CoreModule.Backend; [StructLayout(LayoutKind.Sequential)] -public struct RECT +internal struct RECT { public int Left; public int Top; 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 4a1e0fa..300834b 100644 --- a/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs +++ b/src/EasyWindowsApplication/CoreModule/Frontend/BehaviorBuilderImpl.cs @@ -1,50 +1,15 @@ using EasyWindowsApplication.CoreModule.Backend; -using EasyWindowsApplication.Win32ControlsModule.Frontend; -using EasyWindowsApplication.WindowingModule.Frontend; +using EasyWindowsApplication.Share.Infrastructure; namespace EasyWindowsApplication.CoreModule.Frontend; -internal sealed class BehaviorBuilderImpl : IBehaviorBuilder +internal sealed class BehaviorBuilderImpl : IBehaviorBuilder, ControlAccess.IBehaviorServicesController { - internal Action? Win32Configurator { get; private set; } internal HandleRegistry? Registry { get; set; } - private readonly List<(string Name, Action Handler)> _pendingClicks = new(); - public IBehaviorBuilder OnClick(string controlName, Action handler) - { - var control = Registry?.GetByName(controlName); - if (control is IClickEventSource clickable) - { - clickable.AddClickHandler(handler); - } - else - { - _pendingClicks.Add((controlName, handler)); - } - return this; - } + T ControlAccess.IBehaviorServicesController.Get(string name) + => (T)Registry!.GetByName(name)!; - public IBehaviorBuilder WithWin32State(Action configure) - { - Win32Configurator = configure; - return this; - } - - public T Get(string name) where T : View - { - var control = Registry?.GetByName(name); - if (control is null) - throw new InvalidOperationException($"Control '{name}' not found."); - return (T)control; - } - - internal void ApplyPending(HandleRegistry registry) - { - foreach (var (name, handler) in _pendingClicks) - { - if (registry.GetByName(name) is IClickEventSource clickable) - clickable.AddClickHandler(handler); - } - _pendingClicks.Clear(); - } + T ControlAccess.IBehaviorServicesController.GetWindow(string name) + => (T)Registry!.GetWindow(name)!; } diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationLayoutPhase.cs b/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationLayoutPhase.cs index 87b3cb3..74078af 100644 --- a/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationLayoutPhase.cs +++ b/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationLayoutPhase.cs @@ -4,6 +4,5 @@ namespace EasyWindowsApplication.CoreModule.Frontend; public interface IApplicationLayoutPhase { - IApplicationPostLayoutPhase Layout(); IApplicationPostLayoutPhase Layout(Action configure); } \ No newline at end of file diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationPostLayoutPhase.cs b/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationPostLayoutPhase.cs index 6f927ed..7c5ffae 100644 --- a/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationPostLayoutPhase.cs +++ b/src/EasyWindowsApplication/CoreModule/Frontend/IApplicationPostLayoutPhase.cs @@ -2,7 +2,6 @@ namespace EasyWindowsApplication.CoreModule.Frontend; public interface IApplicationPostLayoutPhase { - IApplicationPostBehaviorPhase Behavior(); IApplicationPostBehaviorPhase Behavior(Action configure); void Initialize(); } \ No newline at end of file diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/IBehaviorBuilder.cs b/src/EasyWindowsApplication/CoreModule/Frontend/IBehaviorBuilder.cs index 2731e7c..0ffc43f 100644 --- a/src/EasyWindowsApplication/CoreModule/Frontend/IBehaviorBuilder.cs +++ b/src/EasyWindowsApplication/CoreModule/Frontend/IBehaviorBuilder.cs @@ -1,16 +1,3 @@ -using System.ComponentModel; -using EasyWindowsApplication.WindowingModule.Frontend; - namespace EasyWindowsApplication.CoreModule.Frontend; -public interface IBehaviorBuilder -{ - [EditorBrowsable(EditorBrowsableState.Never)] - IBehaviorBuilder OnClick(string controlName, Action handler); - - [EditorBrowsable(EditorBrowsableState.Never)] - IBehaviorBuilder WithWin32State(Action configure); - - [EditorBrowsable(EditorBrowsableState.Never)] - T Get(string name) where T : View; -} +public interface IBehaviorBuilder { } diff --git a/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs b/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs deleted file mode 100644 index 1b8af8b..0000000 --- a/src/EasyWindowsApplication/CoreModule/Frontend/IWin32State.cs +++ /dev/null @@ -1,8 +0,0 @@ -using EasyWindowsApplication.WindowingModule.Frontend; - -namespace EasyWindowsApplication.CoreModule.Frontend; - -public interface IWin32State -{ - T Get(string name) where T : View; -} diff --git a/src/EasyWindowsApplication/ControlAccess.cs b/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs similarity index 84% rename from src/EasyWindowsApplication/ControlAccess.cs rename to src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs index 0fe3d75..1ff78a5 100644 --- a/src/EasyWindowsApplication/ControlAccess.cs +++ b/src/EasyWindowsApplication/Share/Infrastructure/ControlAccess.cs @@ -1,8 +1,10 @@ +using System.ComponentModel; using EasyWindowsApplication.Win32ControlsModule.Frontend; using EasyWindowsApplication.WindowingModule.Frontend; -namespace EasyWindowsApplication; +namespace EasyWindowsApplication.Share.Infrastructure; +[EditorBrowsable(EditorBrowsableState.Never)] public static class ControlAccess { private static IBehaviorServicesController? _controller; diff --git a/src/EasyWindowsApplication/CoreModule/Backend/ControlBase.cs b/src/EasyWindowsApplication/Share/Infrastructure/ControlBase.cs similarity index 97% rename from src/EasyWindowsApplication/CoreModule/Backend/ControlBase.cs rename to src/EasyWindowsApplication/Share/Infrastructure/ControlBase.cs index b96e8f8..939f888 100644 --- a/src/EasyWindowsApplication/CoreModule/Backend/ControlBase.cs +++ b/src/EasyWindowsApplication/Share/Infrastructure/ControlBase.cs @@ -1,14 +1,19 @@ +using System.ComponentModel; using System.Runtime.InteropServices; +using EasyWindowsApplication.CoreModule.Backend; using EasyWindowsApplication.LayoutModule.Backend; using EasyWindowsApplication.LayoutModule.Frontend; using EasyWindowsApplication.Share; using EasyWindowsApplication.Win32ControlsModule.Frontend; using static EasyWindowsApplication.CoreModule.Backend.Win32; -namespace EasyWindowsApplication.CoreModule.Backend; +namespace EasyWindowsApplication.Share.Infrastructure; +[EditorBrowsable(EditorBrowsableState.Never)] public abstract class ControlBase : IControl, IClickEventSource, ILayoutable, IDockable { + internal ControlBase() { } + public nint Hwnd { get; internal set; } public string Name { get; set; } = ""; diff --git a/src/EasyWindowsApplication/BuildSupport/IconGenerator.cs b/src/EasyWindowsApplication/Share/Infrastructure/IconGenerator.cs similarity index 94% rename from src/EasyWindowsApplication/BuildSupport/IconGenerator.cs rename to src/EasyWindowsApplication/Share/Infrastructure/IconGenerator.cs index 4e1530b..b1c99eb 100644 --- a/src/EasyWindowsApplication/BuildSupport/IconGenerator.cs +++ b/src/EasyWindowsApplication/Share/Infrastructure/IconGenerator.cs @@ -1,8 +1,10 @@ +using System.ComponentModel; using SkiaSharp; using Svg.Skia; -namespace EasyWindowsApplication.BuildSupport; +namespace EasyWindowsApplication.Share.Infrastructure; +[EditorBrowsable(EditorBrowsableState.Never)] public static class IconGenerator { public static void GenerateIco(string svgPath, string icoPath, int[] sizes) diff --git a/src/EasyWindowsApplication/WindowingModule/Frontend/View.cs b/src/EasyWindowsApplication/Share/Infrastructure/View.cs similarity index 72% rename from src/EasyWindowsApplication/WindowingModule/Frontend/View.cs rename to src/EasyWindowsApplication/Share/Infrastructure/View.cs index c144397..569bbf5 100644 --- a/src/EasyWindowsApplication/WindowingModule/Frontend/View.cs +++ b/src/EasyWindowsApplication/Share/Infrastructure/View.cs @@ -1,10 +1,13 @@ -using EasyWindowsApplication.CoreModule.Backend; +using System.ComponentModel; using EasyWindowsApplication.Win32ControlsModule.Frontend; -namespace EasyWindowsApplication.WindowingModule.Frontend; +namespace EasyWindowsApplication.Share.Infrastructure; +[EditorBrowsable(EditorBrowsableState.Never)] public abstract class View : ControlBase where TSelf : View { + internal View() { } + public new TSelf OnMessage(uint msg, Win32MessageHandler handler) { base.OnMessage(msg, handler); diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs index 0b6a54b..591b3cc 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs @@ -1,4 +1,5 @@ using EasyWindowsApplication.CoreModule.Backend; +using EasyWindowsApplication.Share.Infrastructure; using EasyWindowsApplication.Win32ControlsModule.Frontend; namespace EasyWindowsApplication.Win32ControlsModule.Backend; diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IpAddress.cs b/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IpAddress.cs index f2e5305..908f150 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IpAddress.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Frontend/IpAddress.cs @@ -1,3 +1,4 @@ +using EasyWindowsApplication.Share.Infrastructure; using EasyWindowsApplication.Win32ControlsModule.Backend; using EasyWindowsApplication.WindowingModule.Frontend; diff --git a/src/EasyWindowsApplication/WindowingModule/Backend/UserControl.cs b/src/EasyWindowsApplication/WindowingModule/Backend/UserControl.cs index d6a6130..18d4d07 100644 --- a/src/EasyWindowsApplication/WindowingModule/Backend/UserControl.cs +++ b/src/EasyWindowsApplication/WindowingModule/Backend/UserControl.cs @@ -1,5 +1,6 @@ using EasyWindowsApplication.CoreModule.Backend; using EasyWindowsApplication.LayoutModule.Backend; +using EasyWindowsApplication.Share.Infrastructure; namespace EasyWindowsApplication.WindowingModule.Backend; diff --git a/src/EasyWindowsApplication/WindowingModule/Frontend/WindowsScroll.cs b/src/EasyWindowsApplication/WindowingModule/Frontend/WindowsScroll.cs index 3a72428..176445e 100644 --- a/src/EasyWindowsApplication/WindowingModule/Frontend/WindowsScroll.cs +++ b/src/EasyWindowsApplication/WindowingModule/Frontend/WindowsScroll.cs @@ -2,7 +2,7 @@ namespace EasyWindowsApplication.WindowingModule.Frontend; -public sealed class WindowsScroll : IWindowsScrollConfig +internal sealed class WindowsScroll : IWindowsScrollConfig { public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } = ScrollBarVisibility.Default; public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } = ScrollBarVisibility.Default; diff --git a/src/EasyWindowsApplication/WindowsApplication.cs b/src/EasyWindowsApplication/WindowsApplication.cs index 940af88..0b11dba 100644 --- a/src/EasyWindowsApplication/WindowsApplication.cs +++ b/src/EasyWindowsApplication/WindowsApplication.cs @@ -6,8 +6,6 @@ namespace EasyWindowsApplication; public static class WindowsApplication { - public static IApplicationLayoutPhase Resources() => new Application(); - public static IApplicationLayoutPhase Resources(Action configure) { var app = new Application(); @@ -15,8 +13,6 @@ public static IApplicationLayoutPhase Resources(Action con return app; } - public static IApplicationPostLayoutPhase Layout() => new Application(); - public static IApplicationPostLayoutPhase Layout(Action configure) => new Application().Layout(configure); } \ No newline at end of file diff --git a/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj b/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj index 170cced..c986b96 100644 --- a/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj +++ b/src/ProjectTemplates/EasyWinApp/EasyWinApp.csproj @@ -39,7 +39,7 @@ - + @@ -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 5c28f82..9b1a1a5 100644 --- a/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj +++ b/src/ProjectTemplates/SimpleEasyWinApp/SimpleEasyWinApp.csproj @@ -39,7 +39,7 @@ - + @@ -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 abb24d2..2e5e305 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/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") @@ -12,20 +13,23 @@ .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")); + }) ) ) ) - .Behavior(bh => bh - .BtnIncrement.OnClick(() => + .Behavior(bh => + { + 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 e2e3875..d76f94f 100644 --- a/src/Sample/Sample.csproj +++ b/src/Sample/Sample.csproj @@ -39,7 +39,7 @@ - + @@ -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 diff --git a/src/creators_vision.md b/src/creators_vision.md index 113ec5b..24e13c6 100644 --- a/src/creators_vision.md +++ b/src/creators_vision.md @@ -6,6 +6,7 @@ - `EasyWindowsApplication.Generators` es un proyecto de apoyo a la biblioteca `EasyWindowsApplication`. Mi idea es usar el poder de Source Generator para crear propiedades con el atributo `Name` declarados, donde el generador de código analiza en que contexto aparece en tiempo de compilación y genera una estructura fuertemente tipada (un strongly-typed alias). - `Common` es para código compartido y/o repetitivo entre los módulos, este siempre debe ser interno (internal). - `Share` es para código compartido y/o repetitivo entre los módulos (internamente), pero también externamente (public), para ser usados por los desarrolladores que emplean la biblioteca. +- `Share/Infrastructure` es para código compartido entre módulos que es `public` por razones técnicas (código generado, targets de build o herencia interna), pero NO parte de la API de usuario → debe llevar `[EditorBrowsable(EditorBrowsableState.Never)]`. - `WindowsApplication.cs` el punto de entrada del Fluent Api, debe ser simple, limpio, exponer lo que se necesita para la escribir el flujo evitando que se vuelva un fichero de codigo monolitico, esa es su unica funcion. - La aplicación se construye mediante una Fluent API declarativa dividida estrictamente en secciones secuenciales. Esto garantiza la separación de responsabilidades: ```csharp @@ -117,7 +118,7 @@ - **`Behavior`**: Enrutamiento de eventos y lógica. Gracias a los Source Generators, los nombres dados en el `Layout` (ej. `BtnGuardar`) aparecen aquí fuertemente tipados, eliminando los *Magic Strings*.Soporta dos modalidades: * Behavior (Implícito/Por defecto): Para un flujo moderno basado en estados y señales, ideal para la mayoría de aplicaciones. -* Behavior: Válvula de escape para desarrolladores avanzados. Expone el HWND y permite interceptar mensajes directamente en el WndProc (ej. WM_COMMAND, WM_PAINT) para cada control declarado en el Layout, ofreciendo el mismo nivel de control que C/C++ nativo. +* Behavior: Válvula de escape para desarrolladores avanzados. Expone el HWND y permite interceptar mensajes directamente en el WndProc (ej. WM_COMMAND, WM_PAINT) para cada control declarado en el Layout, ofreciendo el mismo nivel de control que C/C++ nativo. Es plomería interna (`IWin32State` es `internal`), se cablea automáticamente al activar `UseWinApi()`. - **`Initialize()`**: El nodo terminal que bloquea el hilo, cede el control al `CoreModule` y arranca el bucle de mensajes de Win32. En el flujo, es el nodo terminal de la Fluent API. No debe confundirse con un hook de ciclo de vida ni con las acciones del IDE (Build, Run). Su semántica es: en este punto la declaración está completa, todo ha sido definido y el sistema está habilitado para arrancar. Es el equivalente conceptual de `Build()` en el patrón Builder clásico, con la diferencia de que el nombre refleja que los Source Generators ya han actuado en tiempo de compilación, por lo que al llegar aquí no se construye nada, simplemente se lanza lo que ya está construido. - Plantilla para Visual Studio, esta debe servir como un "esqueleto" (scaffolding) para crear las carpetas y poner los archivos placeholder. @@ -640,7 +641,7 @@ Un único fichero `Program.cs` (top-level statements) equivalente al ejemplo de | Paquete | Rol | |---|---| -| `EasyWindowsApplication` (NuGet) | Framework + BuildSupport (`IconGenerator`, `.targets` de icono incluido) | +| `EasyWindowsApplication` (NuGet) | Framework + `Share/Infrastructure` (`IconGenerator`, `.targets` de icono incluido) | | `SkiaSharp` (transitivo) | Solo build-time (generación de .ico desde SVG), `PrivateAssets=all` | | `EasyWindowsApplication.Templates` (NuGet) | Contiene los templates `dotnet new easywinapp` y `simpleeasywinapp` |