diff --git a/example/main.go b/example/main.go index 126e2d5..dd4e864 100644 --- a/example/main.go +++ b/example/main.go @@ -13,48 +13,43 @@ func main() { func App() *PaginatorComponent { w, h := osui.GetTerminalSize() - text := Text("") return Paginator( // Page 1 Div( - Text("Welcome to the example! Press tab to go to the next page or press shift + tab to go to a previous page"), + Text("Welcome to the example! The blue text on the bottom show the keybindings"), + WithPosition(0, h-2, Text(colors.Blue+"Next Page: Tab. Previous Page: shift + tab")), ), // Page 2 Div( - WithPosition(0, 0, Text("This blue square is a div. A div stores multiple components into one. To navigate to each of those components you can use arrow keys ↑↓")), + WithPosition(0, 0, Text("This blue square is a div. A div stores multiple components into one")), WithPosition((w-30)/2, (h-4)/2, - WithSize(30, 4, - WithStyle(&DivStyle{Background: colors.Blue, Foreground: colors.Red}, - Div(WithPosition(0, 0, Text("Hello, World!"))), + WithSize(30, 5, + WithStyle(&DivStyle{Outline: colors.Blue}, + Div(WithPosition(4, 1, Button("Hello, World!", BtnParams{Width: 19}))), ))), + WithPosition(0, h-2, Text(colors.Blue+"Navigate: Ctrl+(W/A/S/D)")), ), // Page 3 Div( WithPosition(0, 0, Text("This is a InputBox. It takes user input just like a GUI. "+colors.Red+colors.Bold+"NOTE: This InputBox is inside a div")), WithPosition((w-32)/2, (h-3)/2, InputBox(30)), + WithPosition(0, h-2, Text(colors.Blue+"Navigate: W/S. Select: Enter. Exit Menu: Q. ")), ), // Page 4 Div( WithPosition(0, 0, Text("This is a Button. To click it press Enter. "+colors.Red+colors.Bold+"NOTE: This Button is inside a div")), WithPosition((w-20)/2, (h-3)/2, Button("This is a button", BtnParams{OnClick: func(bc *ButtonComponent) bool { return false }})), + WithPosition(0, h-2, Text(colors.Blue+"Navigate: Ctrl+(W/A/S/D). Click Button: Enter")), ), // Page 5 Div( - WithPosition(0, 2, Menu("Item 1", "Item 2", "Item 3").OnSelected(func(m *MenuComponent, b bool) { - if b { - text.Text = m.Items[m.SelectedItem] - } - })), - WithPosition(0, 6, Menu("Item A", "Item B", "Item C").OnSelected(func(m *MenuComponent, b bool) { - if b { - text.Text = m.Items[m.SelectedItem] - } - })), - text, - ).OnKey(func(d *DivComponent, k string) string { return "" }), + WithPosition(0, 0, Text("This is a Menu. It prompts you to select a item. "+colors.Red+colors.Bold+"NOTE: This Menu is inside a div")), + WithPosition(0, 2, Menu("Item 1", "Item 2", "Item 3", "Item 4", "Item 5").OnSelected(func(m *MenuComponent, b bool) {})), + WithPosition(0, h-2, Text(colors.Blue+"Navigate: Ctrl+(W/A/S/D). Navigate Menu: W/S. Select: Enter. Exit Menu: Q. ")), + ), ) } diff --git a/isKey/key.go b/isKey/key.go index 67e508a..4f94d6a 100644 --- a/isKey/key.go +++ b/isKey/key.go @@ -31,3 +31,23 @@ func Right(s string) bool { func Left(s string) bool { return s == "\x1b[D" } + +func Char(s string, s1 string) bool { + return s == s1 +} + +func CtrlW(s string) bool { + return s == "\x17" +} + +func CtrlS(s string) bool { + return s == "\x13" +} + +func CtrlA(s string) bool { + return s == "\x01" +} + +func CtrlD(s string) bool { + return s == "\x04" +} \ No newline at end of file diff --git a/osui.go b/osui.go index 49bbe39..683f4ff 100644 --- a/osui.go +++ b/osui.go @@ -25,8 +25,7 @@ type Component interface { } type Screen struct { - component Component - CustomRender func() + component Component } func NewScreen(c Component) *Screen { @@ -35,11 +34,7 @@ func NewScreen(c Component) *Screen { return s } -func (s *Screen) Render() error { - if s.CustomRender != nil { - s.CustomRender() - return nil - } +func (s *Screen) Render() { Clear() width, height := GetTerminalSize() frame := NewFrame(width, height) @@ -55,7 +50,6 @@ func (s *Screen) Render() error { data.DefaultColor = colors.Reset RenderOnFrame(s.component, &frame) fmt.Print(strings.Join(frame, "\n")) - return nil } func (s *Screen) Run() { diff --git a/ui/div.go b/ui/div.go index 4c39068..5f576e3 100644 --- a/ui/div.go +++ b/ui/div.go @@ -11,6 +11,7 @@ import ( type DivStyle struct { Background string `default:"" type:"bg"` Foreground string `default:"" type:"fg"` + Outline string `default:"" type:"fg"` } type DivComponent struct { @@ -18,7 +19,6 @@ type DivComponent struct { Style *DivStyle Components []osui.Component ActiveComponent int - onKey func(*DivComponent, string) string } func (d *DivComponent) GetComponentData() *osui.ComponentData { @@ -31,7 +31,7 @@ func (d *DivComponent) Render() string { for i, c := range d.Components { data := c.GetComponentData() if i == d.ActiveComponent { - data.IsActive = true + data.IsActive = d.Data.IsActive } else { data.IsActive = false } @@ -45,41 +45,28 @@ func (d *DivComponent) Render() string { data.Screen = d.Data.Screen osui.RenderOnFrame(c, &frame) } - for i, f := range frame { - frame[i] = colors.Combine(d.Style.Foreground, d.Style.Background) + f + colors.Reset + if d.Style.Outline == "" { + for i, f := range frame { + frame[i] = colors.Combine(d.Style.Foreground, d.Style.Background) + f + colors.Reset + } + return strings.Join(frame, "\n") + } else { + for i, f := range frame { + frame[i] = d.Style.Outline + "│" + colors.Combine(d.Style.Foreground, d.Style.Background) + f + colors.Reset + d.Style.Outline + "│" + colors.Reset + } } - return strings.Join(frame, "\n") + return " " + d.Style.Outline + strings.Repeat("_", d.Data.Width) + colors.Reset + "\n" + strings.Join(frame, "\n") + "\n " + d.Style.Outline + strings.Repeat("‾", d.Data.Width) + colors.Reset } func (d *DivComponent) Update(key string) bool { - if d.onKey != nil { - o := d.onKey(d, key) - if o == "up" { - d.updateActive(d.ActiveComponent - 1) - } else if o == "down" { - d.updateActive(d.ActiveComponent + 1) - } else if o == "true" { - return true - } else if o == "false" { - return false - } else { - if len(d.Components) > 0 { - d.Components[d.ActiveComponent].GetComponentData().IsActive = d.Data.IsActive - if d.Components[d.ActiveComponent].Update(key) { - if d.ActiveComponent < len(d.Components)-1 { - d.updateActive(d.ActiveComponent + 1) - } else { - return true - } - } - } - } - return false - } - if isKey.Up(key) { - d.updateActive(d.ActiveComponent - 1) - } else if isKey.Down(key) { - d.updateActive(d.ActiveComponent + 1) + if isKey.CtrlW(key) { + d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "up")) + } else if isKey.CtrlS(key) { + d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "down")) + } else if isKey.CtrlA(key) { + d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "left")) + } else if isKey.CtrlD(key) { + d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "right")) } else { if len(d.Components) > 0 { d.Components[d.ActiveComponent].GetComponentData().IsActive = d.Data.IsActive @@ -105,11 +92,6 @@ func (d *DivComponent) updateActive(newIndex int) { } } -func (d *DivComponent) OnKey(f func(*DivComponent, string) string) *DivComponent { - d.onKey = f - return d -} - func (d *DivComponent) AddTo(c *DivComponent) *DivComponent { *c = *d return d diff --git a/ui/menu.go b/ui/menu.go index f36a15d..e9c85f6 100644 --- a/ui/menu.go +++ b/ui/menu.go @@ -52,13 +52,13 @@ func (m *MenuComponent) Render() string { } func (m *MenuComponent) Update(key string) bool { - if isKey.Down(key) { + if isKey.Char(key, "s") { if m.SelectedItem+1 < len(m.Items) { m.SelectedItem++ } else { m.SelectedItem = 0 } - } else if isKey.Up(key) { + } else if isKey.Char(key, "w") { if m.SelectedItem > 0 { m.SelectedItem-- } else { @@ -67,7 +67,7 @@ func (m *MenuComponent) Update(key string) bool { } else if isKey.Enter(key) { m.onSelected(m, true) return true - } else if isKey.Escape(key) { + } else if isKey.Char(key, "q") { m.onSelected(m, false) return true } diff --git a/ui/utils.go b/ui/utils.go index f384641..cb7d76a 100644 --- a/ui/utils.go +++ b/ui/utils.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "math" "strings" "github.com/orus-dev/osui" @@ -44,3 +45,52 @@ func WithSize(width, height int, c osui.Component) osui.Component { data.Height = height return c } + +func findClosestComponent(a []osui.Component, i int, d string) int { + if len(a) == 0 || i < 0 || i >= len(a) { + return -1 // Invalid input + } + + current := a[i].GetComponentData() + closestIndex := -1 + minDistance := math.MaxFloat64 + + for j, c := range a { + component := c.GetComponentData() + if i == j { + continue // Skip the current component + } + + // Check if the component is in the correct direction + isValidDirection := false + switch d { + case "up": + if component.Y < current.Y { + isValidDirection = true + } + case "down": + if component.Y > current.Y { + isValidDirection = true + } + case "left": + if component.X < current.X { + isValidDirection = true + } + case "right": + if component.X > current.X { + isValidDirection = true + } + } + + // If the direction is valid, calculate distance + if isValidDirection { + distance := math.Hypot(float64(component.X-current.X), float64(component.Y-current.Y)) + if distance < minDistance { + minDistance = distance + closestIndex = j + } + } + } + + return closestIndex +} diff --git a/utils.go b/utils.go index 00d7804..6cd7d55 100644 --- a/utils.go +++ b/utils.go @@ -13,48 +13,18 @@ import ( "golang.org/x/term" ) -// NOTE: for now keep this code in case of needing it later - -// func renderLine(frameLine, line string, x int) string { -// result := []rune(frameLine) -// lineRunes := []rune(line) -// x += strings.Count(string(result[:(x+1)]), "\t") + strings.Count(string(result[:(x+1)]), "\b") - -// for range strings.Count(line, "\t") + strings.Count(line, "\b") { -// result = append(result, ' ') -// } - -// frameLen := len(result) - -// for i := 0; i < len(lineRunes) && x+i < frameLen; i++ { -// result[x+i] = lineRunes[i] -// } - -// return string(result) -// } - -// lineRunes := []rune(line) -// x += strings.Count(string(frameLine[:(x+1)]), "\b") -// result = append(result, []rune(strings.Repeat(" ", strings.Count(line, "\t")))...) -// frameLen := len(result) -// d := strings.Count(frameLine, "\b") - strings.Count(string(result), "\b") -// for i := 0; i < len(lineRunes) && x+i < frameLen; i++ { -// result[x+i] = lineRunes[i] -// } -// NOTE: for now keep this code in case of needing it later - func renderLine(frameLine, line_ string, x int) string { - result := []rune(frameLine) + frame := []rune(frameLine) line := []rune(line_) i := 0 v := 0 - for j, c := range result { + for j, c := range frame { if i+j >= x && v < len(line) { if c != '\b' && line[v] == '\t' { - result = append(result, ' ') + frame = append(frame[:i+j], append([]rune{' '}, frame[i+j:]...)...) } - result[i+j] = line[v] + frame[i+j] = line[v] v++ } if c == '\b' { @@ -62,7 +32,7 @@ func renderLine(frameLine, line_ string, x int) string { } } - return string(result) + return string(frame) } func RenderOnFrame(c Component, frame *[]string) {