├── .github └── workflows │ ├── build-macos.yml │ ├── build-ubuntu1804.yml │ └── build-ubuntu2004.yml ├── .gitignore ├── Docs ├── Assets │ ├── WidgetLifecycle.drawio │ └── WidgetLifecycle.svg ├── ComposingWidgets.md ├── CreatingLeafWidgets.md ├── GlobalAppState.md ├── Readme.md ├── WidgetClassOverview.md ├── WidgetState.md ├── WidgetStyling.md ├── demo.png └── minimal_demo.png ├── LICENSE.txt ├── Package.resolved ├── Package.swift ├── Readme.md ├── Sources ├── Application │ └── Application.swift ├── ApplicationBackendSDL2 │ ├── ApplicationBackendSDL2.swift │ ├── Key+SDL2.swift │ ├── MouseButton+SDL2.swift │ ├── SDL2BaseWindow.swift │ └── SDL2Window.swift ├── DevApp │ ├── MainView.swift │ ├── TestWidget.swift │ └── main.swift ├── Drawing │ ├── DrawingBackend.swift │ ├── DrawingContext.swift │ ├── Image.swift │ ├── MockDrawingBackend.swift │ ├── Paint.swift │ └── TextPaint.swift ├── DrawingVulkan │ └── VulkanDrawingSurface.swift ├── Events │ ├── EventHandlerManager.swift │ ├── EventfulObject.swift │ └── PublishingEventManager.swift ├── MinimalDemo │ ├── MainView.swift │ └── main.swift ├── TaskOrganizerDemo │ ├── Data │ │ ├── MainViewRoute.swift │ │ ├── TodoItem.swift │ │ ├── TodoList.swift │ │ ├── TodoSearchResult.swift │ │ ├── TodoSearcher.swift │ │ └── TodoStore.swift │ ├── UI │ │ ├── AppTheme.swift │ │ ├── SearchResultsView.swift │ │ ├── TaskCompletionButton.swift │ │ ├── TodoAppView.swift │ │ ├── TodoListItemView.swift │ │ └── TodoListView.swift │ └── main.swift ├── VertexGUI │ └── lib.swift └── WidgetGUI │ ├── Base │ ├── Child.swift │ ├── Parent.swift │ ├── PseudoElement.swift │ ├── Root.swift │ ├── Widget │ │ ├── Lifecycle │ │ │ ├── Widget+LifecycleFlag.swift │ │ │ ├── Widget+LifecycleManager.swift │ │ │ ├── Widget+LifecycleMethod.swift │ │ │ ├── Widget+LifecycleMethodInvocationAbortionReason.swift │ │ │ ├── Widget+LifecycleMethodInvocationQueue.swift │ │ │ ├── Widget+LifecycleMethodInvocationReason.swift │ │ │ ├── Widget+LifecycleMethodInvocationSignal.swift │ │ │ └── Widget+LifecycleMethodInvocationSignalGroup.swift │ │ ├── Signaling │ │ │ ├── Signal.swift │ │ │ └── Widget+signaling.swift │ │ ├── Widget+CallCounter.swift │ │ ├── Widget+Debugging.swift │ │ ├── Widget+PostInitConfigurableWidget.swift │ │ ├── Widget+ScrollBar.swift │ │ ├── Widget+TreeOperations.swift │ │ ├── Widget+inputEventHandlerRegistration.swift │ │ ├── Widget+invalidateRootSizeDependentThings.swift │ │ ├── Widget+otherEventHandlerRegistration.swift │ │ ├── Widget+resolveWidgetDimensionSize.swift │ │ ├── Widget+with.swift │ │ └── Widget.swift │ ├── WidgetBus.swift │ ├── WidgetContext.swift │ └── WidgetTreeManager.swift │ ├── Composition │ ├── ComposeBuilder.swift │ ├── ComposedContent.swift │ ├── ComposedWidget.swift │ ├── Content.swift │ ├── DirectContent.swift │ ├── DirectContentBuilder.swift │ ├── Dynamic.swift │ ├── Slot.swift │ ├── SlotAcceptingWidgetProtocol.swift │ ├── SlotContent.swift │ ├── SlotContentBuilder.swift │ ├── SlotContentDefinition.swift │ └── SlotContentManager.swift │ ├── ContextMenu │ ├── ContextMenu+Item.swift │ └── ContextMenu.swift │ ├── CumulatedValues │ ├── CumulatedValuesProcessor.swift │ └── Widget+invalidateCumulatedValues.swift │ ├── DeveloperTools │ ├── DeveloperTools+InspectorView.swift │ ├── DeveloperTools+MainRoute.swift │ ├── DeveloperTools+MainView.swift │ ├── DeveloperTools+MessagesView.swift │ ├── DeveloperTools+PerformanceView.swift │ ├── DeveloperTools+Store.swift │ ├── DeveloperTools+Theme.swift │ ├── DeveloperTools+WidgetNestingView.swift │ ├── DeveloperTools.swift │ └── WidgetPropertiesView.swift │ ├── Drawing │ ├── DrawingManager.swift │ ├── LeafWidget.swift │ └── SkiaKitExtensions │ │ ├── Canvas.swift │ │ ├── Color.swift │ │ ├── Font.swift │ │ ├── Image+initFromSwimImage.swift │ │ ├── Paint.swift │ │ ├── Path.swift │ │ ├── Point.swift │ │ └── Rect.swift │ ├── Focus │ ├── FocusManager.swift │ └── Widget+Focusable.swift │ ├── Helpers │ ├── BurstLimiter.swift │ ├── DefinitiveDict.swift │ ├── Logger.swift │ ├── Reference.swift │ ├── Tree │ │ ├── Mirror+allChildren.swift │ │ ├── Tree.swift │ │ ├── TreeMask.swift │ │ ├── TreePath.swift │ │ └── TreeRange.swift │ ├── WeakBox.swift │ ├── WidgetBuilder.swift │ └── WidgetEventHandlerManager.swift │ ├── Input │ ├── Cursor.swift │ ├── Key.swift │ ├── KeyStatesContainer.swift │ ├── MouseButton.swift │ ├── RawEvents │ │ ├── RawKeyboardEvent.swift │ │ ├── RawMouseEvent.swift │ │ └── RawTextInputEvent.swift │ ├── Root+KeyboardEventManager.swift │ ├── Root+TextInputEventManager.swift │ ├── Widget+internalProcessInputEvents.swift │ ├── WidgetEvents │ │ ├── GUIKeyboardEvent.swift │ │ ├── GUIMouseEvent.swift │ │ └── GUITextEvent.swift │ └── WidgetTreeMouseEventManager.swift │ ├── Layouts │ ├── AbsoluteLayout.swift │ ├── FlexLayout.swift │ ├── Layout.swift │ ├── LayoutProperty.swift │ └── SimpleLinearLayout.swift │ ├── Platform │ ├── Screen.swift │ └── Window.swift │ ├── Primitives │ ├── Axis.swift │ ├── BorderWidth.swift │ ├── Bounded.swift │ ├── BoxConstraints.swift │ ├── CornerRadii.swift │ ├── Insets.swift │ ├── Margins.swift │ ├── Overflow.swift │ ├── TextTransform.swift │ ├── Tick.swift │ ├── Visibility.swift │ └── WidgetDimensionSize.swift │ ├── Resources │ ├── LICENSE.txt │ └── materialdesignicons-webfont.ttf │ ├── State │ ├── Dependencies │ │ ├── Dependency.swift │ │ ├── DependencyManager.swift │ │ ├── Inject.swift │ │ └── Widget+provideDependencies.swift │ ├── ImmutableBinding.swift │ ├── MutableBinding.swift │ ├── MutableReactiveProperty.swift │ ├── ObservableDictionary.swift │ ├── PropertyPublisher.swift │ ├── ReactiveProperty.swift │ ├── ReactivePropertyProjection.swift │ ├── State.swift │ └── Store.swift │ ├── Style │ ├── PseudoClass.swift │ ├── Style.swift │ ├── StyleManager.swift │ ├── StyleParser.swift │ ├── StyleProperty.swift │ ├── StylePropertyValue.swift │ ├── StylePropertyValueDefinition.swift │ ├── StylePropertyValueDefinitionsBuilder.swift │ ├── StyleSelector.swift │ ├── StyleSelectorPart.swift │ ├── Theme │ │ ├── FlatTheme.swift │ │ └── Theme.swift │ ├── Widget+PseudoClass.swift │ ├── Widget+StylePropertiesContainer.swift │ ├── Widget+inStyleScope.swift │ ├── Widget+internalPseudoClassManagement.swift │ ├── Widget+invalidateMatchedStyles.swift │ ├── Widget+invalidateResolvedStyleProperties.swift │ ├── Widget+notifySelectorChanged.swift │ └── Widget+resolveStyleProperties.swift │ └── Widgets │ └── Standard │ ├── Charting │ └── BarChart.swift │ ├── Container.swift │ ├── Controls │ ├── Button.swift │ ├── Checkbox.swift │ ├── ColorPicker.swift │ ├── RadioButton.swift │ ├── Select.swift │ ├── TextInput.swift │ └── ToggleButton.swift │ ├── Drawing.swift │ ├── Icons │ ├── MaterialDesignIcon+Identifier.swift │ └── MaterialDesignIcon.swift │ ├── ImageView.swift │ ├── Layout │ ├── Flex+TwoItemStrategy.swift │ ├── Flex+UniversalStrategy.swift │ └── Flex.swift │ ├── List.swift │ ├── Space.swift │ └── Text.swift ├── Tests ├── LinuxMain.swift ├── ReactivePropertiesTests │ ├── BiDirectionalPropertyBindingTests.swift │ ├── ComputedPropertyTests.swift │ ├── DependencyRecorderTests.swift │ ├── IsolationThread.swift │ ├── MutableComputedPropertyTests.swift │ ├── MutablePropertyTests.swift │ ├── ObservablePropertyTests.swift │ ├── StaticPropertyTests.swift │ ├── UniDirectionalPropertyBindingTests.swift │ └── XCTestManifests.swift ├── VisualAppBaseTests │ ├── EventHandlerManagerTests.swift │ ├── RenderObjectTreeRendererTests.swift │ ├── RenderObjectTreeTests.swift │ ├── TreeTests.swift │ └── XCTestManifests.swift └── WidgetGUITests │ ├── Experimental │ └── BuildTests.swift │ ├── Layout │ ├── ContainerTests.swift │ ├── LayoutTest.swift │ └── LayoutTestUtils.swift │ ├── MockContainerWidget.swift │ ├── MockLeafWidget.swift │ ├── MockRoot.swift │ ├── ReactivePropertyTests.swift │ ├── StylableWidgetTests.swift │ ├── Styles │ ├── StyleManagerTests.swift │ ├── StyleParserTests.swift │ ├── StylePropertiesResolverTests.swift │ ├── StylePropertyMergingTests.swift │ ├── StylePropertySupportDefinitionsTests.swift │ ├── StylePropertyTests.swift │ ├── StyleSelectorTests.swift │ ├── StyleTests.swift │ ├── WidgetStyleApiTests.swift │ ├── WidgetStyleScopeApplicationTests.swift │ └── WidgetTreeStyleTests.swift │ └── XCTestManifests.swift └── compiler-errors.md /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | name: build MacOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-macos: 11 | 12 | runs-on: macos-11 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: install_dependencies 17 | run: brew install sdl2 18 | - name: Build 19 | run: FRB_ENABLE_GRAPHICS_VULKAN=0 swift build --target VertexGUI 20 | -------------------------------------------------------------------------------- /.github/workflows/build-ubuntu1804.yml: -------------------------------------------------------------------------------- 1 | name: build Ubuntu 18.04 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-ubuntu1804: 11 | 12 | runs-on: ubuntu-18.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: install_dependencies 17 | run: | 18 | sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" 19 | sudo apt-get update -y -qq 20 | sudo apt-get install libsdl2-dev 21 | - name: Build 22 | run: FRB_ENABLE_GRAPHICS_VULKAN=0 swift build --target VertexGUI -------------------------------------------------------------------------------- /.github/workflows/build-ubuntu2004.yml: -------------------------------------------------------------------------------- 1 | name: build Ubuntu 20.04 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-ubuntu2004: 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: install_dependencies 17 | run: | 18 | sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" 19 | sudo apt-get update -y -qq 20 | sudo apt-get install libsdl2-dev 21 | - name: Build 22 | run: FRB_ENABLE_GRAPHICS_VULKAN=0 swift build --target VertexGUI 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .vscode 9 | .swiftpm -------------------------------------------------------------------------------- /Docs/Assets/WidgetLifecycle.drawio: -------------------------------------------------------------------------------- 1 | 7Vtdc6M2FP01ftwO4tN+3CS7bafTmU7T7iaPCshGE4yokIOdX1/JIIwkEpMEMImTySToIoR0zpF0dSVmzuV6+yuFWfwniVAys61oO3OuZrYN+C//Jyy7ymJ589KyojiqbAfDNX5EMmNl3eAI5UpGRkjCcKYaQ5KmKGSKDVJKCjXbkiTqWzO4QobhOoSJaf2JIxaX1rkdHOy/IbyK5ZuBvyjv3MHwfkXJJq3eN7Od7/uf8vYayrKqhuYxjEjRMDnfZs4lJYSVV+vtJUoEuBK28rnvT9yt601Ryro84P8Hbh5//B0kfyxuwA90WcRx9sX1y2IeYLJBsh372rKdRGjfRiRKsWbORRFjhq4zGIq7BRcFt8VsnfAU4Jdr8gDv9k+KzBTl+LGZJgyyRpqrCTXTKMLNZMV5w1LVFlGGtk/iAGp0uWwRWSNGdzxL9cDcq6itFOtU/BQH9gPglLa4wfx8UWWEleJWddEH1PlFBfxLSPB6JiGCebzPqzICNEaAxgjQGAEqI8BgBPTDCHAtT6VkbnICfM/kxHGG4mR+3pR06CSOa43ZSYA9JCM9QLaQg8ZOw6eBWY1jEzPPHmxgMSDjs1IYS9DytwHYB2a+KjN30dLx7RbM/KEgM4dimGXJbrqQeW1j5aiQmS5EAndkwyYHlR+cGqrAgCqisJgcUIF/aqDkWNpAiqT/4PDewIq3mqmA5IySe3RJEkK5JSUpz3mxxEmimWCCVylPhhwlxO0XAkPMVwZfqxtrHEXiNa0MqBwNPZl4JiFt0+9gMwkwO/kmiyBDYmUW4yTiDZucjF3r5DI2O/zbUBpaZy3zb6vPUi/fe4fM7Ph3G66vCYhrriBV97XTict070i6d1WKWPRG6yeOVoiJNH+3bS0xzUUqpIh3XBPRDzqS2uqgAFoGBdsacyw1FzJrjsAU3CVV4aBlATOyws1VOBHCRrydouLsnBwClRynxSOw28gZTsbW8bkt3NCHGgWURl9FwFYgnMA8x6FKE9pidlPBJ65vxfUvXpW62jZuXe1kIuVNuWkmGk+J5OGxfUo+V9YURUZsWOODt4ZsaIg6uEYMUjHaHun3JsENAr0W/qSNogQy/KDWt43U6g1/ESzGlDqg4+oBHU0XZTurp+xGGFkvaKEWVFdQFlTiYBTEqYe7RrZMZMifqbD2Hte1nq9XoNVLzc8vyhocBF9z8IY+AI73gc6yr6V+27hzTPYHpd8qQh9c9nZH1TunVD0AXruKXix7PvFpHUiPdD6h+96kZroN5yI1uYo6qjVw0iH2I4nNXIWNIrZTTMvupDTj6QvrV2vG1d3yoTVjLkffm2bcjpLxPiXTj2Q6bHx/0DnN6yg1/1Nq/Uitw0GXDyq1rhNh8Cm1fqRmBv2XhHJDiorZM9slEwxc7Y+iYCKK+LLo6/yVFpB1JNHNMOOoAVm5waFGZPP93lbSFmLkZeIsfwrPfqOywJXVk3LuuvGgB2b6w8tcHXC0hMLLrQWx2YAeUBXemaDGe6HFUmlx2o6uBKPK2PSm7pGockQKc7Q5rYa7nvMZTsOmPyD7/JrwWWxScHldz2MMB1eHjezxgv3gZMF+SczxUNRpHff6dIjscLoyuntTC60kfeOgp3i/WeX5CAF8p8M537PQ9ftYjzqGrL1Xytp6Ynupf1l77VPfoLJ2TY/2PGX9Pta+jq4R/7WyBvrad6DdWWAHmosy90aQtbndKp22Ikbiy7QpeW1+y0cT43ptbodtnHMYBqRuJr4XpM9unt3X7BbowbTehgFtFeyPcerCNXea9uEGQQtMowRREYHIEF0Suhb0EXovGpVGMxmimIloXIjyHO4JTvAShbswEXYZubAY4X/qU87//m50nY8TzKiDEs99gNcWzAhePnTx5OGj11ISh0+LnW//Aw== -------------------------------------------------------------------------------- /Docs/ComposingWidgets.md: -------------------------------------------------------------------------------- 1 | # Composing Widgets 2 | 3 | Widgets can be composed into groups to create more complex Widgets. 4 | 5 | *more to be added* -------------------------------------------------------------------------------- /Docs/CreatingLeafWidgets.md: -------------------------------------------------------------------------------- 1 | # Creating LeafWidgets 2 | 3 | *by example* 4 | 5 | **1\. subclass LeafWidget** 6 | 7 | ```swift 8 | import VertexGUI 9 | 10 | // when creating a core Widget you probably need to 11 | // import the specific dependencies instead of the above one 12 | import WidgetGUI // only import when not creating the Widget in the WidgetGUI target 13 | import GfxMath // defines DSize2, DVec2, ... 14 | import Drawing // defines DrawingContext, ... 15 | 16 | final class MyCustomWidget: LeafWidget { 17 | override func performLayout(constraints: BoxConstraints) -> DSize2 { 18 | // to be implemented 19 | return .zero 20 | } 21 | 22 | override func draw(_ drawingContext: DrawingContext) { 23 | // to be implemented 24 | } 25 | } 26 | ``` 27 | 28 | `performLayout` and `draw` must be provided. 29 | 30 | `performLayout` is used to determine the Widgets size. 31 | You can respect the constraints which the parent Widget will pass to your Widget via the `constraints` parameter, by calling `constraints.constrain(DSize2(yourPreferredWidth, yourPreferredHeight))`.
32 | You can also choose to ignore the constraints which might lead to your Widget overflowing the space that is available and overlapping with other Widgets. Do this by simply returning the size you want the Widget to have `return DSize2(yourPreferredWidth, yourPreferredHeight)`.
33 | The preferred size should be calculated based on the content, so that the content can fit inside the returned size without overflowing. 34 | 35 | The size your Widget will have when you choose to respect the constraints passed in depends on the available space and sizes of other Widgets. 36 | 37 | In the `draw` method you have access to the final size through the Widget's `self.layoutedSize.width` and `self.layoutedSize.height` properties. Use these values to calculate sizes of the graphics primitives you want to draw.
38 | Nothing prevents you from drawing outside the bounds of the Widget. However when the Widget's `overflow` property is set to `.cut` any pixels outside of the Widget's bounding box will not be displayed. 39 | 40 | *details* 41 | 42 | [**Overview of the Widget class**](WidgetClassOverview.md) 43 | 44 | [**DrawingContext** - documentation](https://ungast.github.io/swift-gui/generated-doc/DrawingContext/) 45 | 46 |

47 | 48 | *more to be added* 49 | -------------------------------------------------------------------------------- /Docs/GlobalAppState.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /Docs/Readme.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | The framework is designed in such a way that it can be used to create applications that are cross-platform. Therefore the framework does not rely on UI components provided by the host environment but draws custom implementations of them directly to the screen. 4 | 5 | This leads to the UI looking the same on every platform if no platform-specific design is provided. 6 | 7 | To enable the framework for a specific platform a subclass off "VisualApp" has to be created. Implementations for creating windows and receiving input events (mouse, keyboard, ...) and drawing graphics primitves (rectangle, circle, line, ...) need to be provided. 8 | 9 | [Note: the way in which platforms are added to the framework will be reworked, therefore not more information is provided here.] 10 | 11 | *more information to be added* 12 | 13 |
14 | 15 | The UI is defined as a tree of "Widgets". Every Widget is a subclass of the core Widget class. 16 | 17 | There are so called "LeafWidgets" (subclass of Widget) which define a draw method. This method is automatically called by the framework when the content is to be displayed on the screen. 18 | 19 | As the name implies, LeafWidgets are the leafs of the tree. All other Widgets do not define a draw method and instead provide functionality on top of other Widgets. Such a non-leaf Widget might for example be a TextField which provides editing functionality on top of a Text Widget (which is a LeafWidget). 20 | 21 | [Note: right now it is not completely true that only LeafWidgets are drawn to the screen. Every Widget can for example have a background color which is automatically applied to fill the bounding box of any Widget. However this is going to change in future versions so that the background is implemented as a child Widget as well. (e.g. a Rectangle Widget with a certain size and color is added as the first child to every Widget which has a Background)] 22 | 23 | *details:* 24 | 25 | [**overview of the Widget class**](WidgetClassOverview.md) 26 | 27 | [**LeafWidgets** - custom drawing logic, how to create them](CreatingLeafWidgets.md) 28 | 29 | [**composing Widgets**](ComposingWidgets.md) 30 | 31 |
32 | 33 | Widgets can be styled in a CSS like manner. You can create your own Widgets by composing core Widgets or creating a new subclass of LeadWidget and define a custom draw method. 34 | 35 | *details:* [**styling Widgets**](WidgetStyling.md) 36 | 37 |
38 | 39 | State can be shared between Widgets directly by passing "Bindings" to child Widgets. There are mutable and immutable Bindings. 40 | Additionally dependencies can be provided by any parent Widget and can be injected into any child (no matter how deep it is nested) on demand. 41 | 42 | *details*: [**managing Widget state**](WidgetState.md) 43 | 44 |
45 | 46 | The approach to handle the global state of the app can be freely chosen. Currently it is recommended to use "Stores" which define a current state as well as mutations and actions to update the state in a transparent manner. They are in concept very similar to [Vuex](https://vuex.vuejs.org/) Stores. 47 | 48 | *details:* [**managing global app state with Stores**](GlobalAppState.md) -------------------------------------------------------------------------------- /Docs/WidgetClassOverview.md: -------------------------------------------------------------------------------- 1 | *API* 2 | 3 | [**Widget** - generated documentation](https://ungast.github.io/swift-gui/generated-doc/Widget) 4 | 5 |
6 | 7 | # Lifecycle 8 | 9 | Every Widget goes through a chain of lifecycle events. 10 | 11 | 12 | 13 | *more to be to this section* 14 | 15 |
16 | 17 | # User Generated Events 18 | 19 | Every class inheriting from Widget (or LeafWidget, ComposedWidget, because they are subclasses of Widget) allows for registering handlers for a set of user generated events. 20 | 21 | These are: 22 | - onMouseEnter 23 | - onMouseMove 24 | - onMouseLeave 25 | - onClick 26 | - onMouseDown 27 | - onMouseUp 28 | - onMouseWheel 29 | - onKeyDown 30 | - onKeyUp 31 | - onTextInput 32 | 33 |
34 | 35 | To register event handlers on a Widget instance from the outside you can use: `widgetInstance.onSomeEventName(yourHandler)`. This will return the Widget instance which allows for chaining handler registration in the UI tree definition. 36 | 37 | You can choose to receive event data as the first parameter in your handler or omit it. The event data is of a corresponding `GUIEventNameEvent` (i.e. `GUIMouseButtonClickEvent`) type and might contain properties such as a position. 38 | 39 | Handler registration is internally forwarded to a corresponding `onSomeEventNameHandlerManager` which is an instance property of every Widget. You can also use this to register handlers directly by calling `onSomeEventNameHandlerManager.addHandler(yourHandler)`. This will return an unregister callback, which can be discarded or saved in order to remove the registered handler manually later. 40 | 41 | All `onSomeEventNameHandlerManager`s are defined in the [Widget](https://github.com/VertexUI/VertexGUI/blob/master/Sources/WidgetGUI/Base/Widget/Widget.swift) class, look for the "input events" section. 42 | The mappings from `onSomeEventName` to `onSomeEventNameHandlerManager` are defined in [Widget+inputEventHandlerRegistration.swift](https://github.com/VertexUI/VertexGUI/blob/master/Sources/WidgetGUI/Base/Widget/Widget%2BinputEventHandlerRegistration.swift). 43 | 44 | Direct use of `addHandler` on the corresponding `EventHandlerManager` can be useful when you want to handle events generated on a custom Widget inside your custom implementation. For example: 45 | 46 | ```swift 47 | class MyCustomWidget: Widget { // or inherit from LeafWidget, ComposedWidget, ... 48 | var unregisterKeyDownHandler: (() -> ())? = nil 49 | 50 | public init() { 51 | super.init() 52 | // "_ =" means the unregister callback is discarded 53 | // handleClickEvent will be called every time the user 54 | // clicks inside the bounding box of a MyCustomWidget instance 55 | _ = onClickHandlerManager.addHandler(handleClickEvent) 56 | 57 | // to store the unregister callback 58 | unregisterKeyDownHandler = onKeyDownHandlerManager.addHandler(handleKeyDown) 59 | } 60 | 61 | func handleClickEvent(_ event: GUIMouseButtonClickEvent) { 62 | // do something 63 | } 64 | 65 | func handleKeyDownEvent(_ event: GUIKeyDownEvent) { 66 | // do something 67 | // maybe you want to receive only one keyDown event 68 | // you could then simply call the unregister callback 69 | // in this handler 70 | unregisterKeyDownHandler?() 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /Docs/WidgetState.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /Docs/WidgetStyling.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /Docs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VertexUI/VertexGUI/bd1a4b124ebef002524deb66200053bb7b2035e1/Docs/demo.png -------------------------------------------------------------------------------- /Docs/minimal_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VertexUI/VertexGUI/bd1a4b124ebef002524deb66200053bb7b2035e1/Docs/minimal_demo.png -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adrian Zimmermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import Foundation 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "VertexGUI", 7 | 8 | platforms: [ 9 | .macOS(.v11) 10 | ], 11 | 12 | products: [ 13 | .library( 14 | name: "VertexGUI", 15 | targets: ["VertexGUI"] 16 | ), 17 | .executable(name: "MinimalDemo", targets: ["MinimalDemo"]), 18 | .executable(name: "DevApp", targets: ["DevApp"]), 19 | .executable( 20 | name: "TaskOrganizerDemo", 21 | targets: ["TaskOrganizerDemo"]), 22 | ], 23 | 24 | dependencies: [ 25 | .package(name: "GL", url: "https://github.com/UnGast/swift-opengl", .branch("master")), 26 | .package(name: "Swim", url: "https://github.com/UnGast/swim.git", .branch("master")), 27 | .package(name: "GfxMath", url: "https://github.com/UnGast/swift-gfx-math.git", .branch("master")), 28 | .package(url: "https://github.com/OpenCombine/OpenCombine.git", .branch("master")), 29 | .package(url: "https://github.com/mtynior/ColorizeSwift.git", from: "1.6.0"), 30 | .package(url: "https://github.com/UnGast/SkiaKit", .branch("main")), 31 | .package(name: "FirebladePAL", url: "https://github.com/fireblade-engine/pal.git", .branch("main")) 32 | ], 33 | 34 | targets: [ 35 | .target( 36 | name: "Drawing", 37 | dependencies: ["GfxMath", "FirebladePAL", "Swim"]), 38 | 39 | .target( 40 | name: "Application", 41 | dependencies: [ 42 | "WidgetGUI", 43 | "FirebladePAL", 44 | "Drawing", 45 | "GfxMath", 46 | "GL"]), 47 | 48 | .target( 49 | name: "Events", 50 | dependencies: ["OpenCombine"] 51 | ), 52 | 53 | .target( 54 | name: "WidgetGUI", 55 | dependencies: ["Events", "OpenCombine", .product(name: "OpenCombineDispatch", package: "OpenCombine"), "GfxMath", "ColorizeSwift", "Drawing", "SkiaKit"], 56 | resources: [.process("Resources")] 57 | ), 58 | 59 | .target( 60 | name: "TaskOrganizerDemo", 61 | dependencies: ["VertexGUI", "Swim", "OpenCombine"], 62 | resources: [.copy("Resources")]), 63 | 64 | .target( 65 | name: "MinimalDemo", 66 | dependencies: ["VertexGUI"] 67 | ), 68 | 69 | .target( 70 | name: "DevApp", 71 | dependencies: ["VertexGUI", "Swim"] 72 | ), 73 | 74 | .target( 75 | name: "VertexGUI", 76 | dependencies: ["WidgetGUI", "Events", "GfxMath", "Application", "Drawing"], 77 | resources: [.process("Resources")] 78 | ), 79 | 80 | //.testTarget(name: "WidgetGUITests", dependencies: ["VertexGUI"]) 81 | ] 82 | ) 83 | -------------------------------------------------------------------------------- /Sources/ApplicationBackendSDL2/Key+SDL2.swift: -------------------------------------------------------------------------------- 1 | import SDL2 2 | import Application 3 | 4 | fileprivate func asSDLKeycode(_ key: Int) -> SDL_Keycode { 5 | SDL_Keycode(key) 6 | } 7 | 8 | fileprivate func asSDLKeycode(_ key: T) -> SDL_Keycode where T.RawValue == UInt32 { 9 | SDL_Keycode(key.rawValue) 10 | } 11 | 12 | public extension Key { 13 | init?(sdlKeycode: SDL_Keycode) { 14 | switch sdlKeycode { 15 | case asSDLKeycode(SDLK_RETURN): self = .return 16 | case asSDLKeycode(SDLK_KP_ENTER): self = .enter 17 | case asSDLKeycode(SDLK_BACKSPACE): self = .backspace 18 | case asSDLKeycode(SDLK_DELETE): self = .delete 19 | case asSDLKeycode(SDLK_SPACE): self = .space 20 | case asSDLKeycode(SDLK_ESCAPE): self = .escape 21 | 22 | case asSDLKeycode(SDLK_UP): self = .arrowUp 23 | case asSDLKeycode(SDLK_RIGHT): self = .arrowRight 24 | case asSDLKeycode(SDLK_DOWN): self = .arrowDown 25 | case asSDLKeycode(SDLK_LEFT): self = .arrowLeft 26 | 27 | case asSDLKeycode(SDLK_LSHIFT): self = .leftShift 28 | case asSDLKeycode(SDLK_LCTRL): self = .leftCtrl 29 | case asSDLKeycode(SDLK_LALT): self = .leftAlt 30 | 31 | case asSDLKeycode(SDLK_a): self = .a 32 | case asSDLKeycode(SDLK_s): self = .s 33 | case asSDLKeycode(SDLK_d): self = .d 34 | case asSDLKeycode(SDLK_w): self = .w 35 | 36 | case asSDLKeycode(SDLK_PLUS): self = .plus 37 | case asSDLKeycode(SDLK_MINUS): self = .minus 38 | 39 | case asSDLKeycode(SDLK_F1): self = .f1 40 | case asSDLKeycode(SDLK_F2): self = .f2 41 | case asSDLKeycode(SDLK_F3): self = .f3 42 | case asSDLKeycode(SDLK_F4): self = .f4 43 | case asSDLKeycode(SDLK_F5): self = .f5 44 | case asSDLKeycode(SDLK_F6): self = .f6 45 | case asSDLKeycode(SDLK_F7): self = .f7 46 | case asSDLKeycode(SDLK_F8): self = .f8 47 | case asSDLKeycode(SDLK_F9): self = .f9 48 | case asSDLKeycode(SDLK_F10): self = .f10 49 | case asSDLKeycode(SDLK_F11): self = .f11 50 | case asSDLKeycode(SDLK_F12): self = .f12 51 | default: return nil 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApplicationBackendSDL2/MouseButton+SDL2.swift: -------------------------------------------------------------------------------- 1 | import Application 2 | import SDL2 3 | 4 | extension MouseButton { 5 | init(fromSDL sdlMouseButton: UInt8) { 6 | if sdlMouseButton == UInt8(SDL_BUTTON_LEFT) { 7 | self = .left 8 | } else if sdlMouseButton == UInt8(SDL_BUTTON_RIGHT) { 9 | self = .right 10 | } else { 11 | fatalError("sdl mouse button not mapped: \(sdlMouseButton)") 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Sources/ApplicationBackendSDL2/SDL2BaseWindow.swift: -------------------------------------------------------------------------------- 1 | import SDL2 2 | import Drawing 3 | import Application 4 | import GfxMath 5 | import Events 6 | import OpenCombine 7 | 8 | open class SDL2BaseWindow: Window { 9 | public var sdlWindow: OpaquePointer 10 | public let graphicsMode: GraphicsMode 11 | public var surface: DrawingSurface? 12 | 13 | public var size: ISize2 { 14 | var width: Int32 = 0 15 | var height: Int32 = 0 16 | SDL_GetWindowSize(sdlWindow, &width, &height) 17 | return ISize2(Int(width), Int(height)) 18 | } 19 | 20 | // TODO: instead use property wrapper on size so that can do: $size.sink 21 | public let sizeChanged = PassthroughSubject() 22 | public let inputEventPublisher = PassthroughSubject() 23 | 24 | public init(initialSize: ISize2, graphicsMode: GraphicsMode = .cpu) { 25 | SDL_Init(SDL_INIT_VIDEO) 26 | self.graphicsMode = graphicsMode 27 | 28 | var flags = SDL_WINDOW_RESIZABLE.rawValue 29 | if graphicsMode == .vulkan { 30 | flags = flags | SDL_WINDOW_VULKAN.rawValue 31 | } 32 | 33 | sdlWindow = SDL_CreateWindow( 34 | "", 35 | 0, 36 | 0, 37 | Int32(initialSize.width), 38 | Int32(initialSize.height), 39 | flags) 40 | 41 | 42 | /*let drawingBackend = SkiaCpuDrawingBackend(surface: surface) 43 | drawingBackend.drawLine(from: .zero, to: DVec2(options.initialSize), paint: Paint(color: nil, strokeWidth: 1, strokeColor: .blue)) 44 | drawingBackend.drawRect(rect: DRect(min: DVec2(200, 200), max: DVec2(400, 400)), paint: Paint(color: .yellow)) 45 | drawingBackend.drawCircle(center: DVec2(150, 150), radius: 100, paint: Paint(color: .green, strokeWidth: 1.0, strokeColor: .red))*/ 46 | 47 | ApplicationBackendSDL2.windows[Int(SDL_GetWindowID(sdlWindow))] = self 48 | } 49 | 50 | /** 51 | only necessary if you are using CpuBufferDrawingSurface 52 | */ 53 | open func swap() {} 54 | 55 | open func updateSurface() {} 56 | 57 | public func notifySizeChanged() { 58 | updateSurface() 59 | sizeChanged.send(size) 60 | } 61 | 62 | public func close() { 63 | SDL_DestroyWindow(sdlWindow) 64 | } 65 | 66 | public enum GraphicsMode { 67 | case cpu, openGL, vulkan 68 | } 69 | } -------------------------------------------------------------------------------- /Sources/ApplicationBackendSDL2/SDL2Window.swift: -------------------------------------------------------------------------------- 1 | import SDL2 2 | import Drawing 3 | import Application 4 | import GfxMath 5 | import Events 6 | import OpenCombine 7 | 8 | public class SDL2Window: SDL2BaseWindow { 9 | public init(initialSize: ISize2) { 10 | super.init(initialSize: initialSize, graphicsMode: .cpu) 11 | } 12 | 13 | public func getCpuBufferDrawingSurface() -> CpuBufferDrawingSurface { 14 | if self.graphicsMode != .cpu { 15 | fatalError("a cpu surface can only be created for windows which have been configured with graphicsMode: .cpu") 16 | } 17 | if self.surface != nil { 18 | fatalError("can only use one surface per window") 19 | } 20 | 21 | let sdlSurface = SDL_GetWindowSurface(sdlWindow) 22 | 23 | let surface = CpuBufferDrawingSurface(size: size) 24 | surface.buffer = UnsafeMutableBufferPointer( 25 | start: sdlSurface!.pointee.pixels.bindMemory( 26 | to: UInt8.self, capacity: size.width * size.height * 4), 27 | count: size.width * size.height * 4) 28 | self.surface = surface 29 | 30 | return surface 31 | } 32 | 33 | override public func updateSurface() { 34 | if let surface = surface as? CpuBufferDrawingSurface { 35 | let sdlSurface = SDL_GetWindowSurface(sdlWindow) 36 | surface.size = size 37 | surface.buffer = UnsafeMutableBufferPointer( 38 | start: sdlSurface!.pointee.pixels.bindMemory( 39 | to: UInt8.self, capacity: size.width * size.height * 4), 40 | count: size.width * size.height * 4) 41 | } 42 | } 43 | 44 | override public func swap() { 45 | SDL_UpdateWindowSurface(sdlWindow) 46 | } 47 | } -------------------------------------------------------------------------------- /Sources/DevApp/MainView.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | import Swim 3 | import OpenCombine 4 | 5 | public class MainView: ComposedWidget, SlotAcceptingWidgetProtocol { 6 | @Inject 7 | var someInjectedData: String 8 | 9 | @State 10 | var myState: String = "This is a value from an @State property." 11 | @State 12 | var testBackgroundColor: VertexGUI.Color = .orange 13 | @State 14 | var testImage: Swim.Image 15 | @State 16 | var textVisibility: Visibility = .hidden 17 | 18 | static let TestSlot1 = Slot(key: "testSlot1", data: Void.self) 19 | private let testSlot1 = SlotContentManager(MainView.TestSlot1) 20 | 21 | var stateSubscription: AnyCancellable? 22 | 23 | override public init() { 24 | self.testImage = Swim.Image(width: 800, height: 600, color: Swim.Color(r: 0, g: 0, b: 0, a: 255)) 25 | super.init() 26 | stateSubscription = self.$myState.publisher.sink { 27 | print("MY STATEA CHANGED", $0) 28 | } 29 | } 30 | 31 | @Compose override public var content: ComposedContent { 32 | TextInput(text: $myState.mutable, placeholder: "ENTER YOUR TEXT") 33 | } 34 | 35 | override public var style: Style? { 36 | Style("&") {} nested: { 37 | Style(".list-item") { 38 | (\.$foreground, .black) 39 | } 40 | 41 | Style(".list-item:hover") { 42 | (\.$background, .grey) 43 | } 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DevApp/TestWidget.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | public class TestWidget: ComposedWidget { 4 | @MutableBinding 5 | public var boundText: String 6 | 7 | public init(boundText: MutableBinding) { 8 | self._boundText = boundText 9 | } 10 | 11 | @Compose override public var content: ComposedContent { 12 | Container().with(classes: ["container"]).withContent { 13 | TextInput(text: $boundText.mutable, placeholder: "placeholder").with(styleProperties: { 14 | (\.$foreground, .black) 15 | }) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/DevApp/main.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | let app = try VertexGUI.Application() 4 | 5 | try app.createWindow(widgetRoot: WidgetGUI.Root(rootWidget: Container().withContent { 6 | MainView().with(styleProperties: { 7 | (\.$alignSelf, .stretch) 8 | (\.$grow, 1) 9 | (\.$background, .white) 10 | }) 11 | })) 12 | 13 | do { 14 | try app.start() 15 | } catch { 16 | print("Error while running the app", error) 17 | } -------------------------------------------------------------------------------- /Sources/Drawing/DrawingBackend.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | open class DrawingBackend { 4 | public init() {} 5 | 6 | open func activate() { 7 | 8 | } 9 | 10 | open func clip(rect: DRect) { 11 | fatalError("clip() not implemented") 12 | } 13 | 14 | open func resetClip() { 15 | fatalError("resetClip() not implemented") 16 | } 17 | 18 | open func drawLine(from start: DVec2, to end: DVec2, paint: Paint) { 19 | fatalError("drawLine() not implemented") 20 | } 21 | 22 | open func drawRect(rect: DRect, paint: Paint) { 23 | fatalError("drawRect() not implemented") 24 | } 25 | 26 | open func drawCircle(center: DVec2, radius: Double, paint: Paint) { 27 | fatalError("drawCircle() not implemented") 28 | } 29 | 30 | open func drawRoundedRect() { 31 | 32 | } 33 | 34 | open func drawPath() { 35 | 36 | } 37 | 38 | open func drawImage(image: Image2, topLeft: DVec2) { 39 | fatalError("drawImage() not implemented") 40 | } 41 | 42 | /** 43 | // TODO: maybe the result should be a rect to also have access to the position 44 | */ 45 | open func measureText(text: String, paint: TextPaint) -> DSize2 { 46 | fatalError("measureText() not implemented") 47 | } 48 | 49 | open func drawText(text: String, position: DVec2, paint: TextPaint) { 50 | fatalError("drawText() not implemented") 51 | } 52 | 53 | open func deactivate() { 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /Sources/Drawing/Image.swift: -------------------------------------------------------------------------------- 1 | import Swim 2 | 3 | final public class Image2: Hashable { 4 | public private(set) var data: Swim.Image 5 | public var invalid: Bool = true 6 | 7 | public init(fromRGBA data: Swim.Image) { 8 | self.data = data 9 | } 10 | 11 | public func hash(into hasher: inout Hasher) { 12 | hasher.combine(ObjectIdentifier(self)) 13 | } 14 | 15 | public func updateData(_ newData: Swim.Image) throws { 16 | if newData.width != data.width || newData.height != data.height { 17 | throw DataUpdateSizeMismatchError() 18 | } 19 | 20 | data = newData 21 | invalid = true 22 | } 23 | 24 | public static func == (lhs: Image2, rhs: Image2) -> Bool { 25 | lhs === rhs 26 | } 27 | 28 | public struct DataUpdateSizeMismatchError: Error { 29 | let description = "updated data size must match old data size" 30 | } 31 | } -------------------------------------------------------------------------------- /Sources/Drawing/MockDrawingBackend.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | open class MockDrawingBackend: DrawingBackend { 4 | override open func activate() { 5 | } 6 | 7 | override open func clip(rect: DRect) { 8 | } 9 | 10 | override open func resetClip() { 11 | } 12 | 13 | override open func drawLine(from start: DVec2, to end: DVec2, paint: Paint) { 14 | } 15 | 16 | override open func drawRect(rect: DRect, paint: Paint) { 17 | } 18 | 19 | override open func drawCircle(center: DVec2, radius: Double, paint: Paint) { 20 | } 21 | 22 | override open func drawRoundedRect() { 23 | } 24 | 25 | override open func drawPath() { 26 | } 27 | 28 | override open func drawImage(image: Image2, topLeft: DVec2) { 29 | } 30 | 31 | /** 32 | // TODO: maybe the result should be a rect to also have access to the position 33 | */ 34 | override open func measureText(text: String, paint: TextPaint) -> DSize2 { 35 | .zero 36 | } 37 | 38 | override open func drawText(text: String, position: DVec2, paint: TextPaint) { 39 | } 40 | 41 | override open func deactivate() { 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /Sources/Drawing/Paint.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public struct Paint { 4 | public var color: Color? 5 | public var strokeWidth: Double? 6 | public var strokeColor: Color? 7 | 8 | public init(color: Color? = nil, strokeWidth: Double? = nil, strokeColor: Color? = nil) { 9 | self.color = color 10 | self.strokeWidth = strokeWidth 11 | self.strokeColor = strokeColor 12 | } 13 | } -------------------------------------------------------------------------------- /Sources/Drawing/TextPaint.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public struct TextPaint { 4 | public var color: Color? = nil 5 | public var breakWidth: Double? = nil 6 | 7 | public init(color: Color? = nil, breakWidth: Double? = nil) { 8 | self.color = color 9 | self.breakWidth = breakWidth 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/DrawingVulkan/VulkanDrawingSurface.swift: -------------------------------------------------------------------------------- 1 | import Drawing 2 | import GfxMath 3 | import Vulkan 4 | 5 | public class VulkanDrawingSurface: DrawingSurface { 6 | public var vulkanSurface: Vulkan.SurfaceKHR 7 | public var size: ISize2 8 | public var resolution: Double 9 | 10 | public init(vulkanSurface: Vulkan.SurfaceKHR, size: ISize2, resolution: Double) { 11 | self.vulkanSurface = vulkanSurface 12 | self.size = size 13 | self.resolution = resolution 14 | } 15 | 16 | public func getDrawingContext() -> DrawingContext { 17 | fatalError("don't use, change api") 18 | } 19 | } -------------------------------------------------------------------------------- /Sources/Events/EventfulObject.swift: -------------------------------------------------------------------------------- 1 | public protocol EventfulObject: AnyObject { 2 | func removeAllEventHandlers() 3 | } 4 | 5 | extension EventfulObject { 6 | public func removeAllEventHandlers() { 7 | let mirror = Mirror(reflecting: self) 8 | for child in mirror.children { 9 | if let manager = child.value as? AnyEventHandlerManager { 10 | manager.removeAllHandlers() 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Events/PublishingEventManager.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public typealias PublishingEventManager = PassthroughSubject -------------------------------------------------------------------------------- /Sources/MinimalDemo/MainView.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | public class MainView: ComposedWidget { 4 | @State 5 | private var counter = 0 6 | 7 | @Compose override public var content: ComposedContent { 8 | Container().with(classes: ["container"]).withContent { [unowned self] in 9 | Button().onClick { 10 | counter += 1 11 | }.withContent { 12 | Text(ImmutableBinding($counter.immutable, get: { "counter: \($0)" })) 13 | } 14 | } 15 | } 16 | 17 | override public var style: Style { 18 | let primaryColor = Color(77, 255, 154, 255) 19 | 20 | return Style("&") { 21 | (\.$background, Color(10, 20, 30, 255)) 22 | } nested: { 23 | 24 | Style(".container", Container.self) { 25 | (\.$alignContent, .center) 26 | (\.$justifyContent, .center) 27 | } 28 | 29 | Style("Button") { 30 | (\.$padding, Insets(all: 16)) 31 | (\.$background, primaryColor) 32 | (\.$foreground, .black) 33 | (\.$fontWeight, .bold) 34 | } nested: { 35 | 36 | Style("&:hover") { 37 | (\.$background, primaryColor.darkened(20)) 38 | } 39 | 40 | Style("&:active") { 41 | (\.$background, primaryColor.darkened(40)) 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Sources/MinimalDemo/main.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | let app = try VertexGUI.Application() 4 | try! app.createWindow(widgetRoot: Root(rootWidget: MainView())) 5 | 6 | do { 7 | try app.start() 8 | } catch { 9 | print("an error occurred while running the app:", error) 10 | } 11 | -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/Data/MainViewRoute.swift: -------------------------------------------------------------------------------- 1 | public enum MainViewRoute { 2 | case none, selectedList, searchResults 3 | } -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/Data/TodoItem.swift: -------------------------------------------------------------------------------- 1 | import Swim 2 | 3 | public struct TodoItem: Equatable { 4 | public static var nextTodoItemId = 0 5 | 6 | public var id: Int 7 | public var listId: Int 8 | public var description: String 9 | public var images: [Image] = [] 10 | public var completed = false 11 | 12 | public init(listId: Int, description: String) { 13 | self.id = Self.nextTodoItemId 14 | self.listId = listId 15 | Self.nextTodoItemId += 1 16 | self.description = description 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/Data/TodoSearchResult.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public struct TodoSearchResult { 4 | public var query: String 5 | public var filteredLists: [FilteredTodoList] 6 | } 7 | 8 | public struct FilteredTodoList: TodoListProtocol { 9 | public var baseList: TodoList 10 | public var filteredIndices: [Int] 11 | 12 | public var id: Int { 13 | baseList.id 14 | } 15 | 16 | public var name: String { 17 | baseList.name 18 | } 19 | 20 | public var color: Color { 21 | baseList.color 22 | } 23 | 24 | public var items: [TodoItem] { 25 | filteredIndices.map { baseList.items[$0] } 26 | } 27 | } -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/Data/TodoSearcher.swift: -------------------------------------------------------------------------------- 1 | public struct TodoSearcher { 2 | public static func search(query: String, lists: [TodoList]) -> TodoSearchResult { 3 | var newSearchResult = TodoSearchResult(query: query, filteredLists: []) 4 | 5 | for list in lists { 6 | var filteredList = FilteredTodoList(baseList: list, filteredIndices: []) 7 | for (index, item) in list.items.enumerated() { 8 | if item.description.lowercased().contains(query.lowercased()) { 9 | filteredList.filteredIndices.append(index) 10 | } 11 | } 12 | newSearchResult.filteredLists.append(filteredList) 13 | } 14 | 15 | return newSearchResult 16 | } 17 | } -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/UI/AppTheme.swift: -------------------------------------------------------------------------------- 1 | import WidgetGUI 2 | import GfxMath 3 | 4 | // TODO: maybe add AppTheme.defaultStyles -> add globally 5 | public enum AppTheme { 6 | public static let primaryColor = Color(230, 230, 103, 255) 7 | public static let backgroundColor = Color(10, 24, 36, 255) 8 | public static let listItemDividerColor = backgroundColor.darkened(80) 9 | } -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/UI/SearchResultsView.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | public class SearchResultsView: ComposedWidget { 4 | @Inject 5 | private var store: TodoStore 6 | 7 | @Compose override public var content: ComposedContent { 8 | Container().with(classes: ["lists-container"]).withContent { [unowned self] in 9 | 10 | Dynamic(store.$state.searchResult.publisher) { 11 | 12 | (store.state.searchResult?.filteredLists ?? []).map { list in 13 | Container().with(classes: ["list"]).withContent { 14 | buildListHeader(list) 15 | 16 | list.items.map { 17 | buildSearchResult($0) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | func buildListHeader(_ list: TodoListProtocol) -> Widget { 26 | Text(list.name).with(classes: ["list-header"]) 27 | } 28 | 29 | func buildSearchResult(_ todoItem: TodoItem) -> Widget { 30 | TodoListItemView(todoItem).with(classes: ["list-item"]) 31 | } 32 | 33 | override public var style: Style { 34 | Style("&") {} nested: { 35 | Style(".lists-container", Container.self) { 36 | (\.$direction, .column) 37 | (\.$overflowY, .scroll) 38 | (\.$alignContent, .stretch) 39 | } 40 | 41 | Style(".list", Container.self) { 42 | (\.$direction, .column) 43 | (\.$margin, Insets(bottom: 64)) 44 | (\.$alignContent, .stretch) 45 | } 46 | 47 | Style(".list-header") { 48 | (\.$margin, Insets(bottom: 32)) 49 | (\.$fontWeight, .bold) 50 | (\.$fontSize, 36.0) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/UI/TaskCompletionButton.swift: -------------------------------------------------------------------------------- 1 | import WidgetGUI 2 | import SkiaKit 3 | import GfxMath 4 | import Drawing 5 | 6 | public class TaskCompletionButton: LeafWidget { 7 | private let preferredSize = DSize2(16, 16) 8 | private var completed: Bool 9 | 10 | public init(_ completed: Bool) { 11 | self.completed = completed 12 | super.init() 13 | } 14 | 15 | override public func performLayout(constraints: BoxConstraints) -> DSize2 { 16 | constraints.constrain(preferredSize) 17 | } 18 | 19 | override public func draw(_ drawingContext: DrawingContext, canvas: Canvas) { 20 | canvas.drawCircle(center: DVec2(layoutedSize / 2), radius: layoutedSize.min()! * 0.9, paint: Paint.stroke(color: foreground, width: 1.0)) 21 | if completed { 22 | canvas.drawCircle(center: DVec2(layoutedSize / 2), radius: layoutedSize.min()! * 0.8, paint: Paint.fill(color: foreground)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/UI/TodoListItemView.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | 3 | public class TodoListItemView: ComposedWidget { 4 | @Inject private var store: TodoStore 5 | 6 | private var item: TodoItem 7 | private var editable: Bool 8 | private var checkable: Bool 9 | 10 | @State private var editing: Bool = false 11 | @State private var updatedDescriptionBuffer: String = "" 12 | 13 | public init(_ item: TodoItem, editable: Bool = false, checkable: Bool = true) { 14 | self.item = item 15 | self.editable = editable 16 | self.checkable = checkable 17 | updatedDescriptionBuffer = item.description 18 | } 19 | 20 | @Compose override public var content: ComposedContent { 21 | Container().with(classes: ["root-container"]).withContent { 22 | TaskCompletionButton(item.completed).with(classes: ["completion-button"]).onClick { [unowned self] in 23 | if checkable { 24 | var updatedItem = item 25 | updatedItem.completed = !updatedItem.completed 26 | store.commit(.updateTodoItem(updatedItem: updatedItem)) 27 | } 28 | } 29 | 30 | Dynamic($editing.publisher) { [unowned self] in 31 | if editing { 32 | 33 | TextInput(text: $updatedDescriptionBuffer.mutable).with(classes: ["description"]).with { instance in 34 | _ = instance.onMounted { 35 | instance.requestFocus() 36 | } 37 | 38 | instance.onKeyUp { 39 | if $0.key == .return { 40 | var updatedItem = item 41 | updatedItem.description = updatedDescriptionBuffer 42 | store.commit(.updateTodoItem(updatedItem: updatedItem)) 43 | } 44 | } 45 | 46 | instance.$focused.publisher.sink { 47 | if !$0 { 48 | editing = false 49 | } 50 | }.store(in: &instance.cancellables) 51 | } 52 | } else { 53 | 54 | Text(item.description).with(classes: ["description"]).with { [unowned self] instance in 55 | if editable { 56 | instance.onClick { 57 | editing = true 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | override public var style: Style { 67 | Style("&") { 68 | (\.$foreground, .white) 69 | (\.$padding, Insets(top: 16, right: 24, bottom: 16, left: 24)) 70 | (\.$borderColor, AppTheme.listItemDividerColor) 71 | (\.$borderWidth, BorderWidth(bottom: 1.0)) 72 | } nested: { 73 | Style(".root-container", Container.self) { 74 | (\.$alignContent, .center) 75 | } 76 | 77 | Style(".completion-button") { 78 | (\.$foreground, AppTheme.primaryColor) 79 | (\.$margin, Insets(right: 24)) 80 | } nested: { 81 | 82 | Style("&:hover") { 83 | (\.$foreground, AppTheme.primaryColor.darkened(40)) 84 | } 85 | } 86 | 87 | Style(".description") { 88 | (\.$alignSelf, .center) 89 | (\.$grow, 1) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/TaskOrganizerDemo/main.swift: -------------------------------------------------------------------------------- 1 | import VertexGUI 2 | import SkiaKit 3 | import Dispatch 4 | 5 | let app = try VertexGUI.Application() 6 | 7 | let store: TodoStore = TodoStore() 8 | 9 | DispatchQueue.main.async { 10 | try! app.createWindow(widgetRoot: Root(rootWidget: Container().withContent { 11 | TodoAppView().with(styleProperties: { 12 | (\.$grow, 1) 13 | (\.$alignSelf, .stretch) 14 | }) 15 | }.provide(dependencies: store))) 16 | } 17 | 18 | do { 19 | try app.start() 20 | } catch { 21 | print("Error while running app", error) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/VertexGUI/lib.swift: -------------------------------------------------------------------------------- 1 | @_exported import WidgetGUI 2 | @_exported import Drawing 3 | @_exported import Events 4 | @_exported import Application 5 | @_exported import GfxMath -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Child.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | public protocol Child: AnyObject { 4 | var parent: Parent? { get set } 5 | 6 | // TODO: is this necessary? 7 | var onParentChanged: EventHandlerManager { get } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Parent.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | // TODO: mabye rename to WidgetParent and add children: [Widget] 4 | public protocol Parent: AnyObject { 5 | var globalPosition: DPoint2 { get } 6 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/PseudoElement.swift: -------------------------------------------------------------------------------- 1 | open class PseudoElement { 2 | open var identifier: String { 3 | fatalError("identifier not implemented") 4 | } 5 | //public let stylePropertiesResolver = StylePropertiesResolver() 6 | 7 | public init() {} 8 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleFlag.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public enum LifecycleFlag { 3 | case initialized, mounted, built, layouted, rendered, unmounted, destroyed 4 | } 5 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleManager.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | extension Widget { 4 | public class LifecycleManager { 5 | private let getCurrentTick: () -> Tick 6 | 7 | var queues: [LifecycleMethod: LifecycleMethodInvocationQueue] = LifecycleMethod.allCases.reduce(into: [:]) { 8 | $0[$1] = LifecycleMethodInvocationQueue() 9 | } 10 | 11 | public init(_ getCurrentTick: @escaping () -> Tick) { 12 | self.getCurrentTick = getCurrentTick 13 | } 14 | 15 | public func queue(_ method: LifecycleMethod, target: Widget, sender: Widget, reason: LifecycleMethodInvocationReason) { 16 | let newEntry = LifecycleMethodInvocationQueue.Entry(method: method, target: target, sender: sender, reason: reason, tick: getCurrentTick()) 17 | queues[method]!.queue(newEntry) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethod.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public enum LifecycleMethod: CaseIterable { 3 | case mount, build, updateChildren, updateMatchedStyles, resolveStyleProperties, layout, resolveCumulatedValues, draw, unmount, destroy 4 | } 5 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethodInvocationAbortionReason.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public enum LifecycleMethodInvocationAbortionReason { 3 | case layout(LayoutInvocationAbortionReason) 4 | } 5 | 6 | public enum LayoutInvocationAbortionReason { 7 | case layoutStillValid 8 | } 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethodInvocationQueue.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | extension Widget { 4 | public class LifecycleMethodInvocationQueue { 5 | var entries: [Entry] = [] 6 | 7 | let onEntryAdded = EventHandlerManager() 8 | 9 | public init() {} 10 | 11 | public func queue(_ entry: Entry) { 12 | entries.append(entry) 13 | onEntryAdded.invokeHandlers(entry) 14 | } 15 | 16 | public func clear() { 17 | entries = [] 18 | } 19 | 20 | public func iterate() -> Iterator { 21 | let iterator = Iterator(entries: entries) 22 | _ = iterator.onDestroy(onEntryAdded.addHandler { [unowned iterator] in 23 | iterator.entries.append($0) 24 | }) 25 | return iterator 26 | } 27 | 28 | public func iterateSubTreeRoots() -> Iterator { 29 | // TODO: implement iterator in such a way that when a new item is added to the queue 30 | // this item is inserted into the iterator at the correct position -> if at a higher level 31 | // than currently at, add it as the item for the next iteration (items already iterated are discarded by the iterator) 32 | // and remove the items below it 33 | // if it is not included in a tree path already in the iterator, add it to the end 34 | var byTreePath: [TreePath: Entry] = [:] 35 | outer: for entry in entries { 36 | for (otherPath, otherEntry) in byTreePath { 37 | if entry.target.treePath.isParent(of: otherPath) { 38 | byTreePath.removeValue(forKey: otherPath) 39 | } else if otherPath.isParent(of: entry.target.treePath) { 40 | continue outer 41 | } 42 | } 43 | 44 | byTreePath[entry.target.treePath] = entry 45 | } 46 | 47 | return Iterator(entries: Array(byTreePath.values)) 48 | } 49 | 50 | public class Entry { 51 | public var method: LifecycleMethod 52 | public var target: Widget 53 | public var sender: Widget 54 | public var reason: LifecycleMethodInvocationReason 55 | public var tick: Tick 56 | 57 | public init(method: LifecycleMethod, target: Widget, sender: Widget, reason: LifecycleMethodInvocationReason, tick: Tick) { 58 | self.method = method 59 | self.target = target 60 | self.sender = sender 61 | self.reason = reason 62 | self.tick = tick 63 | } 64 | } 65 | 66 | public class Iterator: IteratorProtocol { 67 | var entries: [Entry] 68 | var ownedObjects: [Any] = [] 69 | let onDestroy = EventHandlerManager() 70 | 71 | public init(entries: [Entry]) { 72 | self.entries = entries 73 | } 74 | 75 | public func next() -> Entry? { 76 | entries.popLast() 77 | } 78 | 79 | func removeDuplicates() { 80 | var occurredEntries = [ObjectIdentifier]() 81 | entries = entries.filter { 82 | let id = ObjectIdentifier($0) 83 | if !occurredEntries.contains(id) { 84 | occurredEntries.append(id) 85 | return true 86 | } 87 | return false 88 | } 89 | } 90 | 91 | deinit { 92 | onDestroy.invokeHandlers() 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethodInvocationReason.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public enum LifecycleMethodInvocationReason { 3 | case build(BuildInvocationReason) 4 | case mount(MountInvocationReason) 5 | case layout(LayoutInvocationReason) 6 | case render(RenderInvocationReason) 7 | case unmount(UnmountInvocationReason) 8 | case destroy(DestroyInvocationReason) 9 | case queued(LifecycleMethodInvocationQueue.Entry) 10 | case undefined 11 | } 12 | 13 | public enum BuildInvocationReason { 14 | case parentBuilds 15 | } 16 | 17 | public enum MountInvocationReason { 18 | } 19 | 20 | public enum LayoutInvocationReason { 21 | case parentLayouts 22 | } 23 | 24 | public enum RenderInvocationReason { 25 | case renderRoot 26 | case parentRenders 27 | case renderContentOfParent(Widget) 28 | } 29 | 30 | public enum UpdateRenderStateInvocationReason { 31 | case renderCalled(RenderInvocationReason) 32 | case rootTick 33 | } 34 | 35 | public enum UnmountInvocationReason { 36 | } 37 | 38 | public enum DestroyInvocationReason { 39 | case parentDestroyed 40 | } 41 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethodInvocationSignal.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Widget { 3 | public enum LifecycleMethodInvocationSignal: Equatable { 4 | case started(method: LifecycleMethod, reason: LifecycleMethodInvocationReason, invocationId: Int, timestamp: Double) 5 | case aborted(method: LifecycleMethod, reason: LifecycleMethodInvocationAbortionReason, invocationId: Int, timestamp: Double) 6 | case completed(method: LifecycleMethod, invocationId: Int, timestamp: Double) 7 | 8 | public var method: LifecycleMethod { 9 | switch self { 10 | case let .started(method, _, _, _): 11 | return method 12 | case let .aborted(method, _, _, _): 13 | return method 14 | case let .completed(method, _, _): 15 | return method 16 | } 17 | } 18 | 19 | public var invocationId: Int { 20 | switch self { 21 | case let .started(_, _, invocationId, _): 22 | return invocationId 23 | case let .aborted(_, _, invocationId, _): 24 | return invocationId 25 | case let .completed(_, invocationId, _): 26 | return invocationId 27 | } 28 | } 29 | 30 | public static func == (lhs: Self, rhs: Self) -> Bool { 31 | if case let .started(_, _, invocationId1, _) = lhs, case let .started(_, _, invocationId2, _) = rhs { 32 | return invocationId1 == invocationId2 33 | } else if case let .aborted(_, _, invocationId1, _) = lhs, case let .aborted(_, _, invocationId2, _) = rhs { 34 | return invocationId1 == invocationId2 35 | } else if case let .completed(_, invocationId1, _) = lhs, case let .completed(_, invocationId2, _) = rhs { 36 | return invocationId1 == invocationId2 37 | } else { 38 | return false 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Lifecycle/Widget+LifecycleMethodInvocationSignalGroup.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public struct LifecycleMethodInvocationSignalGroup: Equatable { 3 | public var method: LifecycleMethod 4 | public var invocationId: Int 5 | public var signals: [LifecycleMethodInvocationSignal] 6 | public var startTimestamp: Double { 7 | for signal in signals { 8 | switch signal { 9 | case let .started(_, _, _, timestamp): 10 | return timestamp 11 | default: 12 | break 13 | } 14 | } 15 | return -1 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Signaling/Signal.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | /// Define a PublishingEventManager attribute of a Widget as a publically accessible signal, 4 | /// so that it can be used with `.on(\.$signalName) { execute... }` 5 | @propertyWrapper 6 | public class Signal { 7 | public typealias Value = V 8 | 9 | public var wrappedValue: PublishingEventManager 10 | 11 | public var projectedValue: Signal { 12 | self 13 | } 14 | 15 | public init(wrappedValue: PublishingEventManager) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Signaling/Widget+signaling.swift: -------------------------------------------------------------------------------- 1 | public protocol _SignalingWidget: Widget {} 2 | 3 | extension _SignalingWidget { 4 | /// Registers an event handler that will be destroyed when the widget itself is destroyed. 5 | /// Mainly for use in chaining in UI structure declaration. 6 | public func on(_ signal: KeyPath>, handler: @escaping () -> ()) -> Self { 7 | cancellables.insert(self[keyPath: signal].wrappedValue.sink { _ in 8 | handler() 9 | }) 10 | return self 11 | } 12 | 13 | /// Registers an event handler that will be destroyed when the widget itself is destroyed. 14 | /// Mainly for use in chaining in UI structure declaration. 15 | public func on(_ signal: KeyPath>, handler: @escaping (V) -> ()) -> Self { 16 | cancellables.insert(self[keyPath: signal].wrappedValue.sink { 17 | handler($0) 18 | }) 19 | return self 20 | } 21 | } 22 | 23 | extension Widget: _SignalingWidget {} -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+CallCounter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Widget { 4 | @usableFromInline 5 | internal struct CallCounter { 6 | private static let burstInterval = 0.001 // in seconds 7 | private static let burstLogThreshold = 3 // after which call count in the burst interval should a message be printed? 8 | 9 | public private(set) var counts = Self.makeDictionary(0) 10 | public private(set) var burstCounts = Self.makeDictionary(0) 11 | public private(set) var burstStartTimestamps = Self.makeDictionary(0.0) 12 | 13 | // TODO: does this cause a retain cycle? 14 | unowned public var widget: Widget 15 | 16 | /** 17 | - Returns: true if the burst log threshold was exceeded, false if not 18 | */ 19 | @usableFromInline 20 | @discardableResult 21 | mutating func count(_ callType: CallType) -> Bool { 22 | counts[callType] += 1 23 | let currentTimestamp = Date.timeIntervalSinceReferenceDate 24 | let previousBurstStartTimestamp = burstStartTimestamps[callType] 25 | 26 | if currentTimestamp - previousBurstStartTimestamp < Self.burstInterval { 27 | burstCounts[callType] += 1 28 | } else { 29 | burstCounts[callType] = 1 30 | burstStartTimestamps[callType] = currentTimestamp 31 | } 32 | 33 | if burstCounts[callType] > Self.burstLogThreshold { 34 | Logger.log(LogText(stringLiteral: "\(callType) called \(burstCounts[callType]) times" + 35 | " in \(Self.burstInterval) or less seconds in widget \(widget) with id \(widget.id)"), 36 | level: .Message, context: .Performance) 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | private static func makeDictionary(_ initial: T) -> DefinitiveDictionary { 43 | DefinitiveDictionary( 44 | CallType.allCases.reduce(into: [CallType: T]()) { 45 | $0[$1] = initial 46 | } 47 | ) 48 | } 49 | } 50 | 51 | @usableFromInline internal enum CallType: CaseIterable { 52 | case Build, Layout, Render, InvalidateRenderState, InvalidateLayout 53 | } 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+Debugging.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public struct DebugMessage { 3 | public var message: String 4 | public var sender: Widget 5 | 6 | public init(_ message: String, sender: Widget) { 7 | self.message = message 8 | self.sender = sender 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+PostInitConfigurableWidget.swift: -------------------------------------------------------------------------------- 1 | public protocol PostInitConfigurableWidget { 2 | func with(_ block: (T) -> ()) -> Self 3 | } 4 | 5 | extension Widget: PostInitConfigurableWidget { 6 | public func with(_ block: (T) -> ()) -> Self { 7 | guard let castedSelf = self as? T else { 8 | fatalError("wrong widget type assumed in with(): \(T.self) for widget: \(self)") 9 | } 10 | block(castedSelf) 11 | return self 12 | } 13 | 14 | public func with(classes: String...) -> Self { 15 | self.classes.append(contentsOf: classes) 16 | return self 17 | } 18 | 19 | public func with(classes: [String]) -> Self { 20 | self.classes.append(contentsOf: classes) 21 | return self 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+ScrollBar.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | import Drawing 3 | import SkiaKit 4 | 5 | extension Widget { 6 | public class ScrollBar: LeafWidget { 7 | private let orientation: Orientation 8 | 9 | @StyleProperty 10 | public var xBarHeight: Double = 40 11 | @StyleProperty 12 | public var yBarWidth: Double = 40 13 | 14 | @State 15 | public var scrollProgress = 0.0 16 | public var maxScrollProgress = 0.0 17 | 18 | var trackLength: Double { 19 | let trackLengthBase: Double 20 | switch orientation { 21 | case .horizontal: trackLengthBase = layoutedSize.width 22 | case .vertical: trackLengthBase = layoutedSize.height 23 | } 24 | return trackLengthBase / (1 + maxScrollProgress) 25 | } 26 | 27 | private var trackingMouse = false 28 | private var trackingStartProgress = 0.0 29 | private var trackingStartPosition: DPoint2 = .zero 30 | 31 | public init(orientation: Orientation) { 32 | self.orientation = orientation 33 | super.init() 34 | self.unaffectedByParentScroll = true 35 | 36 | _ = onMouseDown(handleMouseDown) 37 | _ = onMouseUp(handleMouseUp) 38 | 39 | _ = onMounted { [unowned self] in 40 | _ = onDestroy(rootParent.onMouseMoveHandlerManager.addHandler(handleMouseMove)) 41 | } 42 | } 43 | 44 | override public func performLayout(constraints: BoxConstraints) -> DSize2 { 45 | constraints.constrain(DSize2(yBarWidth, xBarHeight)) 46 | } 47 | 48 | func handleMouseDown(_ event: GUIMouseButtonDownEvent) { 49 | if event.button == .Left { 50 | trackingMouse = true 51 | trackingStartProgress = scrollProgress 52 | trackingStartPosition = event.globalPosition 53 | } 54 | } 55 | 56 | func handleMouseMove(_ event: GUIMouseMoveEvent) { 57 | if trackingMouse { 58 | let relevantMove: Double 59 | switch orientation { 60 | case .horizontal: relevantMove = event.globalPosition.x - trackingStartPosition.x 61 | case .vertical: relevantMove = event.globalPosition.y - trackingStartPosition.y 62 | } 63 | scrollProgress = max(min(trackingStartProgress + relevantMove / trackLength, maxScrollProgress), 0) 64 | } 65 | } 66 | 67 | func handleMouseUp(_ event: GUIMouseButtonUpEvent) { 68 | if event.button == .Left { 69 | trackingMouse = false 70 | } 71 | } 72 | 73 | override public func draw(_ drawingContext: DrawingContext, canvas: Canvas) { 74 | let trackOffset = trackLength * scrollProgress 75 | 76 | let trackRect: DRect 77 | switch orientation { 78 | case .horizontal: 79 | trackRect = DRect(min: DVec2(trackOffset, 0), size: DSize2(trackLength, layoutedSize.height)) 80 | case .vertical: 81 | trackRect = DRect(min: DVec2(0, trackOffset), size: DSize2(layoutedSize.width, trackLength)) 82 | } 83 | 84 | canvas.drawRect(trackRect, Paint.fill(color: foreground)) 85 | } 86 | 87 | public enum Orientation { 88 | case horizontal, vertical 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+TreeOperations.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | 3 | public final func findParent(_ condition: (_ parent: Parent) throws -> Bool) rethrows -> Parent? { 4 | var parent: Parent? = self.parent 5 | 6 | while parent != nil { 7 | if try condition(parent!) { 8 | return parent 9 | } 10 | 11 | if let currentParent = parent as? Widget { 12 | parent = currentParent.parent 13 | } 14 | } 15 | 16 | return nil 17 | } 18 | 19 | public final func getParent(ofType type: T.Type) -> T? { 20 | let parents = getParents(ofType: type) 21 | return parents.count > 0 ? parents[0] : nil 22 | } 23 | 24 | /// - Returns: all parents of given type, sorted from nearest to farthest 25 | public final func getParents(ofType type: T.Type) -> [T] { 26 | var selectedParents = [T]() 27 | var currentParent: Parent? = self.parent 28 | 29 | while currentParent != nil { 30 | if let parent = currentParent as? T { 31 | selectedParents.append(parent) 32 | } 33 | 34 | if let childParent = currentParent! as? Child { 35 | currentParent = childParent.parent 36 | 37 | } else { 38 | break 39 | } 40 | } 41 | 42 | return selectedParents 43 | } 44 | 45 | // TODO: might need possibility to return all of type + a method that only returns first + in what order depth first / breadth first 46 | public final func getChild(ofType type: W.Type) -> W? { 47 | for child in children { 48 | if let child = child as? W { 49 | return child 50 | } 51 | } 52 | 53 | for child in children { 54 | if let result = child.getChild(ofType: type) { 55 | return result 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | /** 63 | Get a child at any depth where the given condition is true. Depth first. 64 | */ 65 | public final func getChild(where condition: (_ child: Widget) -> Bool) -> Widget? { 66 | for child in children { 67 | if condition(child) { 68 | return child 69 | } else if let result = child.getChild(where: condition) { 70 | return result 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+invalidateRootSizeDependentThings.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | /// automatically recurses to all children 3 | public func invalidateRootSizeDependentThings() { 4 | for child in children { 5 | child.invalidateRootSizeDependentThings() 6 | } 7 | 8 | updateExplicitConstraints() 9 | } 10 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+otherEventHandlerRegistration.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | import GfxMath 3 | 4 | extension Widget { 5 | public func onSizeChanged(_ handler: @escaping (((newSize: DSize2, firstLayoutPass: Bool)) -> Void)) -> AnyCancellable{ 6 | sizeChangedEventManager.sink(receiveValue: handler) 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+resolveWidgetDimensionSize.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func resolve(widgetDimensionSize value: WidgetDimensionSize) -> Double { 3 | switch value { 4 | case let .a(value): return value 5 | case let .rw(percent): return context.getRootSize().width * percent / 100 6 | case let .rh(percent): return context.getRootSize().height * percent / 100 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/Widget/Widget+with.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func with(classes: [String]? = nil) -> Self { 3 | if let classes = classes { 4 | self.classes.append(contentsOf: classes) 5 | } 6 | return self 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Base/WidgetContext.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | import Events 3 | import Drawing 4 | 5 | public class WidgetContext { 6 | public let getRoot: () -> Root 7 | private let publishDebugMessage: (_ debugMessage: Widget.DebugMessage) -> () 8 | public let getRootSize: () -> DSize2 9 | private var _requestCursor: (_ cursor: Cursor) -> () -> Void 10 | private let _getKeyStates: () -> KeyStatesContainer 11 | private let _getApplicationTime: () -> Double 12 | public let getWindow: () -> Window 13 | public var applicationTime: Double { 14 | _getApplicationTime() 15 | } 16 | private let getRealFps: () -> Double 17 | public var realFps: Double { 18 | getRealFps() 19 | } 20 | public let getClipboardText: () -> String 21 | 22 | private let _queueLifecycleMethodInvocation: (Widget.LifecycleMethod, Widget, Widget, Widget.LifecycleMethodInvocationReason) -> () 23 | 24 | public let inspectionBus = WidgetBus() 25 | 26 | public private(set) var onTick = EventHandlerManager() 27 | 28 | public var keyStates: KeyStatesContainer { 29 | _getKeyStates() 30 | } 31 | 32 | public let focusManager: FocusManager 33 | 34 | public init( 35 | getRoot: @escaping () -> Root, 36 | getRootSize: @escaping () -> DSize2, 37 | getKeyStates: @escaping () -> KeyStatesContainer, 38 | getApplicationTime: @escaping () -> Double, 39 | getWindow: @escaping () -> Window, 40 | getRealFps: @escaping () -> Double, 41 | getClipboardText: @escaping () -> String, 42 | requestCursor: @escaping (_ cursor: Cursor) -> () -> Void, 43 | queueLifecycleMethodInvocation: @escaping (Widget.LifecycleMethod, Widget, Widget, Widget.LifecycleMethodInvocationReason) -> (), 44 | focusManager: FocusManager, 45 | publishDebugMessage: @escaping (Widget.DebugMessage) -> ()) { 46 | self.getRoot = getRoot 47 | self.getRootSize = getRootSize 48 | self._getKeyStates = getKeyStates 49 | self._getApplicationTime = getApplicationTime 50 | self.getWindow = getWindow 51 | self.getRealFps = getRealFps 52 | self.getClipboardText = getClipboardText 53 | self._requestCursor = requestCursor 54 | self._queueLifecycleMethodInvocation = queueLifecycleMethodInvocation 55 | self.focusManager = focusManager 56 | self.publishDebugMessage = publishDebugMessage 57 | } 58 | 59 | public func requestCursor(_ cursor: Cursor) -> () -> Void { 60 | _requestCursor(cursor) 61 | } 62 | 63 | public func queueLifecycleMethodInvocation(_ method: Widget.LifecycleMethod, target: Widget, sender: Widget, reason: Widget.LifecycleMethodInvocationReason) { 64 | _queueLifecycleMethodInvocation(method, target, sender, reason) 65 | } 66 | 67 | public func publish(debugMessage: Widget.DebugMessage) { 68 | publishDebugMessage(debugMessage) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/ComposeBuilder.swift: -------------------------------------------------------------------------------- 1 | public typealias Compose = DirectContentBuilder -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/ComposedContent.swift: -------------------------------------------------------------------------------- 1 | public typealias ComposedContent = DirectContent -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/ComposedWidget.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | open class ComposedWidget: Widget { 4 | private var _content: ComposedContent? 5 | open var content: ComposedContent { 6 | fatalError("content not implemented") 7 | } 8 | 9 | override public init() { 10 | super.init() 11 | _ = onDestroy { [unowned self] in 12 | if let content = _content { 13 | content.destroy() 14 | } 15 | _content = nil 16 | } 17 | } 18 | 19 | override public func performBuild() { 20 | _content = content 21 | contentChildren = _content!.widgets 22 | _ = onDestroy(_content!.onChanged { [unowned self] in 23 | contentChildren = _content!.widgets 24 | }) 25 | } 26 | 27 | override open func performLayout(constraints: BoxConstraints) -> DSize2 { 28 | var maxSize = DSize2.zero 29 | for child in contentChildren { 30 | child.layout(constraints: constraints) 31 | if child.layoutedSize.width > maxSize.width { 32 | maxSize.width = child.layoutedSize.width 33 | } 34 | if child.layoutedSize.height > maxSize.height { 35 | maxSize.height = child.layoutedSize.height 36 | } 37 | } 38 | return maxSize 39 | } 40 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/Content.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Events 3 | 4 | public protocol ContentProtocol: AnyObject { 5 | associatedtype Partial 6 | 7 | var partials: [Partial] { get set } 8 | 9 | var onChanged: EventHandlerManager { get } 10 | var onDestroy: EventHandlerManager { get } 11 | 12 | init(partials: [Partial]) 13 | } 14 | 15 | extension ContentProtocol { 16 | func updateReplacementRanges(ranges: [Int: Range], from startIndex: Int, deltaLength: Int) -> [Int: Range] { 17 | var result = ranges 18 | for (rangeIndex, range) in result { 19 | if rangeIndex == startIndex { 20 | result[rangeIndex] = range.lowerBound.. startIndex { 22 | result[rangeIndex] = range.lowerBound + deltaLength..() 33 | public let onDestroy = EventHandlerManager() 34 | public private(set) var destroyed = false 35 | 36 | public func destroy() { 37 | onDestroy.invokeHandlers() 38 | removeAllEventHandlers() 39 | destroyed = true 40 | } 41 | 42 | deinit { 43 | if !destroyed { 44 | destroy() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/DirectContent.swift: -------------------------------------------------------------------------------- 1 | public class DirectContent: Content, ContentProtocol { 2 | public var partials: [Partial] = [] { 3 | didSet { 4 | if !destroyed { 5 | resolve() 6 | } 7 | } 8 | } 9 | public var widgets: [Widget] = [] 10 | var replacementRanges = [Int: Range]() 11 | 12 | var nestedHandlerRemovers = [() -> ()]() 13 | 14 | public required init(partials: [Partial]) { 15 | self.partials = partials 16 | super.init() 17 | resolve() 18 | } 19 | 20 | func resolve() { 21 | for remove in nestedHandlerRemovers { 22 | remove() 23 | } 24 | widgets = [] 25 | replacementRanges = [:] 26 | nestedHandlerRemovers = [] 27 | 28 | for (index, partial) in partials.enumerated() { 29 | switch partial { 30 | case let .widget(widget): 31 | widgets.append(widget) 32 | 33 | default: 34 | let nestedContent: DirectContent 35 | 36 | if case let .content(nested) = partial { 37 | nestedContent = nested 38 | } else if case let .dynamic(dynamic) = partial { 39 | nestedContent = dynamic.content 40 | } else { 41 | fatalError("unhandled case") 42 | } 43 | 44 | let nestedWidgets = nestedContent.widgets 45 | 46 | replacementRanges[index] = widgets.count..<(widgets.count + nestedWidgets.count) 47 | 48 | widgets.append(contentsOf: nestedWidgets) 49 | 50 | nestedHandlerRemovers.append(nestedContent.onChanged { [unowned self, unowned nestedContent] in 51 | let nestedWidgets = nestedContent.widgets 52 | widgets.replaceSubrange(replacementRanges[index]!, with: nestedWidgets) 53 | 54 | replacementRanges = updateReplacementRanges( 55 | ranges: replacementRanges, 56 | from: index, 57 | deltaLength: nestedWidgets.count - replacementRanges[index]!.count) 58 | 59 | onChanged.invokeHandlers() 60 | }) 61 | } 62 | } 63 | 64 | onChanged.invokeHandlers() 65 | } 66 | 67 | override public func destroy() { 68 | super.destroy() 69 | widgets = [] 70 | partials = [] 71 | for remove in nestedHandlerRemovers { 72 | remove() 73 | } 74 | nestedHandlerRemovers = [] 75 | } 76 | } 77 | 78 | extension DirectContent { 79 | public enum Partial { 80 | case widget(Widget) 81 | case content(DirectContent) 82 | case dynamic(Dynamic) 83 | } 84 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/DirectContentBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | public struct DirectContentBuilder { 3 | public static func buildExpression(_ widget: Widget) -> [DirectContent.Partial] { 4 | [.widget(widget)] 5 | } 6 | 7 | public static func buildExpression(_ widgets: [Widget]) -> [DirectContent.Partial] { 8 | widgets.map { .widget($0) } 9 | } 10 | 11 | public static func buildExpression(_ content: DirectContent) -> [DirectContent.Partial] { 12 | [.content(content)] 13 | } 14 | 15 | public static func buildExpression(_ dynamicContent: Dynamic) -> [DirectContent.Partial] { 16 | [.dynamic(dynamicContent)] 17 | } 18 | 19 | public static func buildOptional(_ partials: [DirectContent.Partial]?) -> [DirectContent.Partial] { 20 | partials ?? [] 21 | } 22 | 23 | public static func buildEither(first: [DirectContent.Partial]) -> [DirectContent.Partial] { 24 | return first 25 | } 26 | 27 | public static func buildEither(second: [DirectContent.Partial]) -> [DirectContent.Partial] { 28 | return second 29 | } 30 | 31 | public static func buildArray(_ components: [[DirectContent.Partial]]) -> [DirectContent.Partial] { 32 | components.flatMap { $0 } 33 | } 34 | 35 | public static func buildBlock(_ partials: [DirectContent.Partial]...) -> [DirectContent.Partial] { 36 | partials.flatMap { $0 } 37 | } 38 | 39 | public static func buildFinalResult(_ partials: [DirectContent.Partial]) -> [DirectContent.Partial] { 40 | partials 41 | } 42 | 43 | public static func buildFinalResult(_ partials: [DirectContent.Partial]) -> DirectContent { 44 | DirectContent(partials: partials) 45 | } 46 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/Dynamic.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | import OpenCombine 3 | 4 | public class Dynamic { 5 | var associatedStyleScope: UInt 6 | 7 | let content: C 8 | 9 | var triggerProperty: AnyObject? 10 | var triggerSubscription: AnyCancellable? 11 | 12 | private init(trigger: P, build: @escaping () -> [C.Partial]) where P.Failure == Never { 13 | self.associatedStyleScope = Widget.activeStyleScope 14 | 15 | let partials = build() 16 | self.content = C(partials: partials) 17 | 18 | triggerSubscription = trigger.sink { [unowned self] _ in 19 | Widget.inStyleScope(self.associatedStyleScope) { 20 | self.content.partials = build() 21 | } 22 | } 23 | } 24 | } 25 | 26 | extension Dynamic where C == DirectContent { 27 | public convenience init(_ trigger: P, @DirectContentBuilder build: @escaping () -> [C.Partial]) where P.Failure == Never { 28 | self.init(trigger: trigger, build: build) 29 | } 30 | 31 | public convenience init(_ trigger: P, @DirectContentBuilder build: @escaping () -> [C.Partial]) { 32 | self.init(trigger: trigger.publisher, build: build) 33 | triggerProperty = trigger 34 | } 35 | } 36 | 37 | extension Dynamic where C == SlotContent { 38 | public convenience init(_ trigger: P, @SlotContentBuilder build: @escaping () -> [C.Partial]) where P.Failure == Never { 39 | self.init(trigger: trigger, build: build) 40 | } 41 | 42 | public convenience init(_ trigger: P, @SlotContentBuilder build: @escaping () -> [C.Partial]) { 43 | self.init(trigger: trigger.publisher, build: build) 44 | triggerProperty = trigger 45 | } 46 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/Slot.swift: -------------------------------------------------------------------------------- 1 | public class Slot: AnySlot { 2 | public let key: String 3 | let dataType: D.Type 4 | 5 | public init(key: String, data: D.Type) { 6 | self.key = key 7 | self.dataType = data 8 | } 9 | 10 | public func callAsFunction(@DirectContentBuilder build: @escaping (D) -> [DirectContent.Partial]) -> SlotContentDefinition { 11 | SlotContentDefinition(slot: self, build: build) 12 | } 13 | 14 | /*public func callAsFunction(@DirectContentBuilder build: @escaping () -> DirectContent) -> SlotContentDefinition where D == Void { 15 | SlotContentDefinition(slot: self, build: { _ in build() }) 16 | }*/ 17 | } 18 | 19 | public protocol AnySlot: AnyObject { 20 | var key: String { get } 21 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/SlotAcceptingWidgetProtocol.swift: -------------------------------------------------------------------------------- 1 | public protocol SlotAcceptingWidgetProtocol: Widget { 2 | var defaultNoDataSlotContentManager: SlotContentManager? { get } 3 | } 4 | 5 | extension SlotAcceptingWidgetProtocol { 6 | public var defaultNoDataSlotContentManager: SlotContentManager? { 7 | nil 8 | } 9 | 10 | func internalWithContent( 11 | buildContent: (Self.Type) -> SlotContent 12 | ) -> Self { 13 | let content = buildContent(Self.self) 14 | 15 | resolveSlotContentWrappers(content) 16 | 17 | var defaultDefinition: SlotContentDefinition? = nil 18 | if let defaultManager = defaultNoDataSlotContentManager { 19 | defaultDefinition = SlotContentDefinition(slot: defaultManager.slot as! Slot) { [unowned content] in 20 | content.directContent 21 | } 22 | 23 | if defaultManager.anyDefinition == nil { 24 | // apply default definition afterward to ensure that it is not overwritten with nil by the resolve logic 25 | defaultManager.anyDefinition = defaultDefinition 26 | } 27 | } 28 | 29 | // accessing content in this closure should capture the content object 30 | // the handler is removed when the widget is destroyed -> content object 31 | // is released 32 | _ = onDestroy(content.onChanged { [weak self] in 33 | self?.resolveSlotContentWrappers(content) 34 | 35 | if let defaultManager = self?.defaultNoDataSlotContentManager, defaultManager.anyDefinition == nil { 36 | defaultManager.anyDefinition = defaultDefinition 37 | } 38 | }) 39 | return self 40 | } 41 | 42 | public func withContent( 43 | @SlotContentBuilder _ buildContent: (Self.Type) -> SlotContent 44 | ) -> Self { 45 | internalWithContent(buildContent: buildContent) 46 | } 47 | 48 | public func withContent( 49 | @SlotContentBuilder _ buildContent: () -> SlotContent 50 | ) -> Self { 51 | internalWithContent(buildContent: { _ in buildContent() }) 52 | } 53 | 54 | fileprivate func resolveSlotContentWrappers(_ content: SlotContent) { 55 | let mirror = Mirror(reflecting: self) 56 | for child in mirror.children { 57 | if let slotContentManager = child.value as? AnySlotContentManager { 58 | slotContentManager.anyDefinition = content.getSlotContentDefinition( 59 | for: slotContentManager.anySlot) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/SlotContentBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | public struct SlotContentBuilder { 3 | public typealias Component = [SlotContent.Partial] 4 | 5 | public static func buildExpression(_ widget: Widget) -> [SlotContent.Partial] { 6 | [.widget(widget)] 7 | } 8 | 9 | public static func buildExpression(_ widgets: [Widget]) -> [SlotContent.Partial] { 10 | widgets.map { .widget($0) } 11 | } 12 | 13 | public static func buildExpression(_ directContent: DirectContent) -> [SlotContent.Partial] { 14 | [.directContent(directContent)] 15 | } 16 | 17 | public static func buildExpression(_ slotContentDefinition: AnySlotContentDefinition) -> [SlotContent.Partial] { 18 | [.slotContentDefinition(slotContentDefinition)] 19 | } 20 | 21 | public static func buildOptional(_ partials: [SlotContent.Partial]?) -> [SlotContent.Partial] { 22 | partials ?? [] 23 | } 24 | 25 | public static func buildExpression(_ dynamicContent: Dynamic) -> [SlotContent.Partial] { 26 | [.dynamic(dynamicContent)] 27 | } 28 | 29 | public static func buildEither(first: [SlotContent.Partial]) -> [SlotContent.Partial] { 30 | return first 31 | } 32 | 33 | public static func buildEither(second: [SlotContent.Partial]) -> [SlotContent.Partial] { 34 | return second 35 | } 36 | 37 | public static func buildArray(_ components: [[SlotContent.Partial]]) -> [SlotContent.Partial] { 38 | components.flatMap { $0 } 39 | } 40 | 41 | public static func buildBlock(_ partials: [SlotContent.Partial]...) -> [SlotContent.Partial] { 42 | partials.flatMap { $0 } 43 | } 44 | 45 | public static func buildFinalResult(_ partials: [SlotContent.Partial]) -> [SlotContent.Partial] { 46 | partials 47 | } 48 | 49 | public static func buildFinalResult(_ partials: [SlotContent.Partial]) -> SlotContent { 50 | SlotContent(partials: partials) 51 | } 52 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/SlotContentDefinition.swift: -------------------------------------------------------------------------------- 1 | public class SlotContentDefinition: AnySlotContentDefinition { 2 | var slot: Slot 3 | public var anySlot: AnySlot { 4 | slot 5 | } 6 | var build: (D) -> [DirectContent.Partial] 7 | 8 | public init(slot: Slot, @DirectContentBuilder build: @escaping (D) -> [DirectContent.Partial]) { 9 | self.slot = slot 10 | let associatedStyleScope = Widget.activeStyleScope 11 | self.build = { data in 12 | Widget.inStyleScope(associatedStyleScope, block: { build(data) }) 13 | } 14 | } 15 | } 16 | 17 | public protocol AnySlotContentDefinition: AnyObject { 18 | var anySlot: AnySlot { get } 19 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Composition/SlotContentManager.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | public class SlotContentManager: AnySlotContentManager, EventfulObject { 4 | public let slot: Slot 5 | public var anySlot: AnySlot { 6 | slot 7 | } 8 | 9 | var anyDefinition: AnySlotContentDefinition? = nil { 10 | didSet { 11 | if oldValue !== anyDefinition { 12 | onDefinitionChanged.invokeHandlers() 13 | } 14 | } 15 | } 16 | var definition: SlotContentDefinition? { 17 | anyDefinition as? SlotContentDefinition 18 | } 19 | 20 | let onDefinitionChanged = EventHandlerManager() 21 | 22 | public init(_ slot: Slot) { 23 | self.slot = slot 24 | } 25 | 26 | public func buildContent(for data: D) -> DirectContent { 27 | let content = DirectContent(partials: []) 28 | if let definition = definition { 29 | content.partials = definition.build(data) 30 | } 31 | _ = content.onDestroy(onDefinitionChanged { [unowned self, unowned content] in 32 | if let definition = definition { 33 | content.partials = definition.build(data) 34 | } else { 35 | content.partials = [] 36 | } 37 | }) 38 | return content 39 | } 40 | 41 | public func buildContent() -> DirectContent where D == Void { 42 | buildContent(for: ()) 43 | } 44 | 45 | @available(*, deprecated, message: "use .buildContent() instead") 46 | public func callAsFunction(_ data: D) -> DirectContent { 47 | buildContent(for: data) 48 | } 49 | 50 | @available(*, deprecated, message: "use .buildContent() instead") 51 | public func callAsFunction() -> DirectContent where D == Void { 52 | callAsFunction(Void()) 53 | } 54 | } 55 | 56 | internal protocol AnySlotContentManager: AnyObject { 57 | var anySlot: AnySlot { get } 58 | var anyDefinition: AnySlotContentDefinition? { get set } 59 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/ContextMenu/ContextMenu+Item.swift: -------------------------------------------------------------------------------- 1 | extension ContextMenu { 2 | public struct Item { 3 | public var title: String 4 | public var action: () -> () 5 | 6 | public init(title: String, action: @escaping () -> ()) { 7 | self.title = title 8 | self.action = action 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/ContextMenu/ContextMenu.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import AppKit 3 | #endif 4 | import GfxMath 5 | 6 | public class ContextMenu { 7 | private let items: [Item] 8 | 9 | #if os(macOS) 10 | // same order as items (necessary for mapping actions) 11 | private var nsMenu: NSMenu? 12 | private var nsMenuItems: [NSMenuItem]? 13 | #endif 14 | 15 | public init(items: [Item]) { 16 | self.items = items 17 | 18 | setup() 19 | } 20 | 21 | private func setup() { 22 | #if os(macOS) 23 | 24 | let menu = NSMenu() 25 | let menuItems: [NSMenuItem] = items.map { 26 | let item = NSMenuItem( 27 | title: $0.title, 28 | action: #selector(onItemAction(_:)), 29 | keyEquivalent: "") 30 | item.isEnabled = true 31 | item.target = self 32 | menu.addItem(item) 33 | return item 34 | } 35 | 36 | self.nsMenu = menu 37 | self.nsMenuItems = menuItems 38 | 39 | #else 40 | fatalError("not implemented") 41 | #endif 42 | } 43 | 44 | public func show(at position: DVec2, in widget: Widget) { 45 | #if os(macOS) 46 | let window = widget.context.getWindow() 47 | let windowPosition = window.bounds.min 48 | let menuPosition = (windowPosition + widget.globalPosition + position) * DVec2(1, -1) + DVec2(0, window.screen.size.y) 49 | 50 | nsMenu?.popUp( 51 | positioning: nil, 52 | at: NSMakePoint(CGFloat(menuPosition.x), CGFloat(menuPosition.y)), 53 | in: nil 54 | ) 55 | #else 56 | fatalError("not implemented") 57 | #endif 58 | } 59 | 60 | #if os(macOS) 61 | @objc private func onItemAction(_ sender: NSMenuItem) { 62 | if let index = nsMenuItems?.index(of: sender) { 63 | items[index].action() 64 | } 65 | } 66 | #endif 67 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/CumulatedValues/CumulatedValuesProcessor.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | class CumulatedValuesProcessor { 4 | var root: Root 5 | 6 | init(_ root: Root) { 7 | self.root = root 8 | } 9 | 10 | func processQueue() { 11 | let queue = root.widgetLifecycleManager.queues[.resolveCumulatedValues]! 12 | 13 | var iterator = queue.iterateSubTreeRoots() 14 | while let next = iterator.next() { 15 | resolveSubTree(rootWidget: next.target) 16 | } 17 | 18 | queue.clear() 19 | } 20 | 21 | func resolveSubTree(rootWidget: Widget) { 22 | var initialParents = [rootWidget] 23 | while let parent = initialParents.last!.parent as? Widget { 24 | initialParents.append(parent) 25 | } 26 | initialParents.reverse() 27 | 28 | var currentTransforms = [DTransform2]() 29 | for parent in initialParents { 30 | currentTransforms.append(contentsOf: getTransforms(parent)) 31 | } 32 | 33 | rootWidget.cumulatedTransforms = currentTransforms 34 | 35 | var queuedIterations = [(rootWidget.children.makeIterator(), currentTransforms)] 36 | 37 | while queuedIterations.count > 0 { 38 | var (iterator, previousTransforms) = queuedIterations.removeFirst() 39 | 40 | while let widget = iterator.next() { 41 | let currentTransforms = previousTransforms + getTransforms(widget) 42 | widget.cumulatedTransforms = currentTransforms 43 | 44 | if widget.children.count > 0 { 45 | // TODO: maybe scroll translation should be added here instead of by accessing parent in getTransforms 46 | queuedIterations.append((widget.children.makeIterator(), currentTransforms)) 47 | } 48 | } 49 | } 50 | } 51 | 52 | func getTransforms(_ widget: Widget) -> [DTransform2] { 53 | var transforms = [DTransform2]() 54 | transforms.append(.translate(widget.layoutedPosition)) 55 | transforms.append(contentsOf: widget.transform) 56 | if let parent = widget.parent as? Widget { 57 | if parent.padding.left != 0 || parent.padding.top != 0 { 58 | transforms.append(.translate(DVec2(parent.padding.left, parent.padding.top))) 59 | } 60 | if !widget.unaffectedByParentScroll, parent.currentScrollOffset != .zero { 61 | transforms.append(.translate(-parent.currentScrollOffset)) 62 | } 63 | } 64 | return transforms 65 | } 66 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/CumulatedValues/Widget+invalidateCumulatedValues.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func invalidateCumulatedValues() { 3 | if !mounted || destroyed { 4 | //print("warning: called invalidateCumulatedValues() on a widget that has not yet been mounted or was already destroyed") 5 | return 6 | } 7 | 8 | context.queueLifecycleMethodInvocation(.resolveCumulatedValues, target: self, sender: self, reason: .undefined) 9 | } 10 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+InspectorView.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | extension DeveloperTools { 4 | public class InspectorView: ComposedWidget { 5 | @Inject var inspectedRoot: Root 6 | @Inject var store: DeveloperTools.Store 7 | 8 | @Compose override public var content: ComposedContent { 9 | Container().withContent { 10 | DeveloperTools.WidgetNestingView(inspectedRoot.rootWidget) 11 | } 12 | } 13 | 14 | override public var style: Style { 15 | Style("&") { 16 | (\.$overflowY, .scroll) 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+MainRoute.swift: -------------------------------------------------------------------------------- 1 | extension DeveloperTools { 2 | public enum MainRoute: String, CaseIterable { 3 | case inspector 4 | case messages 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+MainView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import GfxMath 3 | 4 | extension DeveloperTools { 5 | public class MainView: ComposedWidget { 6 | private let inspectedRoot: Root 7 | 8 | @State private var activeMainRoute: MainRoute = .inspector 9 | 10 | private let store = DeveloperTools.Store() 11 | 12 | public init(_ inspectedRoot: Root) { 13 | self.inspectedRoot = inspectedRoot 14 | super.init() 15 | provide(dependencies: inspectedRoot, store) 16 | } 17 | 18 | @Compose override public var content: ComposedContent { 19 | Container().with(styleProperties: { 20 | (\.$direction, .column) 21 | (\.$alignContent, .stretch) 22 | }).withContent { 23 | buildMenu() 24 | buildActiveView() 25 | } 26 | } 27 | 28 | func buildMenu() -> Widget { 29 | Container().withContent { 30 | MainRoute.allCases.map { 31 | buildMenuItem($0) 32 | } 33 | } 34 | } 35 | 36 | func buildMenuItem(_ route: MainRoute) -> Widget { 37 | Container().with(classes: ["menu-item"]).onClick { [unowned self] in 38 | activeMainRoute = route 39 | }.withContent { 40 | Text(route.rawValue) 41 | } 42 | } 43 | 44 | @DirectContentBuilder func buildActiveView() -> DirectContent { 45 | Dynamic($activeMainRoute.immutable) { [weak self] in 46 | switch self?.activeMainRoute { 47 | case .inspector: 48 | DeveloperTools.InspectorView() 49 | case .messages: 50 | DeveloperTools.MessagesView() 51 | default: 52 | Text("none") 53 | } 54 | } 55 | } 56 | 57 | override public var style: Style { 58 | Style("&") { 59 | (\.$background, theme.backgroundColor) 60 | (\.$foreground, theme.textColorOnBackground) 61 | (\.$height, .rh(100)) 62 | (\.$overflowY, .scroll) 63 | } nested: { 64 | Style(".menu-item") { 65 | (\.$background, theme.primaryColor) 66 | (\.$fontWeight, .bold) 67 | (\.$padding, Insets(all: 16)) 68 | } nested: { 69 | Style("&:hover") { 70 | (\.$background, theme.primaryColor.darkened(30)) 71 | } 72 | } 73 | 74 | theme.styles 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+MessagesView.swift: -------------------------------------------------------------------------------- 1 | extension DeveloperTools { 2 | public class MessagesView: ComposedWidget { 3 | @Inject private var inspectedRoot: Root 4 | 5 | @Compose override public var content: ComposedContent { 6 | Container().withContent { 7 | List(items: inspectedRoot.debugManager.$messages.immutable).withContent { 8 | List.itemSlot { item in 9 | Container().withContent { 10 | Text(item.message) 11 | Text(String(describing: item.sender)) 12 | }.onMouseEnter { 13 | item.sender.debugHighlight = true 14 | }.onMouseLeave { 15 | item.sender.debugHighlight = false 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+Store.swift: -------------------------------------------------------------------------------- 1 | extension DeveloperTools { 2 | public class Store: WidgetGUI.Store { 3 | public init() { 4 | super.init(initialState: State()) 5 | } 6 | 7 | override public func perform(mutation: Mutation, state: SetterProxy) { 8 | switch mutation { 9 | case let .setActiveMainRoute(route): 10 | state.activateMainRoute = route 11 | case let .setInspectedWidget(widget): 12 | state.inspectedWidget = widget 13 | } 14 | } 15 | 16 | public struct State { 17 | public var activateMainRoute: MainRoute = .inspector 18 | public var inspectedWidget: Widget? = nil 19 | } 20 | 21 | public enum Mutation { 22 | case setActiveMainRoute(MainRoute) 23 | case setInspectedWidget(Widget) 24 | } 25 | 26 | public enum Action { 27 | 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+Theme.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | extension DeveloperTools { 4 | static let theme = FlatTheme( 5 | primaryColor: .red, secondaryColor: .lightBlue, backgroundColor: Color(40, 50, 80, 255)) 6 | } 7 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools+WidgetNestingView.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | import Events 3 | 4 | extension DeveloperTools { 5 | public class WidgetNestingView: ComposedWidget { 6 | @Inject var store: DeveloperTools.Store 7 | 8 | private let inspectedWidget: Widget 9 | private let depth: Int 10 | @State private var expanded = false 11 | 12 | public init(_ inspectedWidget: Widget, depth: Int = 0) { 13 | self.inspectedWidget = inspectedWidget 14 | self.depth = depth 15 | if depth < 10 { 16 | self.expanded = true 17 | } 18 | super.init() 19 | } 20 | 21 | @Compose override public var content: ComposedContent { 22 | Container().with(styleProperties: { 23 | (\.$direction, .column) 24 | }).withContent { 25 | 26 | Container().with(classes: ["info-container"]).with(styleProperties: { 27 | (\.$alignContent, .center) 28 | }).withContent { _ in 29 | 30 | MaterialDesignIcon(.menuDown).with(classes: ["expand-icon"]).onClick { [unowned self] in 31 | expanded = !expanded 32 | } 33 | 34 | Text("\(String(describing: inspectedWidget))").with(classes: ["description-text"]).onClick { [unowned self] in 35 | store.commit(.setInspectedWidget(inspectedWidget)) 36 | } 37 | } 38 | 39 | Container().with(styleProperties: { 40 | (\.$direction, .column) 41 | (\.$padding, Insets(left: 16)) 42 | }).withContent { 43 | Dynamic($expanded.publisher) { [unowned self] in 44 | if expanded { 45 | inspectedWidget.children.map { [unowned self] in 46 | WidgetNestingView($0, depth: depth + 1) 47 | } 48 | } else { 49 | Space(.zero) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | override public var style: Style { 57 | Style("&") {} nested: { 58 | Style(".info-container") {} nested: { 59 | Style("&:hover") { 60 | (\.$background, theme.backgroundColor.darkened(10)) 61 | } 62 | } 63 | 64 | Style(".expand-icon") { 65 | (\.$foreground, .white) 66 | (\.$fontSize, 24.0) 67 | (\.$padding, Insets(left: 16)) 68 | } 69 | 70 | Style(".description-text") { 71 | (\.$foreground, theme.textColorOnBackground) 72 | (\.$padding, Insets(all: 16)) 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/DeveloperTools/DeveloperTools.swift: -------------------------------------------------------------------------------- 1 | public enum DeveloperTools {} -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/LeafWidget.swift: -------------------------------------------------------------------------------- 1 | import Drawing 2 | import SkiaKit 3 | 4 | open class LeafWidget: Widget { 5 | open func draw(_ drawingContext: DrawingContext) { 6 | fatalError("draw() not implemented for widget: \(self)") 7 | } 8 | 9 | open func draw(_ drawingContext: DrawingContext, canvas: Canvas) { 10 | draw(drawingContext) 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Canvas.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension Canvas { 5 | public func drawPoint(_ point: DVec2, paint: Paint) { 6 | drawPoint(Float(point.x), Float(point.y), paint) 7 | } 8 | 9 | public func drawPoint(_ point: DVec2, color: GfxMath.Color) { 10 | drawPoint(point, paint: Paint.fill(color: color)) 11 | } 12 | 13 | public func drawLine(from start: DVec2, to end: DVec2, paint: Paint) { 14 | drawLine(x0: Float(start.x), y0: Float(start.y), x1: Float(end.x), y1: Float(end.y), paint: paint) 15 | } 16 | 17 | public func drawRect(_ rect: FRect, _ paint: Paint) { 18 | drawRect(SkiaKit.Rect(rect), paint) 19 | } 20 | 21 | public func drawRect(_ rect: DRect, _ paint: Paint) { 22 | drawRect(FRect(rect), paint) 23 | } 24 | 25 | public func drawCircle(center: FVec2, radius: Float, paint: Paint) { 26 | drawCircle(center.x, center.y, radius, paint) 27 | } 28 | 29 | public func drawCircle(center: DVec2, radius: Double, paint: Paint) { 30 | drawCircle(Float(center.x), Float(center.y), Float(radius), paint) 31 | } 32 | 33 | public func scale(x: Float, y: Float, pivot: FVec2) { 34 | scale(sx: x, sy: y, pivot: SkiaKit.Point(pivot)) 35 | } 36 | 37 | public func clip(rect: DRect) { 38 | clip(rect: Rect(FRect(min: FVec2(rect.min), size: FSize2(rect.size)))) 39 | } 40 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Color.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension SkiaKit.Color { 5 | public init(_ color: GfxMath.Color) { 6 | self.init(r: color.r, g: color.g, b: color.b, a: color.a) 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Font.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension SkiaKit.Font { 5 | public func measureText(_ text: String, paint: Paint) -> DRect { 6 | measureText(text, paint: paint).asDRect() 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Image+initFromSwimImage.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import Swim 3 | 4 | extension SkiaKit.Image { 5 | public convenience init?(_ swimImage: Swim.Image) { 6 | let skiaImageInfo = ImageInfo( 7 | width: Int32(swimImage.width), 8 | height: Int32(swimImage.height), 9 | colorType: .rgba8888, 10 | alphaType: .unpremul) 11 | 12 | let imageData = swimImage.getData() 13 | let drawableImageDataPointer = UnsafeMutablePointer.allocate(capacity: imageData.count) 14 | drawableImageDataPointer.initialize(from: imageData, count: imageData.count) 15 | 16 | let skiaPixmap = Pixmap(info: skiaImageInfo, addr: UnsafeMutableRawPointer(drawableImageDataPointer)) 17 | self.init(pixmap: skiaPixmap, releaseProc: { addr, _ in 18 | addr?.deallocate() 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Paint.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension Paint { 5 | public convenience init(color: GfxMath.Color, style: Paint.Style, isAntialias: Bool = true) { 6 | self.init() 7 | self.color = Color(color) 8 | self.style = style 9 | self.isAntialias = isAntialias 10 | } 11 | 12 | public static func fill(color: GfxMath.Color) -> Paint { 13 | var paint = Paint() 14 | paint.style = .fill 15 | paint.color = Color(color) 16 | paint.isAntialias = true 17 | return paint 18 | } 19 | 20 | public static func stroke(color: GfxMath.Color, width: Double) -> Paint { 21 | var paint = Paint() 22 | paint.style = .stroke 23 | paint.color = Color(color) 24 | paint.strokeWidth = Float(width) 25 | return paint 26 | } 27 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Path.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension Path { 5 | public func move(to point: FVec2) { 6 | moveTo(point.x, point.y) 7 | } 8 | 9 | public func line(to point: FVec2) { 10 | lineTo(point.x, point.y) 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Point.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension SkiaKit.Point { 5 | public func asDVec2() -> DVec2 { 6 | DVec2(Double(x), Double(y)) 7 | } 8 | 9 | public init(_ point: FVec2) { 10 | self.init(x: point.x, y: point.y) 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Drawing/SkiaKitExtensions/Rect.swift: -------------------------------------------------------------------------------- 1 | import SkiaKit 2 | import GfxMath 3 | 4 | extension SkiaKit.Rect { 5 | public func asDRect() -> DRect { 6 | DRect(center: DVec2(Double(midX), Double(midY)), size: DSize2(Double(width), Double(height))) 7 | } 8 | 9 | public init(_ rect: FRect) { 10 | self.init(x: rect.min.x, y: rect.min.y, width: rect.size.width, height: rect.size.height) 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Focus/FocusManager.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public final class FocusManager { 4 | var currentFocusChain: [Widget] = [] 5 | 6 | public func requestFocus(on widget: Widget) { 7 | let newChain = focusParentChain(leaf: widget) 8 | for index in 0..= newChain.count || currentFocusChain[index] !== newChain[index] { 10 | currentFocusChain[index].internalFocused = false 11 | } 12 | } 13 | currentFocusChain = newChain 14 | } 15 | 16 | public func dropFocus(on widget: Widget) { 17 | var unfocusChain = [Widget]() 18 | for (index, focusedWidget) in currentFocusChain.enumerated() { 19 | if focusedWidget === widget { 20 | unfocusChain = Array(currentFocusChain[index.. [Widget] { 31 | var chain = [Widget]() 32 | var next = Optional(leaf) 33 | while let current = next { 34 | current.internalFocused = true 35 | next = current.parent as? Widget 36 | chain.append(current) 37 | } 38 | return chain.reversed() 39 | } 40 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Focus/Widget+Focusable.swift: -------------------------------------------------------------------------------- 1 | public protocol FocusableWidget: Widget { 2 | } 3 | 4 | extension FocusableWidget { 5 | public func requestFocus() { 6 | if !mounted || destroyed { 7 | //print("warning: called requestFocus() on a widget that has not yet been mounted or was already destroyed") 8 | } else { 9 | context.focusManager.requestFocus(on: self) 10 | } 11 | } 12 | 13 | public func dropFocus() { 14 | if !mounted || destroyed { 15 | //print("warning: called dropFocus() on a widget that has not yet been mounted or was already destroyed") 16 | } else { 17 | context.focusManager.dropFocus(on: self) 18 | } 19 | } 20 | } 21 | 22 | extension Widget: FocusableWidget {} -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/BurstLimiter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | 4 | public class BurstLimiter { 5 | 6 | @usableFromInline internal let minDelay: Double 7 | 8 | @usableFromInline internal var lastInvocationTimestamp: Double = 0 9 | 10 | @usableFromInline internal var delayedWorkItem: DispatchWorkItem? = nil 11 | 12 | public init(minDelay: Double) { 13 | 14 | self.minDelay = minDelay 15 | } 16 | 17 | @inlinable public final func limit(_ block: @escaping () -> ()) { 18 | 19 | let currentTimestamp = Date.timeIntervalSinceReferenceDate 20 | 21 | let currentDelay = currentTimestamp - lastInvocationTimestamp 22 | 23 | if currentDelay >= minDelay { 24 | 25 | lastInvocationTimestamp = currentTimestamp 26 | 27 | block() 28 | 29 | } else { 30 | 31 | let remainingDelay = minDelay - currentDelay 32 | 33 | if let currentWorkItem = delayedWorkItem { 34 | 35 | currentWorkItem.cancel() 36 | } 37 | 38 | delayedWorkItem = DispatchWorkItem { [weak self] in 39 | 40 | self?.limit(block) 41 | } 42 | 43 | DispatchQueue.main.asyncAfter(deadline: .now() + remainingDelay, execute: delayedWorkItem!) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/DefinitiveDict.swift: -------------------------------------------------------------------------------- 1 | /// A Dictionary where the key access is guaranteed to always return a non-optional value (or fail with an error). 2 | public struct DefinitiveDictionary { 3 | 4 | private var sourceDict: Dictionary 5 | 6 | public init(_ sourceDict: Dictionary) { 7 | 8 | self.sourceDict = sourceDict 9 | } 10 | 11 | public subscript(_ key: K) -> V { 12 | 13 | get { 14 | 15 | sourceDict[key]! 16 | } 17 | 18 | mutating set { 19 | 20 | sourceDict[key] = newValue 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/Reference.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public protocol AnyReferenceProtocol { 4 | var anyReferenced: Widget? { get set } 5 | } 6 | 7 | @propertyWrapper 8 | public class Reference: AnyReferenceProtocol, Publisher { 9 | public typealias Output = ReferencedWidget? 10 | public typealias Failure = Never 11 | 12 | public unowned var anyReferenced: Widget? { 13 | get { referenced } 14 | set { 15 | if newValue == nil { 16 | referenced = nil 17 | } else { 18 | referenced = newValue as! ReferencedWidget 19 | } 20 | } 21 | } 22 | 23 | public unowned var referenced: ReferencedWidget? { 24 | didSet { 25 | publisher.send(referenced) 26 | } 27 | } 28 | 29 | public var wrappedValue: ReferencedWidget { 30 | get { 31 | return referenced as! ReferencedWidget 32 | } 33 | set { 34 | referenced = newValue 35 | } 36 | } 37 | 38 | public var projectedValue: Reference { 39 | get { 40 | self 41 | } 42 | } 43 | 44 | public var publisher = PassthroughSubject() 45 | 46 | public func receive( 47 | subscriber: S 48 | ) where S.Input == Output, S.Failure == Failure { 49 | publisher.receive(subscriber: subscriber) 50 | } 51 | 52 | public init() {} 53 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/Tree/Mirror+allChildren.swift: -------------------------------------------------------------------------------- 1 | extension Mirror { 2 | public var allChildren: [Mirror.Child] { 3 | var allChildren: [Mirror.Child] = [] 4 | var mirror: Mirror! = self 5 | repeat { 6 | allChildren.append(contentsOf: mirror.children) 7 | mirror = mirror.superclassMirror 8 | } while mirror != nil 9 | return allChildren 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/Tree/Tree.swift: -------------------------------------------------------------------------------- 1 | public protocol TreeNode { 2 | associatedtype Child: TreeNode 3 | var children: [Child] { get } 4 | var isBranching: Bool { get } 5 | } 6 | 7 | public extension TreeNode { 8 | var isBranching: Bool { 9 | false 10 | } 11 | } 12 | 13 | /* 14 | public protocol ProtoTreeNodeImpl: TreeNode { 15 | } 16 | 17 | public struct ImplTreeNode: ProtoTreeNodeImpl where Child: ProtoTreeNodeImpl { 18 | public var children: [Child] 19 | public var isBranching: Bool { false } 20 | } 21 | 22 | public struct ImplTreeNode2: ProtoTreeNodeImpl { 23 | public var children: [Child] 24 | public var isBranching: Bool { false } 25 | 26 | public init() { 27 | self.children = [] 28 | self.children.append(ImplTreeNode()) 29 | } 30 | 31 | } 32 | */ 33 | /*public protocol TreeNode { 34 | } 35 | 36 | 37 | public protocol LeafTreeNode: TreeNode { 38 | } 39 | 40 | public extension LeafTreeNode { 41 | } 42 | 43 | public protocol BranchingTreeNodeMarker: TreeNode { 44 | 45 | } 46 | 47 | public protocol BranchingTreeNode: BranchingTreeNodeMarker { 48 | associatedtype Child: TreeNode 49 | var children: [Child] { get set } 50 | 51 | func otherBranches() -> [Child] 52 | } 53 | 54 | public extension BranchingTreeNode { 55 | func otherBranches() -> [Child] { 56 | var result = [Child]() 57 | for child in children { 58 | if child is BranchingTreeNodeMarker { 59 | result.append(child) 60 | } 61 | } 62 | return result 63 | } 64 | }*/ 65 | /* 66 | 67 | public class CoolTreeNodeTest: TreeNode { 68 | public var children: [CoolTreeNodeTest] = [] 69 | } 70 | 71 | public class CoolLeafNodeTest: CoolTreeNodeTest, LeafTreeNode { 72 | 73 | }*/ 74 | 75 | /*public protocol TreeNode { 76 | 77 | 78 | } 79 | 80 | public protocol BranchingTreeNode: TreeNode { 81 | associatedtype Node: TreeNode 82 | var children: [Node] { get set } 83 | } 84 | 85 | public protocol Tree: BranchingTreeNode { 86 | associatedtype BranchingNode: BranchingTreeNode 87 | } 88 | 89 | public struct AnyTree: Tree { 90 | public typealias Child = Node 91 | public var children: [Node] 92 | }*/ 93 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/Tree/TreeRange.swift: -------------------------------------------------------------------------------- 1 | public struct TreeRange: Hashable { 2 | public var start: TreePath 3 | public var end: TreePath 4 | 5 | public init(from start: TreePath, to end: TreePath) { 6 | self.start = start 7 | self.end = end 8 | } 9 | 10 | public init() { 11 | self.start = TreePath() 12 | self.end = TreePath() 13 | } 14 | 15 | public func contains(_ path: TreePath) -> Bool { 16 | var maxCompareCount = min(path.count, start.count) 17 | for i in 0.. start[i] { 19 | break 20 | } else if path[i] < start[i] { 21 | return false 22 | }/* else if path[i] == start[i] && i == maxCompareCount - 1 && path.count > start.count { 23 | return false 24 | }*/ 25 | } 26 | maxCompareCount = min(path.count, end.count) 27 | for i in 0.. end[i] { 31 | return false 32 | }/* else if path[i] == start[i] && i == maxCompareCount - 1 && path.count > end.count { 33 | return false 34 | }*/ 35 | } 36 | return true 37 | } 38 | 39 | /*public mutating func add(_ path: TreePath) { 40 | 41 | }*/ 42 | // TODO: implement merging with TreeRangeSet 43 | /*public func merged(_ other: TreeRange) { 44 | var result = self 45 | if other.start < self.start { 46 | result.start = other.start 47 | } 48 | if other.end > self.end { 49 | result.end = other.end 50 | } 51 | return result 52 | }*/ 53 | public mutating func extend(with path: TreePath) { 54 | if path < start { 55 | self.start = path 56 | } 57 | if path > end { 58 | self.end = path 59 | } 60 | } 61 | 62 | public func extended(with path: TreePath) -> Self { 63 | var result = self 64 | result.extend(with: path) 65 | return result 66 | } 67 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/WeakBox.swift: -------------------------------------------------------------------------------- 1 | @dynamicMemberLookup 2 | internal final class WeakBox { 3 | weak public var wrapped: T? 4 | 5 | init(_ wrapped: T) { 6 | self.wrapped = wrapped 7 | } 8 | 9 | subscript(dynamicMember keyPath: KeyPath) -> V? { 10 | wrapped?[keyPath: keyPath] 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/WidgetBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | public struct WidgetBuilder { 3 | public static func buildBlock() -> [Widget] { 4 | return [] 5 | } 6 | 7 | public static func buildBlock(_ widget: Widget) -> Widget { 8 | return widget 9 | } 10 | 11 | public static func buildExpression(_ widget: Widget) -> Widget { 12 | return widget 13 | } 14 | 15 | public static func buildExpression(_ widget: Widget) -> [Widget] { 16 | return [widget] 17 | } 18 | 19 | public static func buildExpression(_ widgets: [Widget]) -> [Widget] { 20 | return widgets 21 | } 22 | 23 | public static func buildBlock(_ widget: Widget) -> [Widget] { 24 | return [widget] 25 | } 26 | 27 | public static func buildBlock(_ widgets: Widget?...) -> [Widget] { 28 | return widgets.compactMap { $0 } 29 | } 30 | 31 | public static func buildBlock(_ widgets: [Widget?]) -> [Widget] { 32 | return widgets.compactMap { $0 } 33 | } 34 | 35 | public static func buildBlock(_ widgets: [Widget?]...) -> [Widget] { 36 | return widgets.flatMap { $0 }.compactMap { $0 } 37 | } 38 | 39 | /*public static func buildBlock(_ widget: Widget, _ widgets2: [Widget?]) -> [Widget] { 40 | return [widget] + widgets2.compactMap { $0 } 41 | } */ 42 | 43 | public static func buildOptional(_ widget: Widget?) -> Widget? { 44 | return widget 45 | } 46 | 47 | public static func buildOptional(_ widget: Widget?) -> [Widget] { 48 | return widget != nil ? [widget!] : [] 49 | } 50 | 51 | 52 | public static func buildEither(first: Widget) -> Widget { 53 | return first 54 | } 55 | 56 | /*public static func buildEither(first: Widget) -> [Widget] { 57 | return [first] 58 | }*/ 59 | 60 | public static func buildEither(first: [Widget]) -> [Widget] { 61 | return first 62 | } 63 | 64 | 65 | 66 | public static func buildEither(second: Widget) -> Widget { 67 | return second 68 | } 69 | 70 | /*public static func buildEither(second: Widget) -> [Widget] { 71 | return [second] 72 | }*/ 73 | 74 | public static func buildEither(second: [Widget]) -> [Widget] { 75 | return second 76 | } 77 | 78 | 79 | /*public static func buildBlock(_ widgets: [Widget?]...) -> [Widget] { 80 | return widgets.flatMap { $0 }.compactMap { $0 } 81 | }*/ 82 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Helpers/WidgetEventHandlerManager.swift: -------------------------------------------------------------------------------- 1 | import Events 2 | 3 | internal protocol AnyWidgetEventHandlerManager { 4 | var widget: Widget? { get set } 5 | 6 | func removeAllHandlers() 7 | } 8 | 9 | public class WidgetEventHandlerManager: AnyWidgetEventHandlerManager { 10 | public typealias Handler = (Data) -> Void 11 | public typealias UnregisterCallback = () -> Void 12 | 13 | public var handlers = [Int: Handler]() 14 | private var nextHandlerId = 0 15 | 16 | 17 | public init() {} 18 | 19 | deinit { 20 | removeAllHandlers() 21 | } 22 | 23 | /*public func callAsFunction(_ handler: @escaping Handler) -> UnregisterCallback { 24 | addHandler(handler) 25 | }*/ 26 | 27 | internal var widget: Widget? = nil 28 | 29 | public func chain(_ handler: @escaping Handler) -> Widget { 30 | _ = addHandler(handler) 31 | 32 | return widget! 33 | } 34 | 35 | public func callAsFunction(_ handler: @escaping () -> ()) -> Widget { 36 | _ = addHandler({ _ in handler() }) 37 | return widget! 38 | } 39 | 40 | public func callAsFunction(_ handler: @escaping Handler) -> Widget { 41 | _ = addHandler(handler) 42 | return widget! 43 | } 44 | 45 | // TODO: implement function to add to start of handler list 46 | public func addHandler(_ handler: @escaping Handler) -> UnregisterCallback { 47 | let currentHandlerId = nextHandlerId 48 | handlers[currentHandlerId] = handler 49 | nextHandlerId += 1 50 | 51 | return { 52 | self.handlers.removeValue(forKey: currentHandlerId) 53 | } 54 | } 55 | 56 | public func once(_ handler: @escaping Handler) { 57 | var unregisterCallback: UnregisterCallback? = nil 58 | let wrapperHandler = { (data: Data) in 59 | handler(data) 60 | 61 | if let unregister = unregisterCallback { 62 | unregister() 63 | } 64 | } 65 | 66 | unregisterCallback = addHandler(wrapperHandler) 67 | } 68 | 69 | public func invokeHandlers(_ data: Data) { 70 | // TODO: call handlers in same order as they were added 71 | for handler in handlers.values { 72 | handler(data) 73 | } 74 | } 75 | 76 | public func removeAllHandlers() { 77 | handlers.removeAll() 78 | } 79 | } 80 | 81 | extension WidgetEventHandlerManager where Data == Void { 82 | public func invokeHandlers() { 83 | invokeHandlers(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/Cursor.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | // 4 | 5 | import Foundation 6 | 7 | public enum Cursor { 8 | case Arrow, Hand, Text 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/Key.swift: -------------------------------------------------------------------------------- 1 | /// representing keys on a standard american keyboard 2 | /// which means: Key.y will represent the Key that gives the letter z on german keyboards 3 | /// prefix _ for number keys, e.g. 0 --> _0 4 | public enum Key: CaseIterable { 5 | 6 | /*case ArrowUp, ArrowRight, ArrowDown, ArrowLeft 7 | 8 | case Return, Enter, Backspace, Delete, Space, Escape 9 | 10 | case LeftShift, LeftCtrl, LeftAlt 11 | 12 | case Plus, Minus 13 | 14 | case N0, N1, N2, N3, N4, N5, N6, N7, N8, N9 15 | 16 | case A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z 17 | 18 | case F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12*/ 19 | 20 | case arrowUp, arrowRight, arrowDown, arrowLeft 21 | case leftCtrl, rightCtrl, leftGui, rightGui 22 | case `return`, enter, backspace, delete, space, escape 23 | case _0, _1, _2, _3, _4, _5, _6, _7, _8, _9 24 | case plus, minus 25 | case a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z 26 | case f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12 27 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/KeyStatesContainer.swift: -------------------------------------------------------------------------------- 1 | public struct KeyStatesContainer { 2 | private var states: [Key: Bool] = Key.allCases.reduce(into: [Key: Bool]()) { 3 | $0[$1] = false 4 | } 5 | 6 | public init() {} 7 | 8 | public subscript(_ key: Key) -> Bool { 9 | get { 10 | return states[key]! 11 | } 12 | set { 13 | states[key] = newValue 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/MouseButton.swift: -------------------------------------------------------------------------------- 1 | public enum MouseButton { 2 | case Left, Right 3 | } 4 | 5 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/RawEvents/RawKeyboardEvent.swift: -------------------------------------------------------------------------------- 1 | public protocol RawKeyboardEvent { 2 | /// true if key is down, false if not 3 | var keyStates: KeyStatesContainer { get } 4 | var key: Key { get } 5 | var repetition: Bool { get } 6 | } 7 | 8 | public struct RawKeyDownEvent: RawKeyboardEvent { 9 | public var keyStates: KeyStatesContainer 10 | public var key: Key 11 | public var repetition: Bool 12 | 13 | public init(key: Key, keyStates: KeyStatesContainer, repetition: Bool = false) { 14 | self.keyStates = keyStates 15 | self.key = key 16 | self.repetition = repetition 17 | } 18 | } 19 | 20 | public struct RawKeyUpEvent: RawKeyboardEvent { 21 | public var keyStates: KeyStatesContainer 22 | public var key: Key 23 | public var repetition: Bool 24 | 25 | public init(key: Key, keyStates: KeyStatesContainer, repetition: Bool = false) { 26 | self.keyStates = keyStates 27 | self.key = key 28 | self.repetition = repetition 29 | } 30 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/RawEvents/RawMouseEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import GfxMath 3 | 4 | // TODO: maybe remove the "Raw" again and only prefix GUI for the widget events.. 5 | public protocol RawMouseEvent { 6 | var position: DPoint2 { get set } 7 | } 8 | 9 | public struct RawMouseButtonUpEvent: RawMouseEvent { 10 | public var button: MouseButton 11 | public var position: DPoint2 12 | 13 | public init(button: MouseButton, position: DPoint2) { 14 | self.button = button 15 | self.position = position 16 | } 17 | } 18 | 19 | public struct RawMouseButtonDownEvent: RawMouseEvent { 20 | public var button: MouseButton 21 | public var position: DPoint2 22 | 23 | public init(button: MouseButton, position: DPoint2) { 24 | self.button = button 25 | self.position = position 26 | } 27 | } 28 | 29 | public struct RawMouseWheelEvent: RawMouseEvent { 30 | public var scrollAmount: DVec2 31 | public var position: DPoint2 32 | 33 | public init(scrollAmount: DVec2, position: DPoint2) { 34 | self.scrollAmount = scrollAmount 35 | self.position = position 36 | } 37 | } 38 | 39 | public struct RawMouseMoveEvent: RawMouseEvent { 40 | public var position: DPoint2 41 | public var previousPosition: DPoint2 42 | public var move: DVec2 { 43 | get { 44 | DVec2(position.x - previousPosition.x, position.y - previousPosition.y) 45 | } 46 | } 47 | 48 | public init(position: DPoint2, previousPosition: DPoint2) { 49 | self.position = position 50 | self.previousPosition = previousPosition 51 | } 52 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/RawEvents/RawTextInputEvent.swift: -------------------------------------------------------------------------------- 1 | public struct RawTextInputEvent { 2 | public var text: String 3 | 4 | public init(_ text: String) { 5 | self.text = text 6 | } 7 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/Root+KeyboardEventManager.swift: -------------------------------------------------------------------------------- 1 | extension Root { 2 | class KeyboardEventManager { 3 | unowned let root: Root 4 | 5 | init(root: Root) { 6 | self.root = root 7 | } 8 | 9 | func process(event rawEvent: RawKeyboardEvent) { 10 | var next = Optional(root.rootWidget) 11 | while let current = next { 12 | switch rawEvent { 13 | case let rawEvent as RawKeyDownEvent: 14 | current.processKeyboardEvent(GUIKeyDownEvent( 15 | key: rawEvent.key, 16 | keyStates: rawEvent.keyStates, 17 | repetition: rawEvent.repetition)) 18 | case let rawEvent as RawKeyUpEvent: 19 | current.processKeyboardEvent(GUIKeyUpEvent( 20 | key: rawEvent.key, 21 | keyStates: rawEvent.keyStates, 22 | repetition: rawEvent.repetition)) 23 | default: 24 | fatalError("unsupported RawKeyboardEvent: \(rawEvent)") 25 | } 26 | 27 | next = nil 28 | for child in current.children { 29 | if child.focused { 30 | next = child 31 | break 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/Root+TextInputEventManager.swift: -------------------------------------------------------------------------------- 1 | extension Root { 2 | class TextInputEventManager { 3 | unowned let root: Root 4 | 5 | init(root: Root) { 6 | self.root = root 7 | } 8 | 9 | func process(event: RawTextInputEvent) { 10 | var next = Optional(root.rootWidget) 11 | while let current = next { 12 | current.processTextEvent(GUITextInputEvent(event.text)) 13 | 14 | next = nil 15 | for child in current.children { 16 | if child.focused { 17 | next = child 18 | break 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/Widget+internalProcessInputEvents.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | internal func processMouseEvent(_ event: GUIMouseEvent) { 3 | switch event { 4 | case let event as GUIMouseEnterEvent: 5 | self.enablePseudoClass(Widget.PseudoClasses.hover) 6 | self.onMouseEnterHandlerManager.invokeHandlers(event) 7 | 8 | case let event as GUIMouseMoveEvent: 9 | self.onMouseMoveHandlerManager.invokeHandlers(event) 10 | 11 | case let event as GUIMouseLeaveEvent: 12 | self.disablePseudoClass(Widget.PseudoClasses.hover) 13 | self.onMouseLeaveHandlerManager.invokeHandlers(event) 14 | 15 | case let event as GUIMouseButtonDownEvent: 16 | self.enablePseudoClass(Widget.PseudoClasses.active) 17 | self.onMouseDownHandlerManager.invokeHandlers(event) 18 | 19 | case let event as GUIMouseButtonUpEvent: 20 | self.disablePseudoClass(Widget.PseudoClasses.active) 21 | self.onMouseUpHandlerManager.invokeHandlers(event) 22 | 23 | case let event as GUIMouseButtonClickEvent: 24 | self.onClickHandlerManager.invokeHandlers(event) 25 | 26 | case let event as GUIMouseWheelEvent: 27 | self.onMouseWheelHandlerManager.invokeHandlers(event) 28 | 29 | default: 30 | fatalError("not implemented event type \(event)") 31 | } 32 | } 33 | 34 | internal func processKeyboardEvent(_ event: GUIKeyboardEvent) { 35 | switch event { 36 | case let event as GUIKeyDownEvent: 37 | self.onKeyDownHandlerManager.invokeHandlers(event) 38 | 39 | case let event as GUIKeyUpEvent: 40 | self.onKeyUpHandlerManager.invokeHandlers(event) 41 | 42 | default: 43 | fatalError("not implemented event type \(event)") 44 | } 45 | } 46 | 47 | internal func processTextEvent(_ event: GUITextEvent) { 48 | switch event { 49 | case let event as GUITextInputEvent: 50 | self.onTextInputHandlerManager.invokeHandlers(event) 51 | 52 | default: 53 | fatalError("not implemented event type \(event)") 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/WidgetEvents/GUIKeyboardEvent.swift: -------------------------------------------------------------------------------- 1 | 2 | public protocol GUIKeyboardEvent { 3 | /// true if key is down, false if not 4 | var keyStates: KeyStatesContainer { get } 5 | var key: Key { get } 6 | var repetition: Bool { get } 7 | } 8 | 9 | extension GUIKeyboardEvent { 10 | // whether any of the ctrl keys was pressed when the event was fired 11 | var haveCtrl: Bool { 12 | keyStates[.leftCtrl] || keyStates[.rightCtrl] 13 | } 14 | 15 | // whether any of the gui keys is pressed (CMD on MacOS) 16 | var haveGui: Bool { 17 | keyStates[.leftGui] || keyStates[.rightGui] 18 | } 19 | } 20 | 21 | public struct GUIKeyDownEvent: GUIKeyboardEvent { 22 | public var keyStates: KeyStatesContainer 23 | public var key: Key 24 | public var repetition: Bool 25 | 26 | public init(key: Key, keyStates: KeyStatesContainer, repetition: Bool = false) { 27 | self.keyStates = keyStates 28 | self.key = key 29 | self.repetition = repetition 30 | } 31 | } 32 | 33 | public struct GUIKeyUpEvent: GUIKeyboardEvent { 34 | public var keyStates: KeyStatesContainer 35 | public var key: Key 36 | public var repetition: Bool 37 | 38 | public init(key: Key, keyStates: KeyStatesContainer, repetition: Bool = false) { 39 | self.keyStates = keyStates 40 | self.key = key 41 | self.repetition = repetition 42 | } 43 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Input/WidgetEvents/GUITextEvent.swift: -------------------------------------------------------------------------------- 1 | public protocol GUITextEvent { 2 | var text: String { get } 3 | } 4 | 5 | public struct GUITextInputEvent: GUITextEvent { 6 | public var text: String 7 | 8 | public init(_ text: String) { 9 | self.text = text 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Layouts/AbsoluteLayout.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public class AbsoluteLayout: Layout { 4 | override public func layout(constraints: BoxConstraints) -> DSize2 { 5 | var maxSize = DSize2.zero 6 | for widget in widgets { 7 | let childConstraints = BoxConstraints(minSize: .zero, maxSize: constraints.maxSize) 8 | widget.layout(constraints: childConstraints) 9 | if widget.layoutedSize.width > maxSize.width { 10 | maxSize.width = widget.layoutedSize.width 11 | } 12 | if widget.layoutedSize.height > maxSize.height { 13 | maxSize.height = widget.layoutedSize.height 14 | } 15 | } 16 | return maxSize 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Layouts/FlexLayout.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public class FlexLayout: Layout { 4 | //@LayoutProperty(\.$direction) 5 | var direction: Direction = .row 6 | 7 | override public func layout(constraints: BoxConstraints) -> DSize2 { 8 | let primaryAxisIndex: Int 9 | let secondaryAxisIndex: Int 10 | switch direction { 11 | case .row: 12 | primaryAxisIndex = 0 13 | secondaryAxisIndex = 1 14 | case .column: 15 | primaryAxisIndex = 1 16 | secondaryAxisIndex = 0 17 | } 18 | 19 | var maxSize = DSize2.zero 20 | for widget in widgets { 21 | var widgetConstraints = constraints 22 | /*if widget.stylePropertyValue(ChildKeys.alignSelf, as: FlexAlign.self) == .stretch { 23 | widgetConstraints.minSize.width = maxSize.width 24 | }*/ 25 | widget.layout(constraints: widgetConstraints) 26 | 27 | var widgetPosition = DVec2.zero 28 | widgetPosition[primaryAxisIndex] = maxSize[primaryAxisIndex] 29 | widgetPosition[secondaryAxisIndex] = 0 30 | widget.layoutedPosition = widgetPosition 31 | 32 | maxSize[primaryAxisIndex] += widget.layoutedSize[primaryAxisIndex] 33 | if widget.layoutedSize[secondaryAxisIndex] > maxSize[secondaryAxisIndex] { 34 | maxSize[secondaryAxisIndex] = widget.layoutedSize[secondaryAxisIndex] 35 | } 36 | } 37 | return maxSize 38 | } 39 | 40 | public enum Direction { 41 | case row, column 42 | } 43 | 44 | public enum FlexAlign { 45 | case start, stretch 46 | } 47 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Layouts/Layout.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | open class Layout { 4 | unowned public internal(set) var container: Container 5 | public internal(set) var widgets: [Widget] { 6 | didSet { 7 | setupChildrenPropertySubscription() 8 | } 9 | } 10 | 11 | required public init(container: Container, widgets: [Widget]) { 12 | self.container = container 13 | self.widgets = widgets 14 | 15 | let mirror = Mirror(reflecting: self) 16 | for child in mirror.allChildren { 17 | if child.label == "widgets" { 18 | continue 19 | } 20 | if let layoutProperty = child.value as? AnyLayoutProperty { 21 | layoutProperty.layoutInstance = self 22 | } 23 | } 24 | 25 | setupChildrenPropertySubscription() 26 | } 27 | 28 | open func setupChildrenPropertySubscription() { 29 | 30 | } 31 | 32 | open func layout(constraints: BoxConstraints) -> DSize2 { 33 | fatalError("layout() not implemented") 34 | } 35 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Layouts/LayoutProperty.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | @propertyWrapper 4 | public class LayoutProperty: AnyLayoutProperty { 5 | private var keyPath: KeyPath> 6 | unowned var layoutInstance: Layout? { 7 | didSet { 8 | setupInstancePropertySubscription() 9 | } 10 | } 11 | var instancePropertySubscription: AnyCancellable? 12 | 13 | public var wrappedValue: T { 14 | layoutInstance!.container[keyPath: keyPath].resolvedValue 15 | } 16 | 17 | public init(_ keyPath: KeyPath>) { 18 | self.keyPath = keyPath 19 | } 20 | 21 | func setupInstancePropertySubscription() { 22 | instancePropertySubscription = layoutInstance!.container[keyPath: keyPath].publisher.sink { [unowned self] _ in 23 | layoutInstance!.container.invalidateLayout() 24 | } 25 | } 26 | } 27 | 28 | internal protocol AnyLayoutProperty: AnyObject { 29 | var layoutInstance: Layout? { get set } 30 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Platform/Screen.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public protocol Screen { 4 | var size: DSize2 { get } 5 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Platform/Window.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public protocol Window { 4 | var bounds: DRect { get } 5 | var screen: Screen { get } 6 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Axis.swift: -------------------------------------------------------------------------------- 1 | public enum Axis { 2 | case Horizontal, Vertical 3 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/BorderWidth.swift: -------------------------------------------------------------------------------- 1 | public typealias BorderWidth = Insets -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Bounded.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public protocol Bounded { 4 | 5 | var globalBounds: DRect { get } 6 | 7 | var bounds: DRect { get } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/BoxConstraints.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | import Foundation 3 | 4 | public struct BoxConstraints: Equatable, CustomDebugStringConvertible { 5 | public var minSize: DSize2 6 | public var maxSize: DSize2 7 | 8 | public var debugDescription: String { 9 | "BoxConstraints { min: \(minWidth) x \(minHeight) | max: \(maxWidth) x \(maxHeight) }" 10 | } 11 | 12 | // TODO: maybe add overflow property to indicate whether overflowing is allowed instead of using infinity in maxSize? 13 | public init(minSize: DSize2, maxSize: DSize2) { 14 | self.minSize = max(.zero, minSize) 15 | self.maxSize = max(.zero, maxSize) 16 | } 17 | 18 | public init(size: DSize2) { 19 | self.init(minSize: size, maxSize: size) 20 | } 21 | 22 | // min size of 0, max size infinity 23 | public static var unconstrained: BoxConstraints { 24 | BoxConstraints(minSize: .zero, maxSize: .infinity) 25 | } 26 | 27 | public var minWidth: Double { 28 | get { 29 | return minSize.width 30 | } 31 | 32 | set { 33 | minSize.width = newValue 34 | } 35 | } 36 | 37 | public var minHeight: Double { 38 | get { 39 | return minSize.height 40 | } 41 | 42 | set { 43 | minSize.height = newValue 44 | } 45 | } 46 | 47 | public var maxWidth: Double { 48 | get { 49 | return maxSize.width 50 | } 51 | 52 | set { 53 | maxSize.width = newValue 54 | } 55 | } 56 | 57 | public var maxHeight: Double { 58 | get { 59 | return maxSize.height 60 | } 61 | 62 | set { 63 | maxSize.height = newValue 64 | } 65 | } 66 | 67 | public func constrain(_ size: DSize2) -> DSize2 { 68 | return DSize2( 69 | min(max(size.width, minSize.width), maxSize.width), 70 | min(max(size.height, minSize.height), maxSize.height)) 71 | } 72 | 73 | /** Subtracts given size from all sizes in BoxConstraints. */ 74 | public static func -= (lhs: inout BoxConstraints, rhs: DSize2) { 75 | lhs.minSize -= rhs 76 | lhs.minSize = max(.zero, lhs.minSize) 77 | lhs.maxSize -= rhs 78 | } 79 | 80 | /** See: BoxConstraints.-= */ 81 | public static func - (lhs: BoxConstraints, rhs: DSize2) -> BoxConstraints { 82 | var result = lhs 83 | result -= rhs 84 | return result 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/CornerRadii.swift: -------------------------------------------------------------------------------- 1 | // TODO: maybe this belongs to GfxMath 2 | public struct CornerRadii { 3 | public var topLeft: Double 4 | public var topRight: Double 5 | public var bottomLeft: Double 6 | public var bottomRight: Double 7 | 8 | public init(topLeft: Double, topRight: Double, bottomLeft: Double, bottomRight: Double) { 9 | self.topLeft = topLeft 10 | self.topRight = topRight 11 | self.bottomLeft = bottomLeft 12 | self.bottomRight = bottomRight 13 | } 14 | 15 | public init(all: Double) { 16 | self.init(topLeft: all, topRight: all, bottomLeft: all, bottomRight: all) 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Insets.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public struct Insets: Equatable { 4 | public var top: Double 5 | public var right: Double 6 | public var bottom: Double 7 | public var left: Double 8 | public var aggregateSize: DSize2 { 9 | DSize2(right + left, top + bottom) 10 | } 11 | 12 | public init(top: Double = 0, right: Double = 0, bottom: Double = 0, left: Double = 0) { 13 | self.top = top 14 | self.right = right 15 | self.bottom = bottom 16 | self.left = left 17 | } 18 | 19 | public init(_ top: Double, _ right: Double, _ bottom: Double, _ left: Double) { 20 | self.init(top: top, right: right, bottom: bottom, left: left) 21 | } 22 | 23 | public init(all value: Double) { 24 | self.init(top: value, right: value, bottom: value, left: value) 25 | } 26 | 27 | public init(_ value: Double) { 28 | self.init(top: value, right: value, bottom: value, left: value) 29 | } 30 | 31 | public static var zero = Insets(all: 0) 32 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Margins.swift: -------------------------------------------------------------------------------- 1 | public struct Margins { 2 | 3 | public var top: Double 4 | 5 | public var right: Double 6 | 7 | public var bottom: Double 8 | 9 | public var left: Double 10 | 11 | public init(top: Double = 0, right: Double = 0, bottom: Double = 0, left: Double = 0) { 12 | 13 | self.top = top 14 | 15 | self.right = right 16 | 17 | self.bottom = bottom 18 | 19 | self.left = left 20 | } 21 | 22 | public init(all: Double) { 23 | 24 | self.init(top: all, right: all, bottom: all, left: all) 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Overflow.swift: -------------------------------------------------------------------------------- 1 | public enum Overflow { 2 | case show, cut, scroll 3 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/TextTransform.swift: -------------------------------------------------------------------------------- 1 | public enum TextTransform { 2 | case uppercase 3 | case lowercase 4 | case capitalize 5 | case none 6 | 7 | func apply(to string: String) -> String { 8 | switch self { 9 | case .uppercase: 10 | return string.uppercased() 11 | case .lowercase: 12 | return string.lowercased() 13 | case .capitalize: 14 | return string.split(separator: " ").map { 15 | $0.count > 0 ? $0[0].uppercased() + $0[1...] : "" 16 | }.joined(separator: " ") 17 | case .none: 18 | return string 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Tick.swift: -------------------------------------------------------------------------------- 1 | public struct Tick: Equatable { 2 | public let deltaTime: Double 3 | public let totalTime: Double 4 | 5 | public init(deltaTime: Double, totalTime: Double) { 6 | self.deltaTime = deltaTime 7 | self.totalTime = totalTime 8 | } 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/Visibility.swift: -------------------------------------------------------------------------------- 1 | public enum Visibility { 2 | case visible, hidden 3 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Primitives/WidgetDimensionSize.swift: -------------------------------------------------------------------------------- 1 | public enum WidgetDimensionSize: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral { 2 | public init(floatLiteral: Double) { 3 | self = .a(floatLiteral) 4 | } 5 | 6 | public init(integerLiteral: Int) { 7 | self = .a(Double(integerLiteral)) 8 | } 9 | 10 | /// absolute value (in pixels) 11 | case a(Double) 12 | /// percent of root width 13 | case rw(Double) 14 | /// percent of root height 15 | case rh(Double) 16 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Resources/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VertexUI/VertexGUI/bd1a4b124ebef002524deb66200053bb7b2035e1/Sources/WidgetGUI/Resources/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/Dependencies/Dependency.swift: -------------------------------------------------------------------------------- 1 | public struct Dependency { 2 | public internal(set) var value: Any 3 | public internal(set) var key: String? 4 | public init(_ value: T, key: String? = nil) { 5 | self.value = value 6 | self.key = key 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/Dependencies/DependencyManager.swift: -------------------------------------------------------------------------------- 1 | public class DependencyManager { 2 | public init() {} 3 | 4 | public func processSubTree(rootWidget: Widget) { 5 | let initialAvailableDependencies = getAvailableDependencies(upTo: rootWidget) 6 | 7 | var iterationStates = [([rootWidget].makeIterator(), initialAvailableDependencies)] 8 | 9 | while iterationStates.count > 0 { 10 | var (iterator, availableDependencies) = iterationStates.removeFirst() 11 | 12 | while let widget = iterator.next() { 13 | resolveDependencies(on: widget, available: availableDependencies) 14 | 15 | if widget.children.count > 0 { 16 | let nextAvailableDependencies = availableDependencies + widget.providedDependencies 17 | iterationStates.append((widget.children.makeIterator(), nextAvailableDependencies)) 18 | } 19 | } 20 | } 21 | } 22 | 23 | public func resolveDependencies(on widget: Widget) { 24 | let availableDependencies = getAvailableDependencies(upTo: widget) 25 | resolveDependencies(on: widget, available: availableDependencies) 26 | } 27 | 28 | func getAvailableDependencies(upTo widget: Widget) -> [Dependency] { 29 | var parents = [Widget]() 30 | var nextParent = widget.parent as? Widget 31 | while let parent = nextParent { 32 | parents.append(parent) 33 | nextParent = parent.parent as? Widget 34 | } 35 | 36 | parents.reverse() 37 | 38 | return parents.flatMap { $0.providedDependencies } 39 | } 40 | 41 | func resolveDependencies(on widget: Widget, available availableDependencies: [Dependency]) { 42 | let mirror = Mirror(reflecting: widget) 43 | for child in mirror.children { 44 | if var inject = child.value as? _AnyInject { 45 | var resolvedValue: Any? = nil 46 | if let key = inject.key { 47 | resolvedValue = availableDependencies.first { $0.key == key } 48 | } else { 49 | resolvedValue = availableDependencies.first { ObjectIdentifier(type(of: $0.value)) == ObjectIdentifier(inject.anyType) }?.value 50 | } 51 | if let resolvedValue = resolvedValue { 52 | inject.anyValue = resolvedValue 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/Dependencies/Inject.swift: -------------------------------------------------------------------------------- 1 | @propertyWrapper 2 | public class Inject: AnyInject { 3 | public typealias Value = T 4 | 5 | internal var key: String? = nil 6 | 7 | internal var value: T? = nil 8 | internal var anyValue: Any? { 9 | get { 10 | return value 11 | } 12 | 13 | set { 14 | if let newValue = newValue as? T { 15 | value = newValue 16 | } else { 17 | fatalError( 18 | "Tried to set value of Inject to different type than specified. Specified type: \(T.self), got new value: \(String(describing: newValue))." 19 | ) 20 | } 21 | } 22 | } 23 | 24 | var anyType: Any.Type = T.self 25 | 26 | public var wrappedValue: T { 27 | if case let .some(value) = value { 28 | return value 29 | } else { 30 | fatalError("a dependency declared with @Inject was not resolved before it's value was accessed") 31 | } 32 | } 33 | 34 | public init(key: String? = nil) { 35 | self.key = key 36 | } 37 | } 38 | 39 | internal protocol AnyInject: AnyObject, _AnyInject {} 40 | 41 | /** 42 | Warning: Do not directly conform to this protocol. Instead conform to AnyInject. 43 | This is necessary to get reference semantics for the Inject containers. Use _AnyInject only 44 | to check whether some property of a class is an Inject container. _AnyInject can not define : AnyObject, because 45 | this crashes Swift because of some NSObject conversion. 46 | */ 47 | internal protocol _AnyInject { 48 | var key: String? { get } 49 | var anyType: Any.Type { get } 50 | var anyValue: Any? { get set } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/Dependencies/Widget+provideDependencies.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func provide(dependencies: [Dependency]) -> Widget { 3 | providedDependencies.append(contentsOf: dependencies) 4 | return self 5 | } 6 | 7 | public func provide(dependencies: Dependency...) -> Widget { 8 | provide(dependencies: dependencies) 9 | } 10 | 11 | public func provide(dependencies: Any...) -> Widget { 12 | provide(dependencies: dependencies.map { Dependency($0) }) 13 | } 14 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/ImmutableBinding.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | @propertyWrapper 4 | public class ImmutableBinding: InternalReactiveProperty { 5 | public typealias Value = O 6 | 7 | public var value: Value { 8 | wrappedValue 9 | } 10 | public var wrappedValue: Value { 11 | _get() 12 | } 13 | private let _get: () ->Value 14 | 15 | lazy public private(set) var publisher = PropertyPublisher(getCurrentValue: { [weak self] in self?.value }) 16 | 17 | lazy public var projectedValue = ReactivePropertyProjection(getImmutable: { [unowned self] in 18 | return ImmutableBinding(self, get: { 19 | $0 20 | }) 21 | }, publisher: AnyPublisher(publisher)) 22 | 23 | private var dependencySubscription: AnyCancellable? 24 | 25 | public init( 26 | _ dependency: Dependency, 27 | get _get: @escaping (DependencyValue) -> Value) where Dependency.Value == DependencyValue { 28 | self._get = { [dependency] in 29 | _get(dependency.value) 30 | } 31 | 32 | dependencySubscription = dependency.publisher.sink { [unowned self] _ in 33 | notifyChange() 34 | } 35 | } 36 | 37 | /// initialize with a plain Publisher from Combine 38 | public init(publisher: P) where P.Output == O, P.Failure == Never { 39 | fatalError("untested") 40 | 41 | var value: P.Output? = nil 42 | 43 | self._get = { [publisher] in 44 | value! 45 | } 46 | 47 | dependencySubscription = publisher.sink { [unowned self] in 48 | value = $0 49 | notifyChange() 50 | } 51 | } 52 | 53 | public init(get _get: @escaping () -> Value) { 54 | self._get = _get 55 | } 56 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/MutableBinding.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | @propertyWrapper 4 | public class MutableBinding: InternalMutableReactiveProperty { 5 | public typealias Value = V 6 | public typealias Output = Value 7 | public typealias Failure = Never 8 | 9 | public var value: Value { 10 | get { 11 | _get() 12 | } 13 | set { 14 | _set(newValue) 15 | } 16 | } 17 | 18 | private let _get: () -> Value 19 | private let _set: (Value) -> () 20 | 21 | public var wrappedValue: Value { 22 | get { value } 23 | set { value = newValue } 24 | } 25 | 26 | lazy public private(set) var publisher = PropertyPublisher(getCurrentValue: { [weak self] in self?.value }) 27 | 28 | lazy public var projectedValue = MutableReactivePropertyProjection(getImmutable: { [unowned self] in 29 | ImmutableBinding(self, get: { 30 | $0 31 | }) 32 | }, getMutable: { [unowned self] in 33 | MutableBinding(self, get: { 34 | $0 35 | }, set: { 36 | $0 37 | }) 38 | }, publisher: AnyPublisher(publisher)) 39 | 40 | private var dependencySubscription: AnyCancellable? 41 | 42 | public init( 43 | _ dependency: Dependency, 44 | get _get: @escaping (Dependency.Value) -> Value, 45 | set _set: @escaping (Value) -> Dependency.Value) { 46 | self._get = { [dependency] in 47 | _get(dependency.value) 48 | } 49 | self._set = { [dependency] in 50 | dependency.value = _set($0) 51 | } 52 | 53 | dependencySubscription = dependency.publisher.sink { [unowned self] _ in 54 | notifyChange() 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/MutableReactiveProperty.swift: -------------------------------------------------------------------------------- 1 | public protocol MutableReactiveProperty: ReactiveProperty { 2 | var value: Value { get set } 3 | } 4 | 5 | protocol InternalMutableReactiveProperty: MutableReactiveProperty, InternalReactiveProperty {} -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/ObservableDictionary.swift: -------------------------------------------------------------------------------- 1 | public final class ObservableDictionary: ExpressibleByDictionaryLiteral { 2 | public typealias Key = K 3 | public typealias Value = V 4 | 5 | fileprivate var data: [Key: Value] 6 | fileprivate var _bindings: [Key: ImmutableBinding] = [:] 7 | 8 | lazy public private(set) var bindings = BindingProxy(self) 9 | 10 | public var keys: Dictionary.Keys { 11 | data.keys 12 | } 13 | 14 | public init(dictionaryLiteral: (Key, Value)...) { 15 | self.data = Dictionary(uniqueKeysWithValues: dictionaryLiteral) 16 | } 17 | 18 | public init(_ initialData: [Key: Value]) { 19 | self.data = initialData 20 | } 21 | 22 | public subscript(_ key: Key) -> Value? { 23 | get { data[key] } 24 | set { 25 | data[key] = newValue 26 | if let binding = _bindings[key] { 27 | binding.notifyChange() 28 | } 29 | } 30 | } 31 | 32 | public class BindingProxy { 33 | unowned let dictionary: ObservableDictionary 34 | 35 | fileprivate init(_ dictionary: ObservableDictionary) { 36 | self.dictionary = dictionary 37 | } 38 | 39 | public subscript(key: Key) -> ReactivePropertyProjection { 40 | if dictionary._bindings[key] == nil { 41 | dictionary._bindings[key] = ImmutableBinding(get: { [unowned self] in 42 | dictionary.data[key] 43 | }) 44 | } 45 | 46 | return dictionary._bindings[key]!.projectedValue 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/PropertyPublisher.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public class PropertyPublisher: Publisher { 4 | public typealias Output = O 5 | public typealias Failure = Never 6 | 7 | private var getCurrentValue: () -> O? 8 | private var subscriptions: [WeakBox>] = [] 9 | 10 | public init(getCurrentValue: @escaping () -> O?) { 11 | self.getCurrentValue = getCurrentValue 12 | } 13 | 14 | public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { 15 | let subscription = ReactivePropertySubscription(subscriber: AnySubscriber(subscriber)) 16 | subscriptions.append(WeakBox(subscription)) 17 | subscriber.receive(subscription: subscription) 18 | if let currentValue = getCurrentValue() { 19 | subscription.receive(currentValue) 20 | } else { 21 | print("warning: no current value present when registering subscriber on property publisher") 22 | } 23 | } 24 | 25 | internal func emit(_ value: Output) { 26 | for subscription in subscriptions { 27 | if let subscription = subscription.wrapped { 28 | subscription.receive(value) 29 | } 30 | } 31 | } 32 | } 33 | 34 | internal class ReactivePropertySubscription: Subscription { 35 | private var subscriber: AnySubscriber? 36 | 37 | init(subscriber: AnySubscriber) { 38 | self.subscriber = subscriber 39 | } 40 | 41 | func request(_ demand: Subscribers.Demand) {} 42 | 43 | func receive(_ value: V) { 44 | _ = subscriber?.receive(value) 45 | } 46 | 47 | func cancel() { 48 | subscriber = nil 49 | } 50 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/ReactiveProperty.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public protocol ReactiveProperty: AnyObject { 4 | associatedtype Value 5 | 6 | var value: Value { get } 7 | 8 | var publisher: PropertyPublisher { get } 9 | } 10 | 11 | internal protocol ErasedInternalReactiveProperty { 12 | func notifyChange() 13 | } 14 | 15 | internal class AnyErasedInternalReactiveProperty: ErasedInternalReactiveProperty { 16 | let _notifyChange: () -> () 17 | 18 | let wrapped: Any 19 | 20 | init(wrapping wrapped: T) { 21 | self._notifyChange = wrapped.notifyChange 22 | self.wrapped = wrapped 23 | } 24 | 25 | func notifyChange() { 26 | _notifyChange() 27 | } 28 | } 29 | 30 | internal protocol InternalReactiveProperty: ReactiveProperty, ErasedInternalReactiveProperty { 31 | } 32 | 33 | extension InternalReactiveProperty { 34 | internal func notifyChange() { 35 | publisher.emit(value) 36 | } 37 | } 38 | 39 | public class AnyReactiveProperty: InternalReactiveProperty { 40 | public typealias Value = V 41 | 42 | public var value: Value { 43 | didSet { 44 | notifyChange() 45 | } 46 | } 47 | 48 | lazy public private(set) var publisher = PropertyPublisher(getCurrentValue: { [weak self] in self?.value }) 49 | 50 | var ownedWrapped: AnyObject 51 | 52 | var wrappedSubscription: AnyObject? 53 | 54 | public init(_ wrapped: P) where P.Value == V { 55 | self.value = wrapped.value 56 | self.ownedWrapped = wrapped 57 | wrappedSubscription = wrapped.publisher.sink(receiveValue: { [unowned self] in 58 | value = $0 59 | }) 60 | } 61 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/ReactivePropertyProjection.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public class ReactivePropertyProjection { 4 | public typealias Value = V 5 | 6 | private let getImmutable: () -> ImmutableBinding 7 | public let publisher: AnyPublisher 8 | 9 | public var immutable: ImmutableBinding { 10 | getImmutable() 11 | } 12 | 13 | init(getImmutable: @escaping () -> ImmutableBinding, publisher: AnyPublisher) { 14 | self.getImmutable = getImmutable 15 | self.publisher = publisher 16 | } 17 | } 18 | 19 | public class MutableReactivePropertyProjection: ReactivePropertyProjection { 20 | private let getMutable: () -> MutableBinding 21 | 22 | public var mutable: MutableBinding { 23 | getMutable() 24 | } 25 | 26 | init(getImmutable: @escaping () -> ImmutableBinding, getMutable: @escaping () -> MutableBinding, publisher: AnyPublisher) { 27 | self.getMutable = getMutable 28 | super.init(getImmutable: getImmutable, publisher: publisher) 29 | } 30 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/State.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | @propertyWrapper 4 | public class State: InternalMutableReactiveProperty { 5 | public typealias Value = V 6 | 7 | public var value: Value { 8 | get { wrappedValue } 9 | set { wrappedValue = newValue } 10 | } 11 | public var wrappedValue: Value { 12 | didSet { 13 | notifyChange() 14 | } 15 | } 16 | 17 | lazy public private(set) var publisher = PropertyPublisher(getCurrentValue: { [weak self] in self?.value }) 18 | 19 | lazy public var projectedValue = MutableReactivePropertyProjection(getImmutable: { [unowned self] in 20 | ImmutableBinding(self, get: { 21 | $0 22 | }) 23 | }, getMutable: { [unowned self] in 24 | MutableBinding(self, get: { 25 | $0 26 | }, set: { 27 | $0 28 | }) 29 | }, publisher: AnyPublisher(publisher)) 30 | 31 | public init(wrappedValue: Value) { 32 | self.wrappedValue = wrappedValue 33 | } 34 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/State/Store.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | open class Store { 4 | public typealias State = S 5 | public typealias Mutation = M 6 | public typealias Action = A 7 | 8 | @StatePropertyWrapper 9 | public var state: State 10 | var stateWrapper: StatePropertyWrapper { 11 | _state 12 | } 13 | public typealias SetterProxy = StateSetterProxy, S, M, A> 14 | lazy var setterProxy = StateSetterProxy(store: self) 15 | 16 | public init(initialState: State) { 17 | self._state = StatePropertyWrapper(initialState: initialState) 18 | } 19 | 20 | open func perform(mutation: Mutation, state: SetterProxy) { 21 | fatalError("perform(mutation:) not implemented") 22 | } 23 | 24 | open func perform(action: Action) -> Future { 25 | fatalError("perform(mutation:) not implemented") 26 | } 27 | 28 | public func commit(_ mutation: Mutation) { 29 | perform(mutation: mutation, state: setterProxy) 30 | } 31 | 32 | @discardableResult public func dispatch(_ action: Action) -> Future { 33 | perform(action: action) 34 | } 35 | } 36 | 37 | @propertyWrapper 38 | public class StatePropertyWrapper { 39 | var state: S 40 | public var wrappedValue: S { 41 | state 42 | } 43 | 44 | lazy public private(set) var projectedValue = ImmutableStateBindingProxy(stateWrapper: self) 45 | var propertyBindings: [AnyKeyPath: AnyErasedInternalReactiveProperty] = [:] 46 | 47 | public init(initialState: S) { 48 | self.state = initialState 49 | } 50 | } 51 | 52 | @dynamicMemberLookup 53 | public class ImmutableStateBindingProxy, State> { 54 | unowned let stateWrapper: StateWrapper 55 | 56 | init(stateWrapper: StateWrapper) { 57 | self.stateWrapper = stateWrapper 58 | } 59 | 60 | public subscript(dynamicMember keyPath: KeyPath) -> ReactivePropertyProjection { 61 | if stateWrapper.propertyBindings[keyPath] == nil { 62 | let newBinding = ImmutableBinding(get: { [unowned self] in stateWrapper.state[keyPath: keyPath] }) 63 | stateWrapper.propertyBindings[keyPath] = AnyErasedInternalReactiveProperty(wrapping: newBinding) 64 | } 65 | return (stateWrapper.propertyBindings[keyPath]!.wrapped as! ImmutableBinding).projectedValue 66 | } 67 | } 68 | 69 | @dynamicMemberLookup 70 | public class StateSetterProxy, State, M, A> { 71 | unowned let store: S 72 | 73 | init(store: S) { 74 | self.store = store 75 | } 76 | 77 | public subscript(dynamicMember keyPath: WritableKeyPath) -> T { 78 | get { 79 | store.stateWrapper.state[keyPath: keyPath] 80 | } 81 | 82 | set { 83 | store.stateWrapper.state[keyPath: keyPath] = newValue 84 | if let binding = store.stateWrapper.propertyBindings[keyPath] { 85 | binding.notifyChange() 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/PseudoClass.swift: -------------------------------------------------------------------------------- 1 | public protocol PseudoClass { 2 | var asString: String { get } 3 | } 4 | 5 | extension PseudoClass where Self: RawRepresentable, Self.RawValue == String { 6 | public var asString: String { 7 | self.rawValue 8 | } 9 | } 10 | 11 | extension String: PseudoClass { 12 | public var asString: String { 13 | self 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Style.swift: -------------------------------------------------------------------------------- 1 | 2 | public class Style { 3 | let selector: StyleSelector 4 | let propertyValueDefinitions: [StylePropertyValueDefinition] 5 | let children: [Style] 6 | let sourceScope: UInt 7 | var treePath: TreePath? = nil { 8 | didSet { 9 | for child in children { 10 | child.treePath = treePath 11 | } 12 | } 13 | } 14 | 15 | public init( 16 | _ selector: StyleSelector, 17 | _ widgetType: W.Type, 18 | @StylePropertyValueDefinitionsBuilder properties buildDefinitions: () -> 19 | [StylePropertyValueDefinition], 20 | @NestedStylesBuilder nested buildNestedStyles: () -> [Style] = { [] } 21 | ) { 22 | self.selector = selector 23 | self.propertyValueDefinitions = buildDefinitions() 24 | self.children = buildNestedStyles() 25 | self.sourceScope = Widget.activeStyleScope 26 | } 27 | 28 | public convenience init(_ selector: StyleSelector, 29 | @StylePropertyValueDefinitionsBuilder properties buildDefinitions: () -> 30 | [StylePropertyValueDefinition], 31 | @NestedStylesBuilder nested buildNestedStyles: () -> [Style] = { [] } 32 | ) { 33 | self.init(selector, Widget.self, properties: buildDefinitions, nested: buildNestedStyles) 34 | } 35 | } 36 | 37 | extension Style { 38 | @resultBuilder 39 | public struct NestedStylesBuilder { 40 | public static func buildExpression(_ style: Style) -> [Style] { 41 | [style] 42 | } 43 | 44 | public static func buildBlock(_ partials: [[Style]]) -> [Style] { 45 | partials.flatMap { $0 } 46 | } 47 | 48 | public static func buildBlock(_ partials: [Style]...) -> [Style] { 49 | buildBlock(partials) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/StyleParser.swift: -------------------------------------------------------------------------------- 1 | public class StyleParser { 2 | /*public init() {} 3 | 4 | public func parse(_ source: String) throws -> [Style] { 5 | var selector: StyleSelector 6 | var properties = [StyleProperty]() 7 | 8 | let lines = source.split(separator: "\n") 9 | 10 | selector = try StyleSelector(parse: String(lines[0][lines[0].startIndex...lines[0].index(of: " ")!])) 11 | 12 | for line in lines[1...] { 13 | let parts = line.split(separator: ":") 14 | 15 | let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) 16 | 17 | let valueLiteral = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) 18 | var value: StyleValue? 19 | for valueParser in valueParsers { 20 | if valueParser.test(valueLiteral) { 21 | value = valueParser.parse(valueLiteral) 22 | if value != nil { 23 | break 24 | } 25 | } 26 | } 27 | 28 | guard let unwrappedValue = value else { 29 | throw ParserError.invalidValueLiteral 30 | } 31 | 32 | properties.append(StyleProperty(key: key, value: unwrappedValue)) 33 | } 34 | 35 | return [Style(selector: selector, properties: StyleProperties(properties), children: [])] 36 | } 37 | 38 | public var valueParsers = [ 39 | ValueParser(test: { _ in 40 | true 41 | }, parse: { 42 | Double($0) 43 | }) 44 | ] 45 | 46 | public struct ValueParser { 47 | public let test: (String) -> Bool 48 | public let parse: (String) -> StyleValue? 49 | 50 | public init(test: @escaping (String) -> Bool, parse: @escaping (String) -> StyleValue?) { 51 | self.test = test 52 | self.parse = parse 53 | } 54 | } 55 | 56 | public enum ParserError: Error { 57 | case invalidValueLiteral 58 | }*/ 59 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/StylePropertyValue.swift: -------------------------------------------------------------------------------- 1 | public enum StylePropertyValue { 2 | case inherit 3 | case value(T) 4 | 5 | public init?(_ any: AnyStylePropertyValue) { 6 | switch any { 7 | case .inherit: 8 | self = .inherit 9 | case let .value(value): 10 | if let value = value as? T { 11 | self = .value(value) 12 | } else { 13 | return nil 14 | } 15 | } 16 | } 17 | } 18 | 19 | public enum AnyStylePropertyValue { 20 | case inherit 21 | case value(Any) 22 | 23 | public init?(_ concreteValue: StylePropertyValue?) { 24 | if let concreteValue = concreteValue { 25 | self.init(concreteValue) 26 | return 27 | } 28 | return nil 29 | } 30 | 31 | public init(_ concreteValue: StylePropertyValue) { 32 | switch concreteValue { 33 | case .inherit: 34 | self = .inherit 35 | case let .value(value): 36 | self = .value(value) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/StylePropertyValueDefinition.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | public struct StylePropertyValueDefinition { 4 | public var keyPath: AnyKeyPath 5 | public var value: Value 6 | 7 | public enum Value { 8 | case constant(AnyStylePropertyValue) 9 | /// expecting a value with type AnyReactiveProperty 10 | /// wherever this enum type is used must ensure value in reactive is 11 | /// as expected by the style processing logic (need to do type check before 12 | /// erasing type!) 13 | case reactive(Any) 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/StylePropertyValueDefinitionsBuilder.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | @resultBuilder 4 | public struct StylePropertyValueDefinitionsBuilder { 5 | public static func buildExpression(_ expression: (KeyPath>, StylePropertyValue)) -> [StylePropertyValueDefinition] { 6 | [StylePropertyValueDefinition( 7 | keyPath: expression.0, 8 | value: .constant(AnyStylePropertyValue(expression.1)) 9 | )] 10 | } 11 | 12 | public static func buildExpression(_ expression: (KeyPath>, StylePropertyValue)) -> [StylePropertyValueDefinition] { 13 | [StylePropertyValueDefinition( 14 | keyPath: expression.0, 15 | value: .constant(AnyStylePropertyValue(expression.1)) 16 | )] 17 | } 18 | 19 | public static func buildExpression(_ expression: (KeyPath>, V)) -> [StylePropertyValueDefinition] { 20 | [StylePropertyValueDefinition( 21 | keyPath: expression.0, 22 | value: .constant(.value(expression.1)) 23 | )] 24 | } 25 | 26 | public static func buildExpression(_ expression: (KeyPath>, V)) -> [StylePropertyValueDefinition] { 27 | [StylePropertyValueDefinition( 28 | keyPath: expression.0, 29 | value: .constant(.value(expression.1)) 30 | )] 31 | } 32 | 33 | public static func buildExpression(_ expression: (KeyPath>, P)) -> [StylePropertyValueDefinition] where P.Value == V { 34 | [StylePropertyValueDefinition( 35 | keyPath: expression.0, 36 | value: .reactive(AnyReactiveProperty(expression.1) as Any) 37 | )] 38 | } 39 | 40 | public static func buildExpression(_ expression: (KeyPath>, P)) -> [StylePropertyValueDefinition] where P.Value == V { 41 | [StylePropertyValueDefinition( 42 | keyPath: expression.0, 43 | value: .reactive(AnyReactiveProperty(expression.1) as Any) 44 | )] 45 | } 46 | 47 | public static func buildBlock(_ partials: [[StylePropertyValueDefinition]]) -> [StylePropertyValueDefinition] { 48 | partials.flatMap { $0 } 49 | } 50 | 51 | public static func buildBlock(_ partials: [StylePropertyValueDefinition]...) -> [StylePropertyValueDefinition] { 52 | buildBlock(partials) 53 | } 54 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Theme/FlatTheme.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public class FlatTheme: Theme { 4 | public let primaryColor: Color 5 | public let textColorOnPrimary: Color 6 | public let secondaryColor: Color 7 | public let textColorOnSecondary: Color 8 | public let backgroundColor: Color 9 | public let textColorOnBackground: Color 10 | 11 | public init(primaryColor: Color, secondaryColor: Color, backgroundColor: Color) { 12 | self.primaryColor = primaryColor 13 | self.secondaryColor = secondaryColor 14 | self.backgroundColor = backgroundColor 15 | if primaryColor.l > 0.5 { 16 | textColorOnPrimary = .black 17 | } else { 18 | textColorOnPrimary = .white 19 | } 20 | if secondaryColor.l > 0.5 { 21 | textColorOnSecondary = .black 22 | } else { 23 | textColorOnSecondary = .white 24 | } 25 | if backgroundColor.l > 0.5 { 26 | textColorOnBackground = .black 27 | } else { 28 | textColorOnBackground = .white 29 | } 30 | } 31 | 32 | public var styles: Style { 33 | Style("&") { 34 | (\.$foreground, textColorOnBackground) 35 | } nested: { 36 | 37 | Style([StyleSelectorPart(type: Button.self)]) { 38 | (\.$background, primaryColor) 39 | (\.$padding, Insets(all: 16)) 40 | (\.$foreground, textColorOnPrimary) 41 | } nested: { 42 | Style([StyleSelectorPart(extendsParent: true, pseudoClasses: ["hover"])]) { 43 | (\.$background, primaryColor.darkened(30)) 44 | } 45 | } 46 | 47 | Style([StyleSelectorPart(type: TextInput.self)], TextInput.self) { 48 | (\.$caretColor, primaryColor) 49 | } 50 | 51 | Style([StyleSelectorPart(type: Widget.ScrollBar.self)], Widget.ScrollBar.self) { 52 | (\.$background, .transparent) 53 | (\.$foreground, primaryColor) 54 | (\.$xBarHeight, 20) 55 | (\.$yBarWidth, 20) 56 | } nested: { 57 | 58 | Style("&:hover") { 59 | (\.$foreground, primaryColor.darkened(30)) 60 | } 61 | } 62 | 63 | Style("Select") { 64 | (\.$background, .yellow) 65 | (\.$debugLayout, true) 66 | } nested: { 67 | Style(".value-field") { 68 | (\.$borderWidth, Insets(all: 1)) 69 | (\.$borderColor, .yellow) 70 | } 71 | 72 | Style(".options-field") { 73 | (\.$borderWidth, Insets(all: 1)) 74 | (\.$borderColor, .yellow) 75 | } 76 | 77 | Style(".option") { 78 | (\.$padding, Insets(all: 8)) 79 | } nested: { 80 | Style("&.selected") { 81 | (\.$background, primaryColor) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Theme/Theme.swift: -------------------------------------------------------------------------------- 1 | public protocol Theme { 2 | var styles: Style { get } 3 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+PseudoClass.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public enum PseudoClasses: String, PseudoClass { 3 | case hover, active 4 | } 5 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+StylePropertiesContainer.swift: -------------------------------------------------------------------------------- 1 | public protocol StylePropertiesContainer: Widget {} 2 | 3 | extension StylePropertiesContainer { 4 | public typealias StyleProperty = WidgetGUI.AnySpecialStyleProperty 5 | 6 | public func with(@StylePropertyValueDefinitionsBuilder styleProperties build: () -> [StylePropertyValueDefinition]) -> Self { 7 | self.DirectStylePropertyValueDefinitions.append(contentsOf: build()) 8 | return self 9 | } 10 | 11 | public func setupStyleProperties() { 12 | let mirror = Mirror(reflecting: self) 13 | for child in mirror.allChildren { 14 | if var property = child.value as? AnyStylePropertyProtocol { 15 | property.container = self 16 | property.name = child.label 17 | } 18 | } 19 | } 20 | } 21 | 22 | extension Widget: StylePropertiesContainer {} -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+inStyleScope.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | @discardableResult 3 | public static func inStyleScope(_ scope: UInt, block: () -> T) -> T { 4 | let previousActiveStyleScope = Widget.activeStyleScope 5 | Widget.activeStyleScope = scope 6 | defer { Widget.activeStyleScope = previousActiveStyleScope } 7 | return block() 8 | } 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+internalPseudoClassManagement.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func enablePseudoClass(_ pseudoClass: PseudoClass) { 3 | pseudoClasses.insert(pseudoClass.asString) 4 | self.notifySelectorChanged() 5 | } 6 | 7 | public func disablePseudoClass(_ pseudoClass: PseudoClass) { 8 | pseudoClasses.remove(pseudoClass.asString) 9 | self.notifySelectorChanged() 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+invalidateMatchedStyles.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func invalidateMatchedStyles() { 3 | if !mounted { 4 | // print("warning: called invalidateMatchedStyles on widget that has not yet been mounted") 5 | return 6 | } 7 | context.queueLifecycleMethodInvocation(.updateMatchedStyles, target: self, sender: self, reason: .undefined) 8 | } 9 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+invalidateResolvedStyleProperties.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func invalidateResolvedStyleProperties() { 3 | if !mounted { 4 | //print("warning: called invalidateResolvedStyleProperties() on a widget that has not yet been mounted") 5 | return 6 | } 7 | 8 | context.queueLifecycleMethodInvocation(.resolveStyleProperties, target: self, sender: self, reason: .undefined) 9 | } 10 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+notifySelectorChanged.swift: -------------------------------------------------------------------------------- 1 | extension Widget { 2 | public func notifySelectorChanged() { 3 | invalidateMatchedStyles() 4 | } 5 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Style/Widget+resolveStyleProperties.swift: -------------------------------------------------------------------------------- 1 | import OpenCombine 2 | 3 | extension Widget { 4 | internal func resolveStyleProperties() { 5 | let sortedMatchedStyles = matchedStyles.sorted { 6 | if ($0.treePath == nil && $1.treePath != nil) || ($0.treePath == nil && $1.treePath == nil) { 7 | return true 8 | } else if $0.treePath != nil && $1.treePath == nil { 9 | return false 10 | } else { 11 | return $0.treePath! < $1.treePath! 12 | } 13 | } 14 | 15 | let matchedStylesDefinitions = sortedMatchedStyles.flatMap { $0.propertyValueDefinitions } 16 | let mergedDefinitions = mergeDefinitions(matchedStylesDefinitions + DirectStylePropertyValueDefinitions) 17 | 18 | var resolvedProperties = [AnyStylePropertyProtocol]() 19 | 20 | for definition in mergedDefinitions { 21 | let definitionWidgetIdentifier = ObjectIdentifier(type(of: definition.keyPath).rootType) 22 | 23 | if let property = self[keyPath: definition.keyPath] as? AnyStylePropertyProtocol { 24 | resolvedProperties.append(property) 25 | 26 | property.definitionValue = definition.value 27 | } 28 | } 29 | 30 | let mirror = Mirror(reflecting: self) 31 | for child in mirror.allChildren { 32 | if type(of: child.value) is AnyStylePropertyProtocol.Type { 33 | let property = child.value as! AnyStylePropertyProtocol 34 | 35 | if resolvedProperties.allSatisfy({ $0 !== property }) { 36 | property.definitionValue = nil 37 | } 38 | } 39 | } 40 | } 41 | 42 | fileprivate func mergeDefinitions(_ definitions: [StylePropertyValueDefinition]) -> [StylePropertyValueDefinition] { 43 | var merged = [AnyKeyPath: (Int, StylePropertyValueDefinition)]() 44 | for (index, definition) in definitions.enumerated() { 45 | merged[definition.keyPath] = (index, definition) 46 | } 47 | return merged.values.sorted { $0.0 < $1.0 }.map { $0.1 } 48 | } 49 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Widgets/Standard/Controls/Button.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | public class Button: ComposedWidget, SlotAcceptingWidgetProtocol { 4 | public static let defaultSlot = Slot(key: "default", data: Void.self) 5 | let defaultSlotManager = SlotContentManager(Button.defaultSlot) 6 | public var defaultNoDataSlotContentManager: SlotContentManager? { 7 | defaultSlotManager 8 | } 9 | 10 | override public var content: DirectContent { 11 | defaultSlotManager() 12 | } 13 | } -------------------------------------------------------------------------------- /Sources/WidgetGUI/Widgets/Standard/Controls/Checkbox.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | /* 3 | public class Checkbox: Widget, GUIMouseEventConsumer { 4 | @MutableProperty 5 | public var checked: Bool 6 | 7 | //public let onCheckedChanged = WidgetEventHandlerManager() 8 | 9 | private var inset: DVec2 { 10 | DVec2(size * 0.1) 11 | } 12 | 13 | // TODO: which type of initializers to keep, and what to do in them? 14 | // --> where to register invalidateRenderState and onCheckedChanged 15 | public init(observe observableChecked: ObservableProperty) { 16 | checked = observableChecked.value 17 | super.init() 18 | _ = onDestroy(observableChecked.onChanged { [unowned self] in 19 | if $0.new != checked { 20 | checked = $0.new 21 | } 22 | }) 23 | _ = onDestroy(self._checked.onChanged { [unowned self] _ in 24 | invalidateRenderState() 25 | //onCheckedChanged.invokeHandlers($0) 26 | }) 27 | } 28 | 29 | public init(bind mutableChecked: MutableProperty) { 30 | _checked = mutableChecked 31 | super.init() 32 | _ = onDestroy(self._checked.onChanged { [unowned self] _ in 33 | invalidateRenderState() 34 | //onCheckedChanged.invokeHandlers($0) 35 | }) 36 | } 37 | 38 | public init(checked: Bool = false) { 39 | self.checked = checked 40 | super.init() 41 | _ = onDestroy(self._checked.onChanged { [unowned self] _ in 42 | invalidateRenderState() 43 | //onCheckedChanged.invokeHandlers($0) 44 | }) 45 | } 46 | 47 | override public func getContentBoxConfig() -> BoxConfig { 48 | BoxConfig(size: DSize2(40, 40)) 49 | } 50 | 51 | override public func performLayout(constraints: BoxConstraints) -> DSize2 { 52 | boxConfig.preferredSize 53 | } 54 | 55 | override public func renderContent() -> RenderObject? { 56 | ContainerRenderObject { 57 | RenderStyleRenderObject(strokeWidth: 2, strokeColor: FixedRenderValue(.black)) { 58 | RectangleRenderObject(globalBounds) 59 | 60 | if checked { 61 | PathRenderObject(Path( 62 | .Start(DPoint2(globalBounds.min.x, globalBounds.center.y) + inset), 63 | .Line(DPoint2(globalBounds.center.x, globalBounds.max.y) - inset), 64 | .Line(DPoint2(globalBounds.max.x - inset.x, globalBounds.min.y + inset.y)))) 65 | } 66 | } 67 | } 68 | } 69 | 70 | public func consume(_ event: GUIMouseEvent) { 71 | if let event = event as? GUIMouseButtonClickEvent { 72 | checked = !checked 73 | } 74 | } 75 | }*/ -------------------------------------------------------------------------------- /Sources/WidgetGUI/Widgets/Standard/Controls/RadioButton.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | /* 3 | public class RadioButton: Widget, GUIMouseEventConsumer { 4 | @MutableProperty 5 | public var checked: Bool 6 | 7 | //private var onCheckedChanged = WidgetEventHandlerManager() 8 | 9 | public init(_ checked: Bool = false) { 10 | self.checked = checked 11 | super.init() 12 | _ = onDestroy(self._checked.onChanged { [unowned self] _ in 13 | invalidateRenderState() 14 | //onCheckedChanged.invokeHandlers($0) 15 | }) 16 | } 17 | 18 | override public func getContentBoxConfig() -> BoxConfig { 19 | BoxConfig(size: DSize2(40, 40)) 20 | } 21 | 22 | override public func performLayout(constraints: BoxConstraints) -> DSize2 { 23 | boxConfig.preferredSize 24 | } 25 | 26 | override public func renderContent() -> RenderObject? { 27 | ContainerRenderObject { 28 | RenderStyleRenderObject(strokeWidth: 2, strokeColor: FixedRenderValue(.black)) { 29 | EllipsisRenderObject(globalBounds) 30 | } 31 | 32 | if checked { 33 | RenderStyleRenderObject(fillColor: .black) { 34 | EllipsisRenderObject(DRect(center: globalBounds.center, size: globalBounds.size * 0.7)) 35 | } 36 | } 37 | } 38 | } 39 | 40 | public func consume(_ event: GUIMouseEvent) { 41 | if let event = event as? GUIMouseButtonClickEvent { 42 | checked = !checked 43 | } 44 | } 45 | }*/ -------------------------------------------------------------------------------- /Sources/WidgetGUI/Widgets/Standard/Controls/Select.swift: -------------------------------------------------------------------------------- 1 | import GfxMath 2 | 3 | private var optionSlots = [ObjectIdentifier: AnySlot]() 4 | 5 | public class Select: ComposedWidget, SlotAcceptingWidgetProtocol { 6 | public typealias Option = O 7 | 8 | override public var name: String { 9 | "Select" 10 | } 11 | 12 | @ImmutableBinding var options: [Option] 13 | @MutableBinding var selectedOption: Option 14 | 15 | @Reference var valueContainer: Widget 16 | @Reference var optionsContainer: Widget 17 | @State var optionsVisibility: Visibility = .hidden 18 | 19 | public static var optionSlot: Slot