[ Guides ]
Mouse Tracking and Hit-Testing
Enable SGR mouse capture, tag widgets with Id tokens, handle left/right clicks and scroll wheel events, and optionally enable drag or hover motion tracking.
Complete working example
Section titled “Complete working example”#include <koto/koto.hpp>#include <cstdio>#include <string>
using namespace koto;
int main(){ App app; FocusManager fm;
ListState items({"Item Alpha", "Item Beta", "Item Gamma", "Item Delta"}); int click_counter = 0; std::string last_hit = "None";
fm.add(items); app.use_focus(fm);
app.on_mouse([&](const MouseEvent& mouse) { if (mouse.is_left_press()) { if (mouse.target == Id("card_counter")) { click_counter++; last_hit = "card_counter"; } else if (mouse.target == Id(&items)) { last_hit = "items list"; } } });
app.on_key([&](const KeyEvent& key) { if (key.ch == 'q' || key.is(Key::Escape)) { app.quit(); } });
app.on_render([&](Frame& frame) { auto [left_area, right_area] = Layout::horizontal(frame.area(), { Constraint::percent(50), Constraint::fill(), });
List(items) .title("Click or Wheel Scroll") .border_style(BorderStyle::Rounded) .highlight_symbol("> ") .highlight_fg(Color::Emerald) .draw(frame, left_area);
auto [card_area, btn_area] = Layout::vertical(right_area, { Constraint::fill(), Constraint::fixed(3), });
Box() .title("Clickable Panel") .border_style(BorderStyle::Rounded) .id(Id("card_counter")) .draw(frame, card_area);
std::string info = "Clicks: " + std::to_string(click_counter) + "\nLast target: " + last_hit; Text(info).draw(frame, card_area.inset(1, 2, 1, 2));
Button("Reset Counter") .id(Id("btn_reset")) .focus(fm) .on_click([&]() { click_counter = 0; }) .draw(frame, btn_area); });
if (auto res = app.run(); !res) { std::fprintf(stderr, "Error: %.*s\n", static_cast<int>(res.error().message().size()), res.error().message().data()); return 1; } return 0;}Step-by-step explanation
Section titled “Step-by-step explanation”- Enabling mouse capture: Calling either
app.on_mouse(...)orapp.use_focus(fm)invokesm_term.enable_mouse_capture()([?1000h[?1006h), which enables SGR 1006 click and wheel reporting (src/app.cpp:31, 53,src/terminal.cpp:373). - Automatic
MouseEvent::targetenrichment: When aMouseEventarrives inApp::dispatch_event,Appsetsm.target = m_frame.hit_at(m.row, m.col)before callingm_mouse_handler(m)andm_focus_manager->handle_mouse(m, m_frame)(src/app.cpp:146-160). - Sub-item routing in
FocusManager:- Left-clicking a
List,Tabs, orButtonregistered inFocusManagerfocuses that widget and forwards theMouseEventtoListState::handle_mouse(selecting the clicked row),TabsState::handle_mouse(selecting the clicked tab), orButton’son_clickcallback (src/focus.cpp:673-681). - Scrolling
MouseButton::WheelUporMouseButton::WheelDownover a registeredList,Tabs, orTextArearoutes the wheel event to the hovered widget viaframe.hit_at(mouse.row, mouse.col)even if another widget currently holds keyboard focus (src/focus.cpp:663-670).
- Left-clicking a
Common mistakes
Section titled “Common mistakes”- Drawing child text over a tagged
Boxand losing the tag: Wait—doesText::draw()overwrite thehit buffer? Let’s checksrc/frame.cpp:127:Frame::set_cell(row, col, c, tag_id)only writes tom_hit_bufferwhen!tag_id.is_none(). BecauseText::draw()callsframe.set_cellwith the defaulttag_id = Id::none(), drawingTextinside a taggedBoxpreserves theBox’sidinm_hit_buffer(src/frame.cpp:127-140). However, if you draw an untagged child widget that explicitly passes a differentId, the child’s tag overwrites those cells. - Expecting
MouseAction::Movewithout enabling drag or hover: Standardenable_mouse_capture()enables mode1000h(press, release, and wheel only) to avoid flooding stdin with motion events (src/terminal.cpp:371-373). CallTerminal::enable_mouse_drag()(?1002h) orTerminal::enable_mouse_hover()(?1003h) if you needMouseAction::Moveevents.