Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f3a8867
feat: motor de menús declarativo + SystemTray con triggers tipados (#48)
cl2raul66 Sep 17, 2026
81dfa1b
fix: callbacks tray empaquetados, tooltip clásico y catálogo Culture …
cl2raul66 Sep 20, 2026
19ccb6d
refactor: consolidar entrada de usuario en IInputSurface compartido (…
cl2raul66 Sep 22, 2026
f8eaf69
feat(core): execute UI pipeline on dedicated STA thread (EWA-UI); add…
cl2raul66 Sep 28, 2026
51a6448
feat(dialogs): redesign TaskDialog API — Text/Content builder, Primar…
cl2raul66 Sep 28, 2026
64daeaa
feat(dialogs): unify file dialogs API — move DefaultDirectory/MultiSe…
cl2raul66 Sep 28, 2026
ae64062
feat(dialogs): DefaultExtension(FilePattern) in ISaveFileDialog — com…
cl2raul66 Sep 28, 2026
a8c1b9d
docs: update system_design.md and CONTRIBUTING.md with new dialog API…
cl2raul66 Sep 28, 2026
fc6e248
feat(dialogs): add ISystemDialog overload support in LayoutBuilder an…
cl2raul66 Sep 28, 2026
e07d8a7
feat(dialogs): add core dialog infrastructure — FilePatternsGenerator…
cl2raul66 Sep 28, 2026
a70f57e
feat(lifecycle): app termination gate + Close/Show recreate for Alter…
cl2raul66 Sep 30, 2026
3c36acf
fix(lifecycle): reuse control instances on recreate + teardown second…
cl2raul66 Oct 1, 2026
816948e
feat(input): non-generic OnInputWithSpatialPosition() + semantic coun…
cl2raul66 Oct 5, 2026
6b2a3b9
refactor(lifecycle): HandleRegistry HwndOnly unregister + MasterRoute…
cl2raul66 Oct 5, 2026
834822a
feat(window): unify public events with OnX methods (Behavior-first) —…
cl2raul66 Oct 7, 2026
2004a01
refactor(window): add ArgumentNullException guards to OnX methods (re…
cl2raul66 Oct 7, 2026
0652f0c
fix(#54): address hallazgos to close issue
cl2raul66 Oct 7, 2026
c6c2bab
feat(router): centralize hwnd cleanup in MasterRouter.CleanupHwnd — #51
cl2raul66 Oct 8, 2026
420732d
refactor(registry): entity store — single table + derived indices (B-…
cl2raul66 Oct 8, 2026
cec6dc7
fix(lifecycle): skip Behavior callbacks on app teardown — #55
cl2raul66 Oct 8, 2026
90c4a6e
test
cl2raul66 Oct 8, 2026
2c67b20
feat(window): Phase A-D — complete IWindow/WindowImpl (Snapshot) — #47
cl2raul66 Oct 9, 2026
981fed9
feat(window): complete Phase C #47 + taskbar-aware FullScreen #56 + K…
cl2raul66 Oct 11, 2026
203882d
fix(window): complete Phase D — WM_NCACTIVATE for TitleBarMode.Simple…
cl2raul66 Oct 11, 2026
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
884 changes: 444 additions & 440 deletions .gitignore

Large diffs are not rendered by default.

51 changes: 45 additions & 6 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

23 changes: 5 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
Crear una aplicación nativa nunca fue tan limpio. Todo se divide en 4 bloques lógicos: `Resources`, `Layout`, `Behavior` e `Initialize`.

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

int counter = 0;

WindowsApplication
Expand All @@ -29,25 +25,22 @@ WindowsApplication
.Window(iw => iw
.Name("MainWindow")
.Title("Mi App")
.Dimensions(800, 600)
.Position(WindowPositionOnScreen.Center)
.Content(c => c
.Children(ch => ch
.View<IButton>(btn => btn
.Name("BtnGuardar")
.Text("Contador: 0")
.Name("BtnIncrease")
.Text("Value: 0")
)
)
)
)
)
.Behavior(bh =>
{
var btn = bh.BtnGuardar; // tipado por Source Generator (View<T> wrapper)
btn.OnClick(() =>
btn.BtnIncrease.OnInputWithSpatialPositio(() =>
{
counter++;
btn.Text = $"Contador: {counter}";
btn.Text = $"Value: {counter}";
});
})
.Initialize();
Expand Down Expand Up @@ -137,7 +130,6 @@ WindowsApplication
.Resources(rd => rd
.Setting(s => s
.UseWinApi()
.AppConfigFile(c => c.Path("./appsettings.json").WithAutoSave())
)
.Services(s => s.Singleton<IMyDatabase, SqlDatabase>())
)
Expand All @@ -153,17 +145,12 @@ WindowsApplication
EasyWindowsApplication/src/
├── EasyWindowsApplication/ # Framework principal
│ ├── Core/ # Fases Fluent API, Resources, Behavior, MasterRouter (100% internal)
│ │ ├── Constants.cs / Entities.cs / Win32.cs # Interop Win32 central ([LibraryImport], MSG/POINT/WM/WS/…)
│ │ ├── Procedures.cs # RunMessageLoop (GetMessageW/Translate/Dispatch)
│ │ ├── UiDefaults/ # IDefaultUiValues + UiDefaultsProvider + FontSpec (DPI scaling)
│ │ ├── Windowing/ # WindowImpl, AlternativeWindowImpl, Procedures.CreateMainWindow/Alternative, Win32/Entities/Enums, UserControl
│ │ └── LayoutEngine/ # ILayoutBuilder, Grid/Stack/Dock layouts, ViewBuilder, ContentBuilder, ILayoutStrategy
│ ├── Common/ # ControlActivatorRegistry + IControlActivator + INativeHandleFactory + Win32Helpers (HIWORD/LOWORD)
│ ├── Share/ # API pública usuario (IBaseWindow→IWindow/IAlternativeWindow/IView, IWindowConfig, IChildrenBuilder con 3 overloads View<T>, View<T> sealed class + ViewBase<TSelf>, Color/Thickness/LayoutLength/GridDefinitions)
│ │ └── Infrastructure/ # Técnicos [EditorBrowsable(Never)]: ControlAccess (único tipo aquí)
│ ├── Win32ControlsModule/ # Frontend: IButton/ILabel/… (30 interfaces) + Backend: Button/Label→Core.ControlBase + Win32UiDefaults/Win32NativeHandleFactory/ControlProcedures (GDI/User32)
│ └── WindowsApplication.cs # Punto de entrada Fluent API (devuelve Share/IApplicationLayoutPhase, static ctor → EnsureInitialized)
├── EasyWindowsApplication.Generators/ # Source Generators (EasyBehaviorGenerator EAWIN001/EAWIN002 + ControlActivatorGenerator)
├── Sample/
└── ProjectTemplates/
├── EasyWinApp/ # Plantilla `dotnet new easywinapp`
└── SimpleEasyWinApp/ # Plantilla `dotnet new simpleeasywinapp`
Expand Down
106 changes: 106 additions & 0 deletions src/EasyWindowsApplication.Generators/CulturesGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Microsoft.CodeAnalysis;
using System.Text;

namespace EasyWindowsApplication.Generators;

[Generator(LanguageNames.CSharp)]
public sealed class CulturesGenerator : IIncrementalGenerator
{
private const string LibraryAssemblyName = "EasyWindowsApplication";

private static readonly string[,] Cultures = new[,]
{
{ "En", "en" },
{ "EnUS", "en-US" },
{ "EnGB", "en-GB" },
{ "EnCA", "en-CA" },
{ "EnAU", "en-AU" },
{ "EnIN", "en-IN" },
{ "Es", "es" },
{ "EsES", "es-ES" },
{ "EsMX", "es-MX" },
{ "EsAR", "es-AR" },
{ "EsCO", "es-CO" },
{ "EsCL", "es-CL" },
{ "Fr", "fr" },
{ "FrFR", "fr-FR" },
{ "FrCA", "fr-CA" },
{ "De", "de" },
{ "DeDE", "de-DE" },
{ "DeAT", "de-AT" },
{ "DeCH", "de-CH" },
{ "It", "it" },
{ "ItIT", "it-IT" },
{ "ItCH", "it-CH" },
{ "Pt", "pt" },
{ "PtBR", "pt-BR" },
{ "PtPT", "pt-PT" },
{ "Nl", "nl" },
{ "NlNL", "nl-NL" },
{ "NlBE", "nl-BE" },
{ "Pl", "pl" },
{ "PlPL", "pl-PL" },
{ "Ru", "ru" },
{ "RuRU", "ru-RU" },
{ "UkUA", "uk-UA" },
{ "TrTR", "tr-TR" },
{ "SvSE", "sv-SE" },
{ "NbNO", "nb-NO" },
{ "DaDK", "da-DK" },
{ "FiFI", "fi-FI" },
{ "CsCZ", "cs-CZ" },
{ "HuHU", "hu-HU" },
{ "ElGR", "el-GR" },
{ "HeIL", "he-IL" },
{ "ThTH", "th-TH" },
{ "ViVN", "vi-VN" },
{ "IdID", "id-ID" },
{ "MsMY", "ms-MY" },
{ "RoRO", "ro-RO" },
{ "BgBG", "bg-BG" },
{ "HrHR", "hr-HR" },
{ "SkSK", "sk-SK" },
{ "CaES", "ca-ES" },
{ "Zh", "zh" },
{ "ZhCN", "zh-CN" },
{ "ZhTW", "zh-TW" },
{ "ZhHK", "zh-HK" },
{ "Ja", "ja" },
{ "JaJP", "ja-JP" },
{ "KoKR", "ko-KR" },
{ "HiIN", "hi-IN" },
{ "Ar", "ar" },
{ "ArSA", "ar-SA" },
{ "ArEG", "ar-EG" },
};

public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(
context.CompilationProvider.Select(static (compilation, _) =>
compilation.AssemblyName == LibraryAssemblyName),
static (spc, isLibrary) =>
{
if (isLibrary)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using System.Globalization;");
sb.AppendLine();
sb.AppendLine("namespace EasyWindowsApplication.Share;");
sb.AppendLine();
sb.AppendLine("public static class Cultures");
sb.AppendLine("{");
for (int i = 0; i < Cultures.GetLength(0); i++)
{
sb.AppendLine($" public static readonly CultureInfo CultureInfo{Cultures[i, 0]} = CultureInfo.GetCultureInfo(\"{Cultures[i, 1]}\");");
}
sb.AppendLine("}");
spc.AddSource("Cultures.g.cs", sb.ToString());
}

spc.AddSource("CulturesUsings.g.cs",
"// <auto-generated />\nglobal using static EasyWindowsApplication.Share.Cultures;\n");
});
}
}
115 changes: 85 additions & 30 deletions src/EasyWindowsApplication.Generators/EasyBehaviorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
sb.AppendLine($" public {item.TypeSymbol} {propName}");
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.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");");
}
var lookup = item.IsWindowType
? $"GetWindow<{item.TypeSymbol}>(\"{item.Name}\")"
: item.IsSurfaceType
? $"GetSurface<{item.TypeSymbol}>(\"{item.Name}\")"
: $"Get<{item.TypeSymbol}>(\"{item.Name}\")";
sb.AppendLine($" public {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .{lookup};");
sb.AppendLine();
}

