From 16c3a01d2b037dc6d8139c503f13d483d034ff4e Mon Sep 17 00:00:00 2001 From: Frank Mayer Date: Mon, 20 Jan 2025 07:34:39 +0100 Subject: [PATCH] init --- .gitignore | 4 ++ LICENSE | 26 ++++++++++++ README.md | 3 ++ button.go | 6 +++ button_darwin.go | 18 +++++++++ effect.go | 41 +++++++++++++++++++ example/go.mod | 11 +++++ example/go.sum | 4 ++ example/main.go | 54 +++++++++++++++++++++++++ go.mod | 5 +++ go.sum | 4 ++ inputview.go | 8 ++++ inputview_darwin.go | 33 +++++++++++++++ layout.go | 8 ++++ layout_darwin.go | 18 +++++++++ naive.go | 20 ++++++++++ naive_darwin.go | 97 +++++++++++++++++++++++++++++++++++++++++++++ stackview.go | 6 +++ stackview_darwin.go | 13 ++++++ tabview.go | 12 ++++++ tabview_darwin.go | 24 +++++++++++ textcontent.go | 12 ++++++ textview.go | 5 +++ textview_darwin.go | 14 +++++++ 24 files changed, 446 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 button.go create mode 100644 button_darwin.go create mode 100644 effect.go create mode 100644 example/go.mod create mode 100644 example/go.sum create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 inputview.go create mode 100644 inputview_darwin.go create mode 100644 layout.go create mode 100644 layout_darwin.go create mode 100644 naive.go create mode 100644 naive_darwin.go create mode 100644 stackview.go create mode 100644 stackview_darwin.go create mode 100644 tabview.go create mode 100644 tabview_darwin.go create mode 100644 textcontent.go create mode 100644 textview.go create mode 100644 textview_darwin.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfeaa54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/naive +.idea/ +.vs/ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2295f1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +MIT NON-AI License + +Copyright (c) 2025, Frank Mayer + +Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions. + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +In addition, the following restrictions apply: + +1. The Software and any modifications made to it may not be used for the purpose of training or improving machine learning algorithms, +including but not limited to artificial intelligence, natural language processing, or data mining. This condition applies to any derivatives, +modifications, or updates based on the Software code. Any usage of the Software in an AI-training dataset is considered a breach of this License. + +2. The Software may not be included in any dataset used for training or improving machine learning algorithms, +including but not limited to artificial intelligence, natural language processing, or data mining. + +3. Any person or organization found to be in violation of these restrictions will be subject to legal action and may be held liable +for any damages resulting from such use. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a52877 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Naive + +Abstraction for platform native UI, similar to React Native. diff --git a/button.go b/button.go new file mode 100644 index 0000000..8af7a6d --- /dev/null +++ b/button.go @@ -0,0 +1,6 @@ +package naive + +type ButtonView struct { + Content TextContent + OnClick func() +} diff --git a/button_darwin.go b/button_darwin.go new file mode 100644 index 0000000..f4e5e83 --- /dev/null +++ b/button_darwin.go @@ -0,0 +1,18 @@ +package naive + +import ( + "github.com/progrium/darwinkit/helper/action" + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/objc" +) + +func (bv ButtonView) toNative() appkit.IView { + nb := appkit.NewButtonWithTitle(bv.Content.Value()) + if eff, ok := bv.Content.(*Effect[string]); ok { + eff.OnChange(nb.SetTitle) + } + action.Set(nb, func(sender objc.Object) { + bv.OnClick() + }) + return nb +} diff --git a/effect.go b/effect.go new file mode 100644 index 0000000..458f009 --- /dev/null +++ b/effect.go @@ -0,0 +1,41 @@ +package naive + +import "fmt" + +type Effect[T any] struct { + value T + listeners []func(newValue T) +} + +var effects []interface{ clearListeners() } + +func clearAllEffectListeners() { + for _, eff := range effects { + eff.clearListeners() + } +} + +func UseEffect[T any](value T) *Effect[T] { + eff := &Effect[T]{value, nil} + effects = append(effects, eff) + return eff +} + +func (eff *Effect[T]) Value() string { + return fmt.Sprintf("%v", eff.value) +} + +func (eff *Effect[T]) SetValue(newValue T) { + eff.value = newValue + for _, listener := range eff.listeners { + listener(newValue) + } +} + +func (eff *Effect[T]) OnChange(listener func(newValue T)) { + eff.listeners = append(eff.listeners, listener) +} + +func (eff *Effect[T]) clearListeners() { + eff.listeners = eff.listeners[:0] +} diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..fdf9db7 --- /dev/null +++ b/example/go.mod @@ -0,0 +1,11 @@ +module example + +go 1.23.3 + +require git.frankmayer.dev/tsukinoko-kun/naive v0.0.0-00010101000000-000000000000 + +require github.com/progrium/darwinkit v0.5.0 // indirect + +replace git.frankmayer.dev/tsukinoko-kun/naive => ../ + +exclude git.frankmayer.dev/tsukinoko-kun/naive v0.0.0 diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..d6bfa4a --- /dev/null +++ b/example/go.sum @@ -0,0 +1,4 @@ +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/progrium/darwinkit v0.5.0 h1:SwchcMbTOG1py3CQsINmGlsRmYKdlFrbnv3dE4aXA0s= +github.com/progrium/darwinkit v0.5.0/go.mod h1:PxQhZuftnALLkCVaR8LaHtUOfoo4pm8qUDG+3C/sXNs= diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..a1c0bb7 --- /dev/null +++ b/example/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "git.frankmayer.dev/tsukinoko-kun/naive" +) + +func main() { + Text1 := naive.UseEffect("foo") + + views := make([]naive.View, 2) + + views[0] = naive.StackView{ + Children: []naive.View{ + naive.TextView{ + Content: Text1, + }, + naive.ButtonView{ + Content: naive.StringTextContent("Back"), + OnClick: func() { + naive.SetView(views[1]) + }, + }, + }, + Orientation: naive.LayoutOrientationVertical, + } + + views[1] = naive.TabView{ + Children: []naive.TabViewItem{ + { + Title: naive.StringTextContent("Tab 1️⃣"), + Content: naive.StackView{ + Children: []naive.View{ + naive.ButtonView{ + Content: Text1, + OnClick: func() { + naive.SetView(views[0]) + }, + }, + naive.TextInputView{ + Value: Text1, + Placeholder: Text1, + }, + naive.TextInputView{ + Placeholder: naive.StringTextContent("just for fun"), + }, + }, + Orientation: naive.LayoutOrientationVertical, + }, + }, + }, + } + + naive.StartApp("Meep", views[1]) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f1b456b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.frankmayer.dev/tsukinoko-kun/naive + +go 1.23.3 + +require github.com/progrium/darwinkit v0.5.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d6bfa4a --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/progrium/darwinkit v0.5.0 h1:SwchcMbTOG1py3CQsINmGlsRmYKdlFrbnv3dE4aXA0s= +github.com/progrium/darwinkit v0.5.0/go.mod h1:PxQhZuftnALLkCVaR8LaHtUOfoo4pm8qUDG+3C/sXNs= diff --git a/inputview.go b/inputview.go new file mode 100644 index 0000000..a09c4dc --- /dev/null +++ b/inputview.go @@ -0,0 +1,8 @@ +package naive + +type ( + TextInputView struct { + Value TextContent + Placeholder TextContent + } +) diff --git a/inputview_darwin.go b/inputview_darwin.go new file mode 100644 index 0000000..7da354f --- /dev/null +++ b/inputview_darwin.go @@ -0,0 +1,33 @@ +package naive + +import ( + "github.com/progrium/darwinkit/dispatch" + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/macos/foundation" +) + +func (tiv TextInputView) toNative() appkit.IView { + ntiv := appkit.NewTextField() + if tiv.Value != nil { + ntiv.SetStringValue(tiv.Value.Value()) + if eff, ok := tiv.Value.(*Effect[string]); ok { + tfd := &appkit.TextFieldDelegate{} + tfd.SetControlTextDidChange(func(obj foundation.Notification) { + dispatch.MainQueue().DispatchAsync(func() { + if ntiv.StringValue() != eff.value { + eff.SetValue(ntiv.StringValue()) + } + }) + }) + ntiv.SetDelegate(tfd) + eff.OnChange(ntiv.SetStringValue) + } + } + if tiv.Placeholder != nil { + ntiv.SetPlaceholderString(tiv.Placeholder.Value()) + if eff, ok := tiv.Placeholder.(*Effect[string]); ok { + eff.OnChange(ntiv.SetPlaceholderString) + } + } + return ntiv +} diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..674ec97 --- /dev/null +++ b/layout.go @@ -0,0 +1,8 @@ +package naive + +type LayoutOrientation uint8 + +const ( + LayoutOrientationHorizontal = iota + LayoutOrientationVertical +) diff --git a/layout_darwin.go b/layout_darwin.go new file mode 100644 index 0000000..a044fb7 --- /dev/null +++ b/layout_darwin.go @@ -0,0 +1,18 @@ +package naive + +import ( + "fmt" + + "github.com/progrium/darwinkit/macos/appkit" +) + +func (lo LayoutOrientation) toNative() appkit.UserInterfaceLayoutOrientation { + switch lo { + case LayoutOrientationHorizontal: + return appkit.UserInterfaceLayoutOrientationHorizontal + case LayoutOrientationVertical: + return appkit.UserInterfaceLayoutOrientationVertical + default: + panic(fmt.Sprintf("invalid layout orientation %d", lo)) + } +} diff --git a/naive.go b/naive.go new file mode 100644 index 0000000..7724d31 --- /dev/null +++ b/naive.go @@ -0,0 +1,20 @@ +package naive + +var ( + appTitle string +) + +func StartApp(title string, view View) { + appTitle = title + startNativeApp(view) +} + +func SetView(view View) { + clearAllEffectListeners() + nativeSetView(view) +} + +func SetTitle(title string) { + appTitle = title + setNativeTitle(title) +} diff --git a/naive_darwin.go b/naive_darwin.go new file mode 100644 index 0000000..8ebbe38 --- /dev/null +++ b/naive_darwin.go @@ -0,0 +1,97 @@ +package naive + +import ( + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/macos/foundation" + "github.com/progrium/darwinkit/objc" +) + +func startNativeApp(view View) { + if nativeAppWindow != nil { + return + } + + app := appkit.Application_SharedApplication() + + delegate := &appkit.ApplicationDelegate{} + delegate.SetApplicationDidFinishLaunching(func(notification foundation.Notification) { + w := appkit.NewWindowWithSize(720, 440) + nativeAppWindow = &w + objc.Retain(nativeAppWindow) + nativeAppWindow.SetTitle(appTitle) + + nativeAppWindow.SetContentView(view.toNative()) + + nativeAppWindow.Center() + nativeAppWindow.MakeKeyAndOrderFront(nil) + nativeAppWindow.SetFrameAutosaveName(appkit.WindowFrameAutosaveName(appTitle)) + setSystemBar(app) + + app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular) + app.ActivateIgnoringOtherApps(true) + + }) + delegate.SetApplicationWillFinishLaunching(func(foundation.Notification) { + setMainMenu(app) + }) + delegate.SetApplicationShouldTerminateAfterLastWindowClosed(func(appkit.Application) bool { + return true + }) + + app.SetDelegate(delegate) + app.Run() +} + +type ( + View interface { + toNative() appkit.IView + } +) + +var ( + nativeAppWindow *appkit.Window +) + +func nativeSetView(view View) { + nativeAppWindow.SetContentView(view.toNative()) +} + +func setNativeTitle(title string) { + nativeAppWindow.SetTitle(title) +} + +func setMainMenu(app appkit.Application) { + menu := appkit.NewMenuWithTitle("main") + app.SetMainMenu(menu) + + mainMenuItem := appkit.NewMenuItemWithSelector("", "", objc.Selector{}) + mainMenuMenu := appkit.NewMenuWithTitle("App") + mainMenuMenu.AddItem(appkit.NewMenuItemWithAction("Hide", "h", func(sender objc.Object) { app.Hide(nil) })) + mainMenuMenu.AddItem(appkit.NewMenuItemWithAction("Quit", "q", func(sender objc.Object) { app.Terminate(nil) })) + mainMenuItem.SetSubmenu(mainMenuMenu) + menu.AddItem(mainMenuItem) + + testMenuItem := appkit.NewMenuItemWithSelector("", "", objc.Selector{}) + editMenu := appkit.NewMenuWithTitle("Edit") + editMenu.AddItem(appkit.NewMenuItemWithSelector("Select All", "a", objc.Sel("selectAll:"))) + editMenu.AddItem(appkit.MenuItem_SeparatorItem()) + editMenu.AddItem(appkit.NewMenuItemWithSelector("Copy", "c", objc.Sel("copy:"))) + editMenu.AddItem(appkit.NewMenuItemWithSelector("Paste", "v", objc.Sel("paste:"))) + editMenu.AddItem(appkit.NewMenuItemWithSelector("Cut", "x", objc.Sel("cut:"))) + editMenu.AddItem(appkit.NewMenuItemWithSelector("Undo", "z", objc.Sel("undo:"))) + editMenu.AddItem(appkit.NewMenuItemWithSelector("Redo", "Z", objc.Sel("redo:"))) + testMenuItem.SetSubmenu(editMenu) + menu.AddItem(testMenuItem) +} + +func setSystemBar(app appkit.Application) { + item := appkit.StatusBar_SystemStatusBar().StatusItemWithLength(appkit.VariableStatusItemLength) + objc.Retain(&item) + img := appkit.Image_ImageWithSystemSymbolNameAccessibilityDescription("multiply.circle.fill", "A multiply symbol inside a filled circle.") + item.Button().SetImage(img) + + menu := appkit.NewMenuWithTitle("main") + menu.AddItem(appkit.NewMenuItemWithAction("Hide", "h", func(sender objc.Object) { app.Hide(nil) })) + menu.AddItem(appkit.NewMenuItemWithAction("Quit", "q", func(sender objc.Object) { app.Terminate(nil) })) + item.SetMenu(menu) +} diff --git a/stackview.go b/stackview.go new file mode 100644 index 0000000..41e6359 --- /dev/null +++ b/stackview.go @@ -0,0 +1,6 @@ +package naive + +type StackView struct { + Children []View + Orientation LayoutOrientation +} diff --git a/stackview_darwin.go b/stackview_darwin.go new file mode 100644 index 0000000..3a419b7 --- /dev/null +++ b/stackview_darwin.go @@ -0,0 +1,13 @@ +package naive + +import "github.com/progrium/darwinkit/macos/appkit" + +func (sv StackView) toNative() appkit.IView { + nsv := appkit.NewStackView() + nsv.SetOrientation(sv.Orientation.toNative()) + nsv.SetTranslatesAutoresizingMaskIntoConstraints(true) + for _, v := range sv.Children { + nsv.AddViewInGravity(v.toNative(), appkit.StackViewGravityTop) + } + return nsv +} diff --git a/tabview.go b/tabview.go new file mode 100644 index 0000000..6bdd811 --- /dev/null +++ b/tabview.go @@ -0,0 +1,12 @@ +package naive + +type ( + TabView struct { + Children []TabViewItem + } + + TabViewItem struct { + Title TextContent + Content View + } +) diff --git a/tabview_darwin.go b/tabview_darwin.go new file mode 100644 index 0000000..46a2b62 --- /dev/null +++ b/tabview_darwin.go @@ -0,0 +1,24 @@ +package naive + +import "github.com/progrium/darwinkit/macos/appkit" + +func (tv TabView) toNative() appkit.IView { + tabView := appkit.NewTabView() + tabView.SetTranslatesAutoresizingMaskIntoConstraints(true) + + for _, tiv := range tv.Children { + tabView.AddTabViewItem(tiv.toNative()) + } + + return tabView +} + +func (tvi TabViewItem) toNative() appkit.TabViewItem { + ti := appkit.NewTabViewItem() + ti.SetLabel(tvi.Title.Value()) + if eff, ok := tvi.Title.(*Effect[string]); ok { + eff.OnChange(ti.SetLabel) + } + ti.SetView(tvi.Content.toNative()) + return ti +} diff --git a/textcontent.go b/textcontent.go new file mode 100644 index 0000000..f27d93e --- /dev/null +++ b/textcontent.go @@ -0,0 +1,12 @@ +package naive + +type ( + StringTextContent string + TextContent interface { + Value() string + } +) + +func (stc StringTextContent) Value() string { + return string(stc) +} diff --git a/textview.go b/textview.go new file mode 100644 index 0000000..22da374 --- /dev/null +++ b/textview.go @@ -0,0 +1,5 @@ +package naive + +type TextView struct { + Content TextContent +} diff --git a/textview_darwin.go b/textview_darwin.go new file mode 100644 index 0000000..be5764f --- /dev/null +++ b/textview_darwin.go @@ -0,0 +1,14 @@ +package naive + +import ( + "github.com/progrium/darwinkit/macos/appkit" +) + +func (tv TextView) toNative() appkit.IView { + ntv := appkit.NewLabel(tv.Content.Value()) + if eff, ok := tv.Content.(*Effect[string]); ok { + eff.OnChange(ntv.SetStringValue) + } + + return ntv +}