1 module modernui.ui.control; 2 3 import modernui.core; 4 import modernui.rx; 5 import modernui.collections; 6 import modernui.ui.core; 7 import modernui.ui.render; 8 9 import std.algorithm.iteration; 10 import std.container.array; 11 12 abstract class Visual : DependencyObject 13 { 14 static this() { registerClass!(typeof(this)); } 15 this() 16 { 17 myVisualChildren = new ReactiveList!Visual; 18 myInvalidMeasure = new Subject!None; 19 myInvalidArrangement = new Subject!None; 20 visualChildren.itemsAdded.then!(ItemsAdded!Visual)(&onVisualChildrenAdded); 21 visualChildren.itemsRemoved.then!(ItemsRemoved!Visual)(&onVisualChildrenRemoved); 22 } 23 24 mixin DefineDependencyPropertyReadOnly!(Visual, "visualParent"); 25 mixin DefineDependencyPropertyReadOnly!(Size, "desiredSize"); 26 mixin DefineDependencyPropertyReadOnly!(Rect, "actualRect"); 27 28 private ReactiveList!Visual myVisualChildren; 29 protected @property ReactiveList!Visual visualChildren() { return myVisualChildren; } 30 31 private Subject!None myInvalidMeasure; 32 @property Observable!None invalidMeasure() { return myInvalidMeasure; } 33 34 private Subject!None myInvalidArrangement; 35 @property Observable!None invalidArrangement() { return myInvalidArrangement; } 36 37 protected void invalidateMeasurement() { myInvalidMeasure.next(None.val); } 38 protected void invalidateArrangement() { myInvalidArrangement.next(None.val); } 39 40 protected abstract Size measure(const Size measure); 41 42 protected abstract void arrange(const Rect site); 43 44 protected abstract void render(RenderContext context); 45 46 private void onVisualChildrenAdded(ItemsAdded!Visual info) 47 { 48 foreach(newItem; info.newItems) 49 { 50 newItem.visualParent = this; 51 } 52 53 invalidateMeasurement(); 54 } 55 56 private void onVisualChildrenRemoved(ItemsRemoved!Visual info) 57 { 58 foreach(oldItem; info.oldItems) 59 { 60 oldItem.visualParent = null; 61 } 62 63 invalidateMeasurement(); 64 } 65 } 66 67 abstract class UIElement : Visual 68 { 69 static this() { registerClass!(typeof(this)); } 70 71 mixin DefineDependencyProperty!(Thickness, "border"); 72 mixin DefineDependencyProperty!(Thickness, "padding"); 73 mixin DefineDependencyProperty!(Thickness, "margin"); 74 mixin DefineDependencyProperty!(double, "width"); 75 mixin DefineDependencyProperty!(double, "height"); 76 mixin DefineDependencyProperty!(HorizontalAlignment, "horizontalAlignment"); 77 mixin DefineDependencyProperty!(VerticalAlignment, "verticalAlignment"); 78 mixin DefineDependencyProperty!(bool, "isVisible"); 79 mixin DefineDependencyProperty!(bool, "isCollapsible"); 80 81 // assumes children already measured, no recursive impl. 82 protected override Size measure(const Size sizeAvailable) 83 { 84 import std.math : isNaN; 85 auto size = Size.init; 86 if(isCollapsible && !isVisible) 87 { 88 desiredSize = size; 89 return size; 90 } 91 92 if(isNaN(width) || isNaN(height)) 93 { 94 foreach(v; this.visualChildren[]) 95 { 96 size = size.unionWith(v.desiredSize); 97 } 98 } 99 100 immutable auto r = [margin, border, padding]; 101 if(isNaN(width)) 102 { 103 size.width = width; 104 } else { 105 size.width += r.map!(x => x.widthContribution).sum; 106 } 107 108 if(isNaN(height)) 109 { 110 size.height = height; 111 } else { 112 size.height += r.map!(x => x.heightContribution).sum; 113 } 114 115 desiredSize = size; 116 return size; 117 } 118 119 // actualRect is relative to parent. 120 protected override void arrange(const Rect site) 121 { 122 // skips unuseful recursion 123 if(site == actualRect) return; 124 125 actualRect = site; 126 immutable auto r = [margin, border, padding]; 127 auto left = r.map!(x => x.left).sum; 128 auto right = r.map!(x => x.right).sum; 129 auto top = r.map!(x => x.top).sum; 130 auto bottom = r.map!(x => x.bottom).sum; 131 132 auto location = Point(left, top); 133 auto size = site.size.shrink(Size(left + right, top + bottom)); 134 auto childrenSite = Rect(location, size); 135 foreach(v; visualChildren[]) 136 { 137 v.arrange(childrenSite); 138 } 139 } 140 } 141 142 class TextElement : UIElement 143 { 144 static this() { registerClass!(typeof(this)); } 145 146 mixin DefineDependencyProperty!(string, "text"); 147 148 protected override void render(RenderContext context) 149 { 150 // TODO 151 } 152 153 this() 154 { 155 propertyChanged.then!PropertyChange((x) { 156 if(x.name != nameof!text) return; 157 invalidateMeasurement(); 158 }); 159 } 160 } 161 162 abstract class ContentControl : UIElement 163 { 164 static this() { registerClass!(typeof(this)); } 165 166 mixin DefineDependencyProperty!(Visual, "content"); 167 168 protected void changeChild(Visual child) 169 { 170 if(visualChildren.length == 0) 171 { 172 visualChildren.add(child); 173 } 174 else 175 { 176 visualChildren[0] = child; 177 } 178 179 invalidateMeasurement(); 180 } 181 182 this() 183 { 184 propertyChanged.then!PropertyChange((x) { 185 if(x.name != nameof!content) return; 186 changeChild(content); 187 }); 188 } 189 } 190 191 version(Windows) 192 { 193 import core.sys.windows.windows; 194 195 version(Unicode) 196 { 197 import std.utf : toUTF16z; 198 private alias toTStringz = toUTF16z; 199 } 200 else 201 { 202 import std.string : toStringz; 203 private alias toTStringz = toStringz; 204 } 205 206 class Window : ContentControl 207 { 208 static this() { registerClass!(typeof(this)); } 209 210 static bool isClassRegistered = false; 211 212 static immutable string className = "ModernUIWindow"; 213 214 private struct DescendantInfo { 215 int level; 216 Array!Subscription subscriptions; 217 } 218 219 private DescendantInfo[Visual] descendants; 220 private Window selfReference; 221 private RenderContext myRenderContext; 222 223 private HINSTANCE hInstance = null; 224 private HWND hWnd = null; 225 226 private void registerWindowClass() { 227 HWND hWnd; 228 MSG msg; 229 WNDCLASS wndclass; 230 231 wndclass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; 232 wndclass.lpfnWndProc = &WindowProc; 233 wndclass.cbClsExtra = 0; 234 wndclass.cbWndExtra = 0; 235 wndclass.hInstance = hInstance; 236 wndclass.hIcon = LoadIcon(null, IDI_APPLICATION); 237 wndclass.hCursor = LoadCursor(null, IDC_ARROW); 238 wndclass.hbrBackground = cast(HBRUSH)GetStockObject(WHITE_BRUSH); 239 wndclass.lpszMenuName = null; 240 wndclass.lpszClassName = className.toTStringz; 241 242 if (!RegisterClass(&wndclass)) 243 { 244 throw new Error("Error registering the window class"); 245 } 246 } 247 248 private void createWindow() 249 { 250 Window* wnd = &selfReference; 251 hWnd = CreateWindowEx( 252 0, 253 className.toTStringz, // window class name 254 "test".toTStringz, // window caption 255 WS_THICKFRAME | 256 WS_MAXIMIZEBOX | 257 WS_MINIMIZEBOX | 258 WS_SYSMENU | 259 WS_VISIBLE, // window style 260 CW_USEDEFAULT, // initial x position 261 CW_USEDEFAULT, // initial y position 262 600, // initial x size 263 400, // initial y size 264 HWND_DESKTOP, // parent window handle 265 null, // window menu handle 266 hInstance, // program instance handle 267 wnd); // creation parameters; 268 269 if(hWnd is null) 270 { 271 throw new Error("Could not create window"); 272 } 273 } 274 275 this() 276 { 277 selfReference = this; 278 hInstance = GetModuleHandle(null); 279 if(!isClassRegistered) 280 { 281 registerWindowClass(); 282 isClassRegistered = true; 283 } 284 285 createWindow(); 286 287 auto desc = DescendantInfo.init; 288 desc.level = 0; 289 descendants[this] = desc; 290 291 visualChildren.itemsAdded.then!(ItemsAdded!Visual)(&onDescendantsAdded); 292 visualChildren.itemsRemoved.then!(ItemsRemoved!Visual)(&onDescendantsRemoved); 293 } 294 295 // For new descendants added, ensure its children are tracked too 296 void onDescendantsAdded(ItemsAdded!Visual x) 297 { 298 foreach(newItem; x.newItems) 299 { 300 auto desc = DescendantInfo.init; 301 auto subscription = newItem.visualChildren.itemsAdded.then!(ItemsAdded!Visual)(&onDescendantsAdded); 302 desc.subscriptions.insertBack(subscription); 303 subscription = newItem.visualChildren.itemsRemoved.then!(ItemsRemoved!Visual)(&onDescendantsRemoved); 304 desc.subscriptions.insertBack(subscription); 305 desc.level = descendants[newItem.visualParent].level + 1; 306 descendants[newItem] = desc; 307 } 308 } 309 310 void onDescendantsRemoved(ItemsRemoved!Visual x) 311 { 312 foreach(oldItem; x.oldItems) 313 { 314 auto desc = descendants[oldItem]; 315 foreach(subscription; desc.subscriptions) 316 { 317 subscription.release; 318 } 319 descendants.remove(oldItem); 320 } 321 } 322 323 void show() 324 { 325 ShowWindow(hWnd, SW_SHOWDEFAULT); 326 UpdateWindow(hWnd); 327 } 328 329 void messageLoop() 330 { 331 MSG msg; 332 while (GetMessageA(&msg, null, 0, 0)) 333 { 334 TranslateMessage(&msg); 335 DispatchMessageA(&msg); 336 } 337 } 338 339 protected override void render(RenderContext rc) 340 { 341 rc.clear(Color(1.0,1.0,1.0)); 342 foreach(child; visualChildren) child.render(rc); 343 } 344 345 extern(Windows) 346 static LRESULT WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) nothrow 347 { 348 try 349 { 350 if(message == WM_NCCREATE) 351 { 352 auto pCreate = cast(CREATESTRUCT*)lParam; 353 auto wnd = cast(Window*)pCreate.lpCreateParams; 354 SetWindowLongPtr(hWnd, GWLP_USERDATA, cast(size_t)wnd); 355 wnd.myRenderContext = new modernui.ui.direct2d.Direct2DRenderContext(hWnd); 356 return true; 357 } 358 359 auto self = cast(Window*)GetWindowLongPtr(hWnd, GWLP_USERDATA); 360 switch (message) 361 { 362 case WM_PAINT: 363 self.myRenderContext.begin; 364 scope(exit) self.myRenderContext.end; 365 self.render(self.myRenderContext); 366 break; 367 368 case WM_DESTROY: 369 PostQuitMessage(0); 370 break; 371 372 case WM_SIZE: 373 self.myRenderContext.resize; 374 break; 375 376 default: 377 return DefWindowProcA(hWnd, message, wParam, lParam); 378 } 379 } 380 catch(Throwable t) 381 { 382 PostQuitMessage(240); 383 } 384 385 return 0; 386 } 387 } 388 }