This commit is contained in:
Frank Mayer 2025-01-20 07:34:39 +01:00
commit 16c3a01d2b
Signed by: tsukinoko-kun
GPG Key ID: 427B3E61E69C2E51
24 changed files with 446 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/naive
.idea/
.vs/
.vscode/

26
LICENSE Normal file
View File

@ -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.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Naive
Abstraction for platform native UI, similar to React Native.

6
button.go Normal file
View File

@ -0,0 +1,6 @@
package naive
type ButtonView struct {
Content TextContent
OnClick func()
}

18
button_darwin.go Normal file
View File

@ -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
}

41
effect.go Normal file
View File

@ -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]
}

11
example/go.mod Normal file
View File

@ -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

4
example/go.sum Normal file
View File

@ -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=

54
example/main.go Normal file
View File

@ -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])
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.frankmayer.dev/tsukinoko-kun/naive
go 1.23.3
require github.com/progrium/darwinkit v0.5.0

4
go.sum Normal file
View File

@ -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=

8
inputview.go Normal file
View File

@ -0,0 +1,8 @@
package naive
type (
TextInputView struct {
Value TextContent
Placeholder TextContent
}
)

33
inputview_darwin.go Normal file
View File

@ -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
}

8
layout.go Normal file
View File

@ -0,0 +1,8 @@
package naive
type LayoutOrientation uint8
const (
LayoutOrientationHorizontal = iota
LayoutOrientationVertical
)

18
layout_darwin.go Normal file
View File

@ -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))
}
}

20
naive.go Normal file
View File

@ -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)
}

97
naive_darwin.go Normal file
View File

@ -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)
}

6
stackview.go Normal file
View File

@ -0,0 +1,6 @@
package naive
type StackView struct {
Children []View
Orientation LayoutOrientation
}

13
stackview_darwin.go Normal file
View File

@ -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
}

12
tabview.go Normal file
View File

@ -0,0 +1,12 @@
package naive
type (
TabView struct {
Children []TabViewItem
}
TabViewItem struct {
Title TextContent
Content View
}
)

24
tabview_darwin.go Normal file
View File

@ -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
}

12
textcontent.go Normal file
View File

@ -0,0 +1,12 @@
package naive
type (
StringTextContent string
TextContent interface {
Value() string
}
)
func (stc StringTextContent) Value() string {
return string(stc)
}

5
textview.go Normal file
View File

@ -0,0 +1,5 @@
package naive
type TextView struct {
Content TextContent
}

14
textview_darwin.go Normal file
View File

@ -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
}