Expand Down Expand Up @@ -147,18 +143,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (!usesWinApi && !item.IsWindowType) continue;
var propName = SanitizeName(item.Name);
if (item.IsWindowType)
{
sb.AppendLine($" internal static {item.TypeSymbol} {propName}");
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.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .Get<{item.TypeSymbol}>(\"{item.Name}\");");
}
var lookup = item.IsWindowType
? $"GetWindow<{item.TypeSymbol}>(\"{item.Name}\")"
: item.IsSurfaceType
? $"GetSurface<{item.TypeSymbol}>(\"{item.Name}\")"
: $"Get<{item.TypeSymbol}>(\"{item.Name}\")";
sb.AppendLine($" internal static {item.TypeSymbol} {propName}");
sb.AppendLine($" => global::EasyWindowsApplication.Share.Infrastructure.ControlAccess");
sb.AppendLine($" .{lookup};");
sb.AppendLine();
}

Expand Down Expand Up @@ -222,28 +214,90 @@ private static bool IsNameCandidate(SyntaxNode node)
{
var typeArg = generic.TypeArgumentList.Arguments[0];
var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg).Type;
if (typeSymbol == null) return null;
if (typeSymbol is null) return null;

return new NamedInfo(
name,
typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
isWindowType: false,
isSurfaceType: IsSurfaceType(context, typeSymbol),
location);
}

