| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
BubbleTea and lipgloss allow you to build extremely fast terminal interfaces, in a semantic and scalable way. Through abstracting layout, colors, events, and more, it's very easy to build a user-friendly application. BubbleTea also supports mouse events, either through the "basic" mouse events, like MouseButtonLeft, MouseButtonRight, MouseButtonWheelUp and MouseButtonWheelDown (and more), or through full motion tracking, allowing hover and mouse movement tracking.
This works great for a single-component application, where the state is managed in one location. However, when you start expanding your application, where components have various children, and those children have children, calculating mouse events like MouseButtonLeft and MouseButtonRight and determining which component was clicked becomes complicated, and rather tedious.
BubbleZone is one solution to this problem. BubbleZone allows you to wrap your components in zero-printable-width (to not impact lipgloss.Width() calculations) identifiers. Additionally, there is a scan method that wraps the entire application, stores the offsets of those identifiers as zones, and then removes them from the resulting output.
Any time there is a mouse event, pass it down to all children, thus allowing you to easily check if the event is within the bounds of the components zone. This makes it very simple to do things like focusing on various components, clicking "buttons", and more. Take a look at this example, where I didn't have to calculate where the mouse was being clicked, and which component was under the mouse:
go get -u github.com/lrstanley/bubblezone/v2@latestBubbleZone supports either a global zone manager (initialized via NewGlobal()), or non-global (via New()). Using the global zone manager, simply use zone.<method>. The below examples will use the global manager.
Initialize the zone manager:
package main
import (
// [...]
zone "github.com/lrstanley/bubblezone/v2"
)
func main() {
// [...]
zone.NewGlobal()
// If the UI will be closed at some point and the application will still run,
// use zone.Close() to stop all background workers:
// defer zone.Close()
//
// [...]
//
// Initialize your application here.
}In your root model, wrap your View() output in zone.Scan(), which will register and monitor all zones, including stripping the ANSI sequences injected by zone.Mark().
func (r app) View() tea.View {
var view tea.View
// Ensure that alt-screen is enabled, as bubblezone will only work in alt-screen mode.
view.AltScreen = true
// Enable mouse motion tracking.
view.MouseMode = tea.MouseModeCellMotion
// Wrap view in [zone.Scan].
view.SetContent(zone.Scan(r.someStyle.Render(generatedChildViews)))
return view
}In your children models View() method, use zone.Mark() to wrap the area you want to mark as a zone. Make sure you give the zone a unique ID (see also: tips: overlapping markers):
func (m model) View() string {
// [...]
buttons := lipgloss.JoinHorizontal(
lipgloss.Top,
zone.Mark("confirm", okButton),
zone.Mark("cancel", cancelButton),
)
return m.someStyle.Render(buttons)
}In your children models Update() method, use zone.Get(<id>).InBounds(mouseMsg) to check if the mouse event was in the bounds of the zone:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// [...]
case tea.MouseReleaseMsg:
if msg.Button != tea.MouseLeft {
return m, nil
}
if zone.Get("confirm").InBounds(msg) {
// Do something if it's in bounds, e.g. toggling a model flag to let
// View() know to change its highlight colors.
m.active = "confirm"
} else if zone.Get("cancel").InBounds(msg) {
m.active = "cancel"
}
// x, y := zone.Get("confirm").Pos() can be used to get the relative
// coordinates within the zone. Useful if you need to move a cursor in a
// input box as an example.
return m, nil
}
return m, nil
}... and that's it!
Below are a couple of tips to ensure you have the best experience using BubbleZone.
To prevent overlapping marker ID's in child components, use NewPrefix() which will generate a guaranteed-unique prefix you can use in combination with your regular IDs.
Use lipgloss.Width() for width measurements, rather than len() or similar. BubbleZone has been specifically designed so that markers will be ignored by lipgloss.Width() (in addition to this being the recommended width checking method even if you're not using BubbleZone, as len() breaks with fg/bg colors, and other control characters).
MaxHeight() and MaxWidth() do a hard-trim of characters to enforce a specific height and width. As such, if a child component is wrapped in a zone, and overlaps the maximum height/width, the zone will break, and standard bounds checks will not work. Due to this, it is recommended to ensure MaxHeight and MaxWidth() are only enforcing limits that should already be set by normal height/width limits on your components (i.e. just don't exceed the max viewport dimensions 😅).
Make sure zone.Scan() is only used at the root level model, it will likely not work as you intend it in any other situation.
BubbleZones InBounds() checks calculate bounds based on a box region. For example, if you have a model that generates a large circle, make sure the zone is properly padded (e.g. lipgloss.Place() or similar), to capture the entire circle. Though note that because it checks for the entire box, a mouse event will still be considered in bounds if the outer corners outside of the circle are clicked.
Example:
Caution
bubblezone v2 may not work when using the lipgloss v2 canvas/compositor. lipgloss/bubbletea v2 have some more native features for mouse event tracking. That said, I do plan to release another library that covers advanced layouts/layering/etc with improved mouse event tracking (that's even better than bubblezone).
MIT License Copyright (c) 2022 Liam Stanley <liam@liam.sh> Permission is hereby granted, free of charge, to any person obtaining a copy of this 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. 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.
Also located here
| Back | FazBrowse Home | New Git URL |