[GH-ISSUE #128] Questions: efficient way draw screen #98

Closed
opened 2026-03-04 01:01:55 +03:00 by kerem · 6 comments
Owner

Originally created by @stephencheng on GitHub (Jun 2, 2018).
Original GitHub issue: https://github.com/rivo/tview/issues/128

I am not entirely clearly understanding how Draw() method works and how it is efficient.

For example, I have a page with a few flexs, then the flex contains tables/list/textview etc.

I did update using app.Draw() each time when a primitive updated with the new data. It ends up with a few app.Draw() calls in just almost a second. The whole screen sometimes flashes in a funny way that one region/area was updated but updated again due to the refresh of the bigger area of parent primitive.

So questions:

  1. Is the app.Draw the only method to call to redraw the screen? and does it have the smart to refresh the root primitive and all children primitives?

  2. If so, how do I refresh each child primitive element individually?

  3. I'd assume the interface method: Draw(screen tcell.Screen) is for app.Draw to trigger the draw for each primitive, am I right? again, is there a method in primitive I can call to only refresh that primitive only?

Thanks

Originally created by @stephencheng on GitHub (Jun 2, 2018). Original GitHub issue: https://github.com/rivo/tview/issues/128 I am not entirely clearly understanding how Draw() method works and how it is efficient. For example, I have a page with a few flexs, then the flex contains tables/list/textview etc. I did update using app.Draw() each time when a primitive updated with the new data. It ends up with a few app.Draw() calls in just almost a second. The whole screen sometimes flashes in a funny way that one region/area was updated but updated again due to the refresh of the bigger area of parent primitive. So questions: 1. Is the app.Draw the only method to call to redraw the screen? and does it have the smart to refresh the root primitive and all children primitives? 2. If so, how do I refresh each child primitive element individually? 3. I'd assume the interface method: Draw(screen tcell.Screen) is for app.Draw to trigger the draw for each primitive, am I right? again, is there a method in primitive I can call to only refresh that primitive only? Thanks
kerem closed this issue 2026-03-04 01:01:55 +03:00
Author
Owner

@rivo commented on GitHub (Jun 5, 2018):

To answer your questions:

  1. app.Draw() is the only application-wide method to update the screen. Apart from some synchronization, it only calls the Draw() function of the root primitive. If your root primitive is a container primitive (e.g. Flex, Grid, or Pages), it will then call Draw() of its contained primitives. Drawing always happens top-down.
  2. You can't. But my understanding is that tcell will only update changes to the screen buffer. (I haven't actually tested this myself but that's my assumption.) So if there was no change to any of your primitives, you can call app.Draw() many times and nothing will be sent to the terminal. (You will have some CPU overhead to redraw the screen buffer but I would think that's negligible.)
  3. See (2). There are methods that let you override app.Draw(), or to be more precise, insert additional drawing actions before and after drawing the primitives, but I think that's not what you're asking for. But in general, there is no dependency from the primitives back to Application. In theory, you could write your own replacement for Application and handle drawing differently. (But then you'd also have to do all the event handling yourself.)

This is what tcell says on the subject:

Reasonable attempts have been made to minimize sending data to terminals, avoiding repeated sequences or drawing the same cell on refresh updates.

Two additional notes:

  • app.Draw() is called implicitly in response to events. So if you do something in response to an event (in the same goroutine), you don't need to call app.Draw() again. Only if you modify your primitives in a separate goroutine (e.g. writing to TextView), you need to call app.Draw() to reflect those changes.
  • If you make many small changes in a different goroutine, you may want to buffer them before calling app.Draw(). For example, if you write individual characters to a TextView instead of longer strings, you may want to call app.Draw() periodically instead of after each "changed" event.
<!-- gh-comment-id:394737726 --> @rivo commented on GitHub (Jun 5, 2018): To answer your questions: 1. `app.Draw()` is the only application-wide method to update the screen. Apart from some synchronization, it only calls the `Draw()` function of the root primitive. If your root primitive is a container primitive (e.g. `Flex`, `Grid`, or `Pages`), it will then call `Draw()` of its contained primitives. Drawing always happens top-down. 2. You can't. But my understanding is that `tcell` will only update changes to the screen buffer. (I haven't actually tested this myself but that's my assumption.) So if there was no change to any of your primitives, you can call `app.Draw()` many times and nothing will be sent to the terminal. (You will have some CPU overhead to redraw the screen buffer but I would think that's negligible.) 3. See (2). There are methods that let you override `app.Draw()`, or to be more precise, insert additional drawing actions before and after drawing the primitives, but I think that's not what you're asking for. But in general, there is no dependency from the primitives back to `Application`. In theory, you could write your own replacement for `Application` and handle drawing differently. (But then you'd also have to do all the event handling yourself.) This is what `tcell` says on the subject: > Reasonable attempts have been made to minimize sending data to terminals, avoiding repeated sequences or drawing the same cell on refresh updates. Two additional notes: - `app.Draw()` is called implicitly in response to events. So if you do something in response to an event (in the same goroutine), you don't need to call `app.Draw()` again. Only if you modify your primitives in a separate goroutine (e.g. writing to `TextView`), you need to call `app.Draw()` to reflect those changes. - If you make many small changes in a different goroutine, you may want to buffer them before calling `app.Draw()`. For example, if you write individual characters to a `TextView` instead of longer strings, you may want to call `app.Draw()` periodically instead of after each ["changed"](https://godoc.org/github.com/rivo/tview#TextView.SetChangedFunc) event.
Author
Owner

@stephencheng commented on GitHub (Jun 12, 2018):

Thanks, it's very good explanation. I think currently I will just rely on existing implementation. I am happy as long as the Draw only update when needed(there is buffer change)

Thank you

<!-- gh-comment-id:396470937 --> @stephencheng commented on GitHub (Jun 12, 2018): Thanks, it's very good explanation. I think currently I will just rely on existing implementation. I am happy as long as the Draw only update when needed(there is buffer change) Thank you
Author
Owner

@ardnew commented on GitHub (Jul 6, 2018):

To minimize updates/flickering, I'm currently doing as you suggested in your comment -- primitives will post to a channel when they've made a change, the channel is polled periodically with an app.Draw() being called one time if necessary, and then the channel is drained. This seems to be working fine.

However, since it appears from #59 that our Application's tcell.Screen won't be exposed to us, I'm wondering how we are supposed to use all the primitives' draw methods (e.g. func (*TreeView) Draw(tcell.Screen)) that are exported through the API? We obviously can't use them without a tcell.Screen. Please correct me if I'm wrong, but the only way then to access those primitive draw methods (without defining our own Application replacement) is to:

  1. Install a pre-draw callback, e.g. func tryDraw(tcell.Screen) bool, via SetBeforeDrawFunc()
  2. Invoke app.Draw() periodically
  3. In the periodic tryDraw() events, check each primitive if it has a draw update available and call its primitive draw method using the screen reference passed into the callback
  4. Return true from the callback so that drawing does not continue (hopefully for the remainder of primitives only?)

This seems cumbersome (and susceptible to unintentionally cancelling other draw events) so I'm wondering if I am misunderstanding how to use those primitive draw methods. Is there a simpler way to tell a single primitive to redraw?

Thanks

<!-- gh-comment-id:403130591 --> @ardnew commented on GitHub (Jul 6, 2018): To minimize updates/flickering, I'm currently doing as you suggested in your comment -- primitives will post to a channel when they've made a change, the channel is polled periodically with an `app.Draw()` being called one time if necessary, and then the channel is drained. This seems to be working fine. However, since it appears from [#59](https://github.com/rivo/tview/pull/59) that our `Application`'s `tcell.Screen` _won't_ be exposed to us, I'm wondering how we are supposed to use all the primitives' draw methods (e.g. `func (*TreeView) Draw(tcell.Screen)`) that _are_ exported through the API? We obviously can't use them without a `tcell.Screen`. Please correct me if I'm wrong, but the only way then to access those primitive draw methods (without defining our own `Application` replacement) is to: 1. Install a pre-draw callback, e.g. `func tryDraw(tcell.Screen) bool`, via `SetBeforeDrawFunc()` 2. Invoke `app.Draw()` periodically 3. In the periodic `tryDraw()` events, check each primitive if it has a draw update available and call its primitive draw method using the screen reference passed into the callback 4. Return `true` from the callback so that drawing does not continue (hopefully for the remainder of primitives **only**?) This seems cumbersome (and susceptible to unintentionally cancelling _other_ draw events) so I'm wondering if I am misunderstanding how to use those primitive draw methods. Is there a simpler way to tell a single primitive to redraw? Thanks
Author
Owner

@rivo commented on GitHub (Jul 17, 2018):

A primitive's Draw() function is exported, yes, but only because those types implement the public Primitive interface. It's not meant to be called directly by any application, unless you write your own Application replacement.

So, similar to @stephencheng's question, I'm wondering why you want to call a primitive's Draw() function directly. It should be safe to call app.Draw() for updates. As mentioned further above, tcell attempts to minimize the number of characters sent to the terminal so the main overhead is drawing into the internal screen buffer which should be minimal.

Also, tview does not implement clipping. So if you use Pages for example, with overlapping windows (e.g. a Modal), updating a primitive in the background will mess up your layout.

If you insist on calling a primitive's Draw() function directly, I would suggest writing your own Application class. But note that in the end, you'll have to call tcell's screen.Show() function, too, which also updates the entire screen and not just your primitive. So I'm not sure if you'll gain something from that.

Let me know what you think about this.

<!-- gh-comment-id:405674441 --> @rivo commented on GitHub (Jul 17, 2018): A primitive's `Draw()` function is exported, yes, but only because those types implement the public `Primitive` interface. It's not meant to be called directly by any application, unless you write your own `Application` replacement. So, similar to @stephencheng's question, I'm wondering why you want to call a primitive's `Draw()` function directly. It should be safe to call `app.Draw()` for updates. As mentioned further above, `tcell` attempts to minimize the number of characters sent to the terminal so the main overhead is drawing into the internal screen buffer which should be minimal. Also, `tview` does not implement clipping. So if you use `Pages` for example, with overlapping windows (e.g. a `Modal`), updating a primitive in the background will mess up your layout. If you insist on calling a primitive's `Draw()` function directly, I would suggest writing your own `Application` class. But note that in the end, you'll have to call `tcell`'s `screen.Show()` function, too, which also updates the entire screen and not just your primitive. So I'm not sure if you'll gain something from that. Let me know what you think about this.
Author
Owner

@ardnew commented on GitHub (Jul 17, 2018):

I'll admit, the intent with calling the primitives' Draw() routine was premature optimization on my part without fully understanding the mechanics. I was misunderstanding their purpose for being exported, assuming they were an efficient alternative to app.Draw(). Also -- painful experiences from certain other Win32 GUI toolkits I was trying to avoid.

Once I implemented the buffering method (by polling for Draw() updates) you suggested, I've had zero performance issues with screen drawing, so tcell must be handling the updates efficiently.

I'm comfortable just trusting tcell here, as it keeps my code cleaner and far simpler (with probable equivalent or superior performance).

<!-- gh-comment-id:405686349 --> @ardnew commented on GitHub (Jul 17, 2018): I'll admit, the intent with calling the primitives' `Draw()` routine was premature optimization on my part without fully understanding the mechanics. I was misunderstanding their purpose for being exported, assuming they were an efficient alternative to `app.Draw()`. Also -- painful experiences from certain other Win32 GUI toolkits I was trying to avoid. Once I implemented the buffering method (by polling for `Draw()` updates) you suggested, I've had zero performance issues with screen drawing, so `tcell` must be handling the updates efficiently. I'm comfortable just trusting `tcell` here, as it keeps my code cleaner and far simpler (with probable equivalent or superior performance).
Author
Owner

@rivo commented on GitHub (Jul 17, 2018):

I'm glad that you found a solution! If there's anything else regarding this topic, feel free to keep the discussion going. I haven't tested tcell's update performance myself so if it fails in some cases, maybe there's some merit in looking into optimizing on tview's side (or even examine tcell in more detail).

<!-- gh-comment-id:405702287 --> @rivo commented on GitHub (Jul 17, 2018): I'm glad that you found a solution! If there's anything else regarding this topic, feel free to keep the discussion going. I haven't tested `tcell`'s update performance myself so if it fails in some cases, maybe there's some merit in looking into optimizing on `tview`'s side (or even examine `tcell` in more detail).
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#98
No description provided.