if (parentMa.Name.Identifier.Text == "Window")
{
return new NamedInfo(name, "global::EasyWindowsApplication.Share.IWindow", isWindowType: true, location);
return new NamedInfo(name, "global::EasyWindowsApplication.Share.IWindow", isWindowType: true, isSurfaceType: false, location);
}

if (parentMa.Name is GenericNameSyntax altGeneric
&& altGeneric.Identifier.Text == "AlternativeWindow"
&& altGeneric.TypeArgumentList.Arguments.Count == 1)
{
var typeArg = altGeneric.TypeArgumentList.Arguments[0];
var typeSymbol = context.SemanticModel.GetTypeInfo(typeArg).Type;
if (typeSymbol is null) return null;

var compilation = context.SemanticModel.Compilation;
var iBaseWindow = compilation.GetTypeByMetadataName(
"EasyWindowsApplication.Share.IBaseWindow");
if (iBaseWindow is not null && Implements(typeSymbol, iBaseWindow))
{
return new NamedInfo(
name,
typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
isWindowType: true,
isSurfaceType: false,
location);
}
if (IsSurfaceType(context, typeSymbol))
{
return new NamedInfo(
name,
typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
isWindowType: false,
isSurfaceType: true,
location);
}
return null;
}

if (parentMa.Name.Identifier.Text == "AlternativeWindow")
{
return new NamedInfo(name, "global::EasyWindowsApplication.Share.IAlternativeWindow", isWindowType: true, location);
return new NamedInfo(name, "global::EasyWindowsApplication.Share.IAlternativeWindow", isWindowType: true, isSurfaceType: false, location);
}

