├── .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