mouse.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package tui
  3. import (
  4. "fmt"
  5. "path/filepath"
  6. "time"
  7. "kitty"
  8. "kitty/tools/config"
  9. "kitty/tools/tui/loop"
  10. "kitty/tools/utils"
  11. )
  12. var _ = fmt.Print
  13. type LinePos interface {
  14. LessThan(other LinePos) bool
  15. Equal(other LinePos) bool
  16. MinX() int
  17. MaxX() int
  18. }
  19. type SelectionBoundary struct {
  20. line LinePos
  21. x int
  22. in_first_half_of_cell bool
  23. }
  24. func (self *SelectionBoundary) LessThan(other *SelectionBoundary) bool {
  25. if self.line.LessThan(other.line) {
  26. return true
  27. }
  28. if !self.line.Equal(other.line) {
  29. return false
  30. }
  31. if self.x == other.x {
  32. return !self.in_first_half_of_cell && other.in_first_half_of_cell
  33. }
  34. return self.x < other.x
  35. }
  36. func (self *SelectionBoundary) Equal(other SelectionBoundary) bool {
  37. if self.x != other.x || self.in_first_half_of_cell != other.in_first_half_of_cell {
  38. return false
  39. }
  40. if self.line == nil {
  41. return other.line == nil
  42. }
  43. return self.line.Equal(other.line)
  44. }
  45. type MouseSelection struct {
  46. start, end SelectionBoundary
  47. is_active bool
  48. min_y, max_y int
  49. cell_width, cell_height int
  50. drag_scroll struct {
  51. timer_id loop.IdType
  52. pixel_gap int
  53. mouse_event loop.MouseEvent
  54. }
  55. }
  56. func (self *MouseSelection) IsEmpty() bool { return self.start.Equal(self.end) }
  57. func (self *MouseSelection) IsActive() bool { return self.is_active }
  58. func (self *MouseSelection) Finish() { self.is_active = false }
  59. func (self *MouseSelection) Clear() { *self = MouseSelection{} }
  60. func (ms *MouseSelection) StartNewSelection(ev *loop.MouseEvent, line LinePos, min_y, max_y, cell_width, cell_height int) {
  61. *ms = MouseSelection{cell_width: cell_width, cell_height: cell_height, min_y: min_y, max_y: max_y}
  62. ms.start.line = line
  63. ms.start.x = max(line.MinX(), min(ev.Cell.X, line.MaxX()))
  64. cell_start := cell_width * ev.Cell.X
  65. ms.start.in_first_half_of_cell = ev.Pixel.X <= cell_start+cell_width/2
  66. ms.end = ms.start
  67. ms.is_active = true
  68. }
  69. func (ms *MouseSelection) Update(ev *loop.MouseEvent, line LinePos) {
  70. ms.drag_scroll.timer_id = 0
  71. if ms.is_active {
  72. ms.end.x = max(line.MinX(), min(ev.Cell.X, line.MaxX()))
  73. cell_start := ms.cell_width * ms.end.x
  74. ms.end.in_first_half_of_cell = ev.Pixel.X <= cell_start+ms.cell_width/2
  75. ms.end.line = line
  76. }
  77. }
  78. func (ms *MouseSelection) LineBounds(line_pos LinePos) (start_x, end_x int) {
  79. if ms.IsEmpty() {
  80. return -1, -1
  81. }
  82. a, b := &ms.start, &ms.end
  83. if b.LessThan(a) {
  84. a, b = b, a
  85. }
  86. adjust_end := func(x int, b *SelectionBoundary) (int, int) {
  87. if b.in_first_half_of_cell {
  88. if b.x > x {
  89. return x, b.x - 1
  90. }
  91. return -1, -1
  92. }
  93. return x, b.x
  94. }
  95. adjust_start := func(a *SelectionBoundary, x int) (int, int) {
  96. if a.in_first_half_of_cell {
  97. return a.x, x
  98. }
  99. if x > a.x {
  100. return a.x + 1, x
  101. }
  102. return -1, -1
  103. }
  104. adjust_both := func(a, b *SelectionBoundary) (int, int) {
  105. if a.in_first_half_of_cell {
  106. return adjust_end(a.x, b)
  107. } else {
  108. if b.in_first_half_of_cell {
  109. s, e := a.x+1, b.x-1
  110. if e <= s {
  111. return -1, -1
  112. }
  113. return s, e
  114. } else {
  115. return adjust_start(a, b.x)
  116. }
  117. }
  118. }
  119. if a.line.LessThan(line_pos) {
  120. if line_pos.LessThan(b.line) {
  121. return line_pos.MinX(), line_pos.MaxX()
  122. } else if b.line.Equal(line_pos) {
  123. return adjust_end(line_pos.MinX(), b)
  124. }
  125. } else if a.line.Equal(line_pos) {
  126. if line_pos.LessThan(b.line) {
  127. return adjust_start(a, line_pos.MaxX())
  128. } else if b.line.Equal(line_pos) {
  129. return adjust_both(a, b)
  130. }
  131. }
  132. return -1, -1
  133. }
  134. func FormatPartOfLine(sgr string, start_x, end_x, y int) string { // uses zero based indices
  135. // DECCARA used to set formatting in specified region using zero based indexing
  136. return fmt.Sprintf("\x1b[%d;%d;%d;%d;%s$r", y+1, start_x+1, y+1, end_x+1, sgr)
  137. }
  138. func (ms *MouseSelection) LineFormatSuffix(line_pos LinePos, sgr string, y int) string {
  139. s, e := ms.LineBounds(line_pos)
  140. if s > -1 {
  141. return FormatPartOfLine(sgr, s, e, y)
  142. }
  143. return ""
  144. }
  145. func (ms *MouseSelection) StartLine() LinePos {
  146. return ms.start.line
  147. }
  148. func (ms *MouseSelection) EndLine() LinePos {
  149. return ms.end.line
  150. }
  151. func (ms *MouseSelection) OutOfVerticalBounds(ev *loop.MouseEvent) bool {
  152. return ev.Pixel.Y < ms.min_y*ms.cell_height || ev.Pixel.Y > (ms.max_y+1)*ms.cell_height
  153. }
  154. func (ms *MouseSelection) DragScrollTick(timer_id loop.IdType, lp *loop.Loop, callback loop.TimerCallback, do_scroll func(int, *loop.MouseEvent) error) error {
  155. if !ms.is_active || ms.drag_scroll.timer_id != timer_id || ms.drag_scroll.pixel_gap == 0 {
  156. return nil
  157. }
  158. amt := 1
  159. if ms.drag_scroll.pixel_gap < 0 {
  160. amt *= -1
  161. }
  162. err := do_scroll(amt, &ms.drag_scroll.mouse_event)
  163. if err == nil {
  164. ms.drag_scroll.timer_id, _ = lp.AddTimer(50*time.Millisecond, false, callback)
  165. }
  166. return err
  167. }
  168. func (ms *MouseSelection) DragScroll(ev *loop.MouseEvent, lp *loop.Loop, callback loop.TimerCallback) {
  169. if !ms.is_active {
  170. return
  171. }
  172. upper := ms.min_y * ms.cell_height
  173. lower := (ms.max_y + 1) * ms.cell_height
  174. if ev.Pixel.Y < upper {
  175. ms.drag_scroll.pixel_gap = ev.Pixel.Y - upper
  176. } else if ev.Pixel.Y > lower {
  177. ms.drag_scroll.pixel_gap = ev.Pixel.Y - lower
  178. }
  179. if ms.drag_scroll.timer_id == 0 && ms.drag_scroll.pixel_gap != 0 {
  180. ms.drag_scroll.timer_id, _ = lp.AddTimer(50*time.Millisecond, false, callback)
  181. }
  182. ms.drag_scroll.mouse_event = *ev
  183. }
  184. type CellRegion struct {
  185. TopLeft, BottomRight struct{ X, Y int }
  186. Id string
  187. OnClick []func(id string) error
  188. }
  189. func (c CellRegion) Contains(x, y int) bool { // 0-based
  190. if c.TopLeft.Y > y || c.BottomRight.Y < y {
  191. return false
  192. }
  193. return (y > c.TopLeft.Y || (y == c.TopLeft.Y && x >= c.TopLeft.X)) && (y < c.BottomRight.Y || (y == c.BottomRight.Y && x <= c.BottomRight.X))
  194. }
  195. type MouseState struct {
  196. Cell, Pixel struct{ X, Y int }
  197. Pressed struct{ Left, Right, Middle, Fourth, Fifth, Sixth, Seventh bool }
  198. regions []*CellRegion
  199. region_id_map map[string][]*CellRegion
  200. hovered_ids *utils.Set[string]
  201. default_url_style struct {
  202. value string
  203. loaded bool
  204. }
  205. }
  206. func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion {
  207. cr := CellRegion{TopLeft: struct{ X, Y int }{start_x, start_y}, BottomRight: struct{ X, Y int }{end_x, end_y}, Id: id, OnClick: on_click}
  208. m.regions = append(m.regions, &cr)
  209. if m.region_id_map == nil {
  210. m.region_id_map = make(map[string][]*CellRegion)
  211. }
  212. m.region_id_map[id] = append(m.region_id_map[id], &cr)
  213. return &cr
  214. }
  215. func (m *MouseState) ClearCellRegions() {
  216. m.regions = nil
  217. m.region_id_map = nil
  218. m.hovered_ids = nil
  219. }
  220. func (m *MouseState) UpdateHoveredIds() (changed bool) {
  221. h := utils.NewSet[string]()
  222. for _, r := range m.regions {
  223. if r.Contains(m.Cell.X, m.Cell.Y) {
  224. h.Add(r.Id)
  225. }
  226. }
  227. changed = !h.Equal(m.hovered_ids)
  228. m.hovered_ids = h
  229. return
  230. }
  231. func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) {
  232. if m.hovered_ids == nil {
  233. return
  234. }
  235. hs := ""
  236. if len(style) == 0 {
  237. if !m.default_url_style.loaded {
  238. m.default_url_style.loaded = true
  239. conf := filepath.Join(utils.ConfigDir(), "kitty.conf")
  240. color, style := kitty.DefaultUrlColor, kitty.DefaultUrlStyle
  241. cp := config.ConfigParser{LineHandler: func(key, val string) error {
  242. switch key {
  243. case "url_color":
  244. color = val
  245. case "url_style":
  246. style = val
  247. }
  248. return nil
  249. },
  250. }
  251. _ = cp.ParseFiles(conf) // ignore errors and use defaults
  252. if style != "none" && style != "" {
  253. m.default_url_style.value = fmt.Sprintf("u=%s uc=%s", style, color)
  254. }
  255. }
  256. hs = m.default_url_style.value
  257. } else {
  258. hs = style[0]
  259. }
  260. is_hovered := false
  261. for id := range m.hovered_ids.Iterable() {
  262. for _, r := range m.region_id_map[id] {
  263. lp.StyleRegion(hs, r.TopLeft.X, r.TopLeft.Y, r.BottomRight.X, r.BottomRight.Y)
  264. is_hovered = true
  265. }
  266. }
  267. if is_hovered {
  268. if s, has := lp.CurrentPointerShape(); !has || s != loop.POINTER_POINTER {
  269. lp.PushPointerShape(loop.POINTER_POINTER)
  270. }
  271. } else {
  272. lp.ClearPointerShapes()
  273. }
  274. }
  275. func (m *MouseState) ClickHoveredRegions() error {
  276. seen := utils.NewSet[string]()
  277. for id := range m.hovered_ids.Iterable() {
  278. for _, r := range m.region_id_map[id] {
  279. if seen.Has(r.Id) {
  280. continue
  281. }
  282. seen.Add(r.Id)
  283. for _, f := range r.OnClick {
  284. if err := f(r.Id); err != nil {
  285. return err
  286. }
  287. }
  288. }
  289. }
  290. return nil
  291. }
  292. func (m *MouseState) UpdateState(ev *loop.MouseEvent) (hovered_ids_changed bool) {
  293. m.Cell = ev.Cell
  294. m.Pixel = ev.Pixel
  295. if ev.Event_type == loop.MOUSE_PRESS || ev.Event_type == loop.MOUSE_RELEASE {
  296. pressed := ev.Event_type == loop.MOUSE_PRESS
  297. if ev.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
  298. m.Pressed.Left = pressed
  299. }
  300. if ev.Buttons&loop.RIGHT_MOUSE_BUTTON != 0 {
  301. m.Pressed.Right = pressed
  302. }
  303. if ev.Buttons&loop.MIDDLE_MOUSE_BUTTON != 0 {
  304. m.Pressed.Middle = pressed
  305. }
  306. if ev.Buttons&loop.FOURTH_MOUSE_BUTTON != 0 {
  307. m.Pressed.Fourth = pressed
  308. }
  309. if ev.Buttons&loop.FIFTH_MOUSE_BUTTON != 0 {
  310. m.Pressed.Fifth = pressed
  311. }
  312. if ev.Buttons&loop.SIXTH_MOUSE_BUTTON != 0 {
  313. m.Pressed.Sixth = pressed
  314. }
  315. if ev.Buttons&loop.SEVENTH_MOUSE_BUTTON != 0 {
  316. m.Pressed.Seventh = pressed
  317. }
  318. }
  319. return m.UpdateHoveredIds()
  320. }