return null;
}

private static bool IsSurfaceType(GeneratorSyntaxContext context, ITypeSymbol typeSymbol)
{
var iControl = context.SemanticModel.Compilation.GetTypeByMetadataName(
"EasyWindowsApplication.Win32ControlsModule.Frontend.IControl");
if (iControl is not null && Implements(typeSymbol, iControl))
return false;

var iViewSurface = context.SemanticModel.Compilation.GetTypeByMetadataName(
"EasyWindowsApplication.Share.IViewSurface");
if (iViewSurface is null)
return false;

return Implements(typeSymbol, iViewSurface);
}

private static bool Implements(ITypeSymbol typeSymbol, ITypeSymbol iface)
{
if (typeSymbol.Equals(iface, SymbolEqualityComparer.Default))
return true;

foreach (var implemented in typeSymbol.AllInterfaces)
{
if (implemented.Equals(iface, SymbolEqualityComparer.Default))
return true;
}

return false;
}

private static bool IsBehaviorCandidate(SyntaxNode node)
{
if (node is not InvocationExpressionSyntax inv)
Expand Down Expand Up @@ -386,18 +440,19 @@ private static string SanitizeName(string name)
return sb.ToString();
}

private readonly struct NamedInfo(string name, string typeSymbol, bool isWindowType, Location location)
private readonly struct NamedInfo(string name, string typeSymbol, bool isWindowType, bool isSurfaceType, Location location)
{
public readonly string Name = name;
public readonly string TypeSymbol = typeSymbol;
public readonly bool IsWindowType = isWindowType;
public readonly bool IsSurfaceType = isSurfaceType;
public readonly Location Location = location;

public override bool Equals(object obj) =>
obj is NamedInfo other && Name == other.Name && TypeSymbol == other.TypeSymbol && IsWindowType == other.IsWindowType;
obj is NamedInfo other && Name == other.Name && TypeSymbol == other.TypeSymbol && IsWindowType == other.IsWindowType && IsSurfaceType == other.IsSurfaceType;

public override int GetHashCode() =>
(Name?.GetHashCode() ?? 0) ^ (TypeSymbol?.GetHashCode() ?? 0) ^ IsWindowType.GetHashCode();
(Name?.GetHashCode() ?? 0) ^ (TypeSymbol?.GetHashCode() ?? 0) ^ IsWindowType.GetHashCode() ^ IsSurfaceType.GetHashCode();
}

private sealed class EnclosingTypeInfo(string ns, string className)
Expand Down
Loading
Loading