[GH-ISSUE #583] How to update a widget inside a flex #427

Closed
opened 2026-03-04 01:04:53 +03:00 by kerem · 1 comment
Owner

Originally created by @michaeltlombardi on GitHub (Mar 23, 2021).
Original GitHub issue: https://github.com/rivo/tview/issues/583

I'm (slowly) working my way through a project to build a UI with tview and want to be able to do the following things:

  1. Have a "loading page" you press enter to leave
  2. Have a "content page" with a menu of options which enables you to swap the "body" widget beside the menu.
  3. Be able to swap focus between the body and menu with a keystroke (CtrlM is fine)

I've run into the following issues:

  1. I have not been able to figure out how to update a widget inside a flex without building a nearly identical page and swapping to that page; I think I could get this to work if I was only using TextView for the body widget, but I plan to also use a form.
  2. If I add an input handler to the app at all, it breaks the front page functionality
  3. I have not (yet) been able to deduce how to swap between the menu and the body, only to switch to the menu.

Repro code (NB: code organization and logic is bad/non-idiomatic, this is both my first go project and my first stab at writing non-library code at all):

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

var (
	appTitle        string = `[green]Project Title`
	appHeaderText   string = `[yellow::b]Extremely engaging and thought provoking text`
	loadingPageText string = `

	Welcome to Repro Example

	[yellow]Press Enter to continue
`
	landingBodyText string = `A, B, C.`
	swapBodyText    string = `1, 2, 3.`
)

// Instantiate the application and pages so they're available across functions
var app = tview.NewApplication()
var pages = tview.NewPages()

// Set the header for the app
func newHeader(title string, text string) tview.Primitive {
	header := tview.NewTextView().SetText(text).
		SetTextAlign(1).
		SetDynamicColors(true)
	header.SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetBorderColor(tcell.ColorPurple).
		SetTitle(title)
	return header
}

// Create the Landing TextView box with border and title
func newLandingText() tview.Primitive {
	landingBody := tview.NewTextView().
		SetWordWrap(true).
		SetText(landingBodyText)
	landingBody.SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetBorderColor(tcell.ColorPurple).
		SetTitle("[green]Introduction")
	return landingBody
}

var landingText = newLandingText()

// Create the Swap TextView box with border and title
func newSwapText() tview.Primitive {
	swapBody := tview.NewTextView().
		SetWordWrap(true).
		SetDynamicColors(true).
		SetRegions(true).
		SetText(string(swapBodyText))
	swapBody.SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetBorderColor(tcell.ColorPurple).
		SetTitle("[green]Swap Title")
	return swapBody
}

var swapText = newSwapText()

// The main menu controls
var mainMenu = tview.NewList().
	AddItem("Landing", "Return to start", 'l', func() {
		pages.SwitchToPage("main")
	}).
	AddItem("Swap", "Change the body", 's', func() {
		pages.SwitchToPage("swap")
	}).
	AddItem("Quit", "Press to exit", 'q', func() { app.Stop() })

// Create the menu with its list
func newFlexMenu(menu tview.Primitive) tview.Primitive {
	mainMenuFlex := tview.NewFlex().AddItem(menu, 0, 1, false)
	mainMenuFlex.SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetBorderColor(tcell.ColorPurple).
		SetTitle("[green]Menu")
	return mainMenuFlex
}

// Create the loading page
func newLoadingPage(menu tview.Primitive) (textview tview.Primitive, flex tview.Primitive) {
	frontTextView := tview.NewTextView().
		SetChangedFunc(func() {
			app.Draw()
		}).
		SetTextAlign(tview.AlignCenter).
		SetDoneFunc(func(key tcell.Key) {
			if key == tcell.KeyEnter {
				pages.SwitchToPage("main")
				app.SetFocus(menu)
			}
		})

	go func() {
		for _, word := range strings.Split(loadingPageText, "\n") {
			fmt.Fprintf(frontTextView, "%s\n", word)
			time.Sleep(100 * time.Millisecond)
		}
	}()

	frontTextView.
		SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetBorderColor(tcell.ColorPurple)

	frontFlex := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(newHeader(appTitle, appHeaderText), 0, 1, false).
		AddItem(frontTextView, 0, 6, true)

	return frontTextView, frontFlex
}

// Create the Content Pages - one for each possible body
// This feels like a bad hack - would prefer to update the body ref
func newContentPage(body tview.Primitive) tview.Primitive {
	main := tview.NewFlex().
		AddItem(newFlexMenu(mainMenu), 0, 1, false).
		AddItem(body, 0, 3, false)
	flex := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(newHeader(appTitle, appHeaderText), 0, 1, false).
		AddItem(main, 0, 8, false)
	return flex
}

func main() {
	// Create the pages
	frontText, frontFlex := newLoadingPage(mainMenu)
	pages.AddPage("front", frontFlex, true, true)
	pages.AddPage("main", newContentPage(landingText), true, false).Focus(func(p tview.Primitive) {
		app.SetFocus(mainMenu)
	})
	pages.AddPage("swap", newContentPage(swapText), true, false).Focus(func(p tview.Primitive) {
		app.SetFocus(mainMenu)
	})

	// Enabling this code block breaks the front page
	// app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
	// 	if event.Key() == tcell.KeyCtrlM {
	// 		app.SetFocus(mainMenu)
	// 		return nil
	// 	}
	// 	return event
	// })

	if err := app.SetRoot(pages, true).SetFocus(frontText).EnableMouse(true).Run(); err != nil {
		panic(err)
	}
}

Originally created by @michaeltlombardi on GitHub (Mar 23, 2021). Original GitHub issue: https://github.com/rivo/tview/issues/583 I'm (slowly) working my way through a project to build a UI with tview and want to be able to do the following things: 1. Have a "loading page" you press enter to leave 2. Have a "content page" with a menu of options which enables you to swap the "body" widget beside the menu. 3. Be able to swap focus between the body and menu with a keystroke (`CtrlM` is fine) I've run into the following issues: 1. I have not been able to figure out how to update a widget inside a flex without building a nearly identical page and swapping to that page; I _think_ I could get this to work if I was only using TextView for the body widget, but I plan to also use a form. 2. If I add an input handler to the app at all, it breaks the front page functionality 3. I have not (yet) been able to deduce how to swap between the menu and the body, only to switch to the menu. Repro code (NB: code organization and logic is bad/non-idiomatic, this is both my first go project and my first stab at writing non-library code at all): ```go package main import ( "fmt" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) var ( appTitle string = `[green]Project Title` appHeaderText string = `[yellow::b]Extremely engaging and thought provoking text` loadingPageText string = ` Welcome to Repro Example [yellow]Press Enter to continue ` landingBodyText string = `A, B, C.` swapBodyText string = `1, 2, 3.` ) // Instantiate the application and pages so they're available across functions var app = tview.NewApplication() var pages = tview.NewPages() // Set the header for the app func newHeader(title string, text string) tview.Primitive { header := tview.NewTextView().SetText(text). SetTextAlign(1). SetDynamicColors(true) header.SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetBorderColor(tcell.ColorPurple). SetTitle(title) return header } // Create the Landing TextView box with border and title func newLandingText() tview.Primitive { landingBody := tview.NewTextView(). SetWordWrap(true). SetText(landingBodyText) landingBody.SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetBorderColor(tcell.ColorPurple). SetTitle("[green]Introduction") return landingBody } var landingText = newLandingText() // Create the Swap TextView box with border and title func newSwapText() tview.Primitive { swapBody := tview.NewTextView(). SetWordWrap(true). SetDynamicColors(true). SetRegions(true). SetText(string(swapBodyText)) swapBody.SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetBorderColor(tcell.ColorPurple). SetTitle("[green]Swap Title") return swapBody } var swapText = newSwapText() // The main menu controls var mainMenu = tview.NewList(). AddItem("Landing", "Return to start", 'l', func() { pages.SwitchToPage("main") }). AddItem("Swap", "Change the body", 's', func() { pages.SwitchToPage("swap") }). AddItem("Quit", "Press to exit", 'q', func() { app.Stop() }) // Create the menu with its list func newFlexMenu(menu tview.Primitive) tview.Primitive { mainMenuFlex := tview.NewFlex().AddItem(menu, 0, 1, false) mainMenuFlex.SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetBorderColor(tcell.ColorPurple). SetTitle("[green]Menu") return mainMenuFlex } // Create the loading page func newLoadingPage(menu tview.Primitive) (textview tview.Primitive, flex tview.Primitive) { frontTextView := tview.NewTextView(). SetChangedFunc(func() { app.Draw() }). SetTextAlign(tview.AlignCenter). SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEnter { pages.SwitchToPage("main") app.SetFocus(menu) } }) go func() { for _, word := range strings.Split(loadingPageText, "\n") { fmt.Fprintf(frontTextView, "%s\n", word) time.Sleep(100 * time.Millisecond) } }() frontTextView. SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetBorderColor(tcell.ColorPurple) frontFlex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(newHeader(appTitle, appHeaderText), 0, 1, false). AddItem(frontTextView, 0, 6, true) return frontTextView, frontFlex } // Create the Content Pages - one for each possible body // This feels like a bad hack - would prefer to update the body ref func newContentPage(body tview.Primitive) tview.Primitive { main := tview.NewFlex(). AddItem(newFlexMenu(mainMenu), 0, 1, false). AddItem(body, 0, 3, false) flex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(newHeader(appTitle, appHeaderText), 0, 1, false). AddItem(main, 0, 8, false) return flex } func main() { // Create the pages frontText, frontFlex := newLoadingPage(mainMenu) pages.AddPage("front", frontFlex, true, true) pages.AddPage("main", newContentPage(landingText), true, false).Focus(func(p tview.Primitive) { app.SetFocus(mainMenu) }) pages.AddPage("swap", newContentPage(swapText), true, false).Focus(func(p tview.Primitive) { app.SetFocus(mainMenu) }) // Enabling this code block breaks the front page // app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { // if event.Key() == tcell.KeyCtrlM { // app.SetFocus(mainMenu) // return nil // } // return event // }) if err := app.SetRoot(pages, true).SetFocus(frontText).EnableMouse(true).Run(); err != nil { panic(err) } } ```
kerem closed this issue 2026-03-04 01:04:53 +03:00
Author
Owner

@rivo commented on GitHub (Apr 26, 2021):

  1. I have not been able to figure out how to update a widget inside a flex without building a nearly identical page and swapping to that page; I think I could get this to work if I was only using TextView for the body widget, but I plan to also use a form.

You'll want to use a Pages component inside the Flex. Then you can always switch between pages to bring them to the front. And it won't require you to duplicate all the other parts of the page.

  1. If I add an input handler to the app at all, it breaks the front page functionality

That's because you're using Ctrl-M which is also triggered when hitting the Enter key (see here for more infos). Maybe switch to Ctrl-N.

  1. I have not (yet) been able to deduce how to swap between the menu and the body, only to switch to the menu.

If you use the Pages component as suggested above, you can simply switch the focus to it and it should then also select the top-most page.

Let me know if this helps.

<!-- gh-comment-id:826829160 --> @rivo commented on GitHub (Apr 26, 2021): > 1. I have not been able to figure out how to update a widget inside a flex without building a nearly identical page and swapping to that page; I think I could get this to work if I was only using TextView for the body widget, but I plan to also use a form. You'll want to use a `Pages` component inside the `Flex`. Then you can always switch between pages to bring them to the front. And it won't require you to duplicate all the other parts of the page. > 2. If I add an input handler to the app at all, it breaks the front page functionality That's because you're using `Ctrl-M` which is also triggered when hitting the `Enter` key (see [here](https://stackoverflow.com/questions/40444408/is-ctrlm-the-same-as-enter) for more infos). Maybe switch to `Ctrl-N`. > 3. I have not (yet) been able to deduce how to swap between the menu and the body, only to switch to the menu. If you use the `Pages` component as suggested above, you can simply switch the focus to it and it should then also select the top-most page. Let me know if this helps.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#427
No description provided.