1package nbview23import (4 "time"56 tea "charm.land/bubbletea/v2"78 "src.rest/cat/012/internal/notebook"9)1011// The mouse, as in JupyterLab: a click selects a cell, a click in its12// box edits it with the caret where it landed, ▶ runs it (■ stops it),13// Shift+click selects the cells between, a click on an output selects14// it and a double click opens it full-screen, a click left of an output15// folds it, and the wheel over an output's window scrolls it.1617// hit is what's at a line of the body.18type hit struct {19 cell int20 kind rowKind21 r int // which line of its kind22}2324// hitAt is what's at line y of the body, if a cell is there.25func (v *View) hitAt(y int) (hit, bool) {26 line := v.top + y27 at := 028 for i, c := range v.h.Cells() {29 b := v.blockOf(i, c)30 if line >= at && line < at+b.lines() {31 kind, r := b.row(line - at)32 return hit{cell: i, kind: kind, r: r}, true33 }34 at += b.lines()35 }36 return hit{}, false37}3839// inBox reports whether column x is inside a box's sides.40func (v *View) inBox(x int) bool { return x > gutter && x < gutter+v.content()+boxW-1 }4142// onRun reports whether column x is a box's ▶.43func (v *View) onRun(x int) bool {44 at := gutter + v.content() + boxW + 145 return x >= at-1 && x <= at46}4748// tap is a click, for telling a double click.49type tap struct {50 at time.Time51 cell int52 kind rowKind53}5455// Click takes a left click at column x of line y of the body; shift56// extends the selection to the cell clicked.57func (v *View) Click(x, y int, shift bool) tea.Cmd {58 if v.full != nil {59 return nil60 }61 h, ok := v.hitAt(y)62 if !ok {63 return nil64 }65 cells := v.h.Cells()66 c := cells[h.cell]67 boxed := v.boxed(h.cell, c)68 if v.edit.on && h.cell == v.sel && h.kind == rowSrc && boxed && v.inBox(x) {69 v.edit.area.Place(v.content(), h.r, x-textX)70 v.follow()71 return v.rest()72 }73 double := !shift && v.lastTap.cell == h.cell && v.lastTap.kind == h.kind && time.Since(v.lastTap.at) < 400*time.Millisecond74 v.lastTap = tap{at: time.Now(), cell: h.cell, kind: h.kind}75 v.StopEdit()76 if shift {77 from := v.sel78 if v.anchor >= 0 {79 from = v.anchor80 }81 v.SelectRange(from, h.cell)82 return nil83 }84 return v.clickCell(h, c, x, boxed, double)85}8687// clickCell is a plain click on cell c.88func (v *View) clickCell(h hit, c notebook.Cell, x int, boxed, double bool) tea.Cmd {89 switch {90 case h.kind == rowSrc && h.r == 0 && boxed && v.onRun(x):91 v.Select(h.cell, false)92 if v.h.State(c.ID).Running {93 return v.h.Run("nb.stop")94 }95 return v.h.Run("nb.run")96 case h.kind == rowOut && x < gutter:97 v.Select(h.cell, true)98 v.ToggleHidden()99 case h.kind == rowOut:100 v.Select(h.cell, true)101 if double {102 v.OpenFull()103 }104 case h.kind == rowSrc && boxed && v.inBox(x):105 v.Select(h.cell, false)106 cmd := v.StartEdit()107 v.edit.area.Place(v.content(), h.r, x-textX)108 v.follow()109 return cmd110 default:111 v.Select(h.cell, false)112 if double && c.Kind == notebook.Note {113 return v.StartEdit()114 }115 }116 return nil117}118119// RightClick selects the cell at line y for its context menu, unless120// it's one of the cells selected.121func (v *View) RightClick(y int) {122 h, ok := v.hitAt(y)123 if !ok {124 return125 }126 if from, to := v.Range(); h.cell < from || h.cell > to {127 v.StopEdit()128 v.Select(h.cell, false)129 }130}131132// Wheel scrolls d lines with the mouse at line y: the output's window133// under it while it has more that way, or the body.134func (v *View) Wheel(y, d int) {135 if v.full != nil {136 if g := v.full.Grid(); g != nil {137 g.Scroll(d)138 return139 }140 v.full.row += d141 v.full.settle()142 return143 }144 if h, ok := v.hitAt(y); ok && y >= 0 && h.kind == rowOut {145 if v.scrollBy(v.h.Cells()[h.cell], d) {146 return147 }148 }149 v.top = max(min(v.top+d, v.total()-v.height), 0)150}151152// Cursor is where the terminal's caret goes in the body, while a cell153// is edited.154func (v *View) Cursor() (x, y int, ok bool) {155 if !v.edit.on {156 return 0, 0, false157 }158 row, col := v.edit.caret(v.content())159 y = v.startOf(v.sel) + 1 + row - v.top160 if y < 0 || y >= v.height {161 return 0, 0, false162 }163 return textX + col, y, true164}