123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package tui
- import (
- "fmt"
- "path/filepath"
- "time"
- "kitty"
- "kitty/tools/config"
- "kitty/tools/tui/loop"
- "kitty/tools/utils"
- )
- var _ = fmt.Print
- type LinePos interface {
- LessThan(other LinePos) bool
- Equal(other LinePos) bool
- MinX() int
- MaxX() int
- }
- type SelectionBoundary struct {
- line LinePos
- x int
- in_first_half_of_cell bool
- }
- func (self *SelectionBoundary) LessThan(other *SelectionBoundary) bool {
- if self.line.LessThan(other.line) {
- return true
- }
- if !self.line.Equal(other.line) {
- return false
- }
- if self.x == other.x {
- return !self.in_first_half_of_cell && other.in_first_half_of_cell
- }
- return self.x < other.x
- }
- func (self *SelectionBoundary) Equal(other SelectionBoundary) bool {
- if self.x != other.x || self.in_first_half_of_cell != other.in_first_half_of_cell {
- return false
- }
- if self.line == nil {
- return other.line == nil
- }
- return self.line.Equal(other.line)
- }
- type MouseSelection struct {
- start, end SelectionBoundary
- is_active bool
- min_y, max_y int
- cell_width, cell_height int
- drag_scroll struct {
- timer_id loop.IdType
- pixel_gap int
- mouse_event loop.MouseEvent
- }
- }
- func (self *MouseSelection) IsEmpty() bool { return self.start.Equal(self.end) }
- func (self *MouseSelection) IsActive() bool { return self.is_active }
- func (self *MouseSelection) Finish() { self.is_active = false }
- func (self *MouseSelection) Clear() { *self = MouseSelection{} }
- func (ms *MouseSelection) StartNewSelection(ev *loop.MouseEvent, line LinePos, min_y, max_y, cell_width, cell_height int) {
- *ms = MouseSelection{cell_width: cell_width, cell_height: cell_height, min_y: min_y, max_y: max_y}
- ms.start.line = line
- ms.start.x = max(line.MinX(), min(ev.Cell.X, line.MaxX()))
- cell_start := cell_width * ev.Cell.X
- ms.start.in_first_half_of_cell = ev.Pixel.X <= cell_start+cell_width/2
- ms.end = ms.start
- ms.is_active = true
- }
- func (ms *MouseSelection) Update(ev *loop.MouseEvent, line LinePos) {
- ms.drag_scroll.timer_id = 0
- if ms.is_active {
- ms.end.x = max(line.MinX(), min(ev.Cell.X, line.MaxX()))
- cell_start := ms.cell_width * ms.end.x
- ms.end.in_first_half_of_cell = ev.Pixel.X <= cell_start+ms.cell_width/2
- ms.end.line = line
- }
- }
- func (ms *MouseSelection) LineBounds(line_pos LinePos) (start_x, end_x int) {
- if ms.IsEmpty() {
- return -1, -1
- }
- a, b := &ms.start, &ms.end
- if b.LessThan(a) {
- a, b = b, a
- }
- adjust_end := func(x int, b *SelectionBoundary) (int, int) {
- if b.in_first_half_of_cell {
- if b.x > x {
- return x, b.x - 1
- }
- return -1, -1
- }
- return x, b.x
- }
- adjust_start := func(a *SelectionBoundary, x int) (int, int) {
- if a.in_first_half_of_cell {
- return a.x, x
- }
- if x > a.x {
- return a.x + 1, x
- }
- return -1, -1
- }
- adjust_both := func(a, b *SelectionBoundary) (int, int) {
- if a.in_first_half_of_cell {
- return adjust_end(a.x, b)
- } else {
- if b.in_first_half_of_cell {
- s, e := a.x+1, b.x-1
- if e <= s {
- return -1, -1
- }
- return s, e
- } else {
- return adjust_start(a, b.x)
- }
- }
- }
- if a.line.LessThan(line_pos) {
- if line_pos.LessThan(b.line) {
- return line_pos.MinX(), line_pos.MaxX()
- } else if b.line.Equal(line_pos) {
- return adjust_end(line_pos.MinX(), b)
- }
- } else if a.line.Equal(line_pos) {
- if line_pos.LessThan(b.line) {
- return adjust_start(a, line_pos.MaxX())
- } else if b.line.Equal(line_pos) {
- return adjust_both(a, b)
- }
- }
- return -1, -1
- }
- func FormatPartOfLine(sgr string, start_x, end_x, y int) string { // uses zero based indices
- // DECCARA used to set formatting in specified region using zero based indexing
- return fmt.Sprintf("\x1b[%d;%d;%d;%d;%s$r", y+1, start_x+1, y+1, end_x+1, sgr)
- }
- func (ms *MouseSelection) LineFormatSuffix(line_pos LinePos, sgr string, y int) string {
- s, e := ms.LineBounds(line_pos)
- if s > -1 {
- return FormatPartOfLine(sgr, s, e, y)
- }
- return ""
- }
- func (ms *MouseSelection) StartLine() LinePos {
- return ms.start.line
- }
- func (ms *MouseSelection) EndLine() LinePos {
- return ms.end.line
- }
- func (ms *MouseSelection) OutOfVerticalBounds(ev *loop.MouseEvent) bool {
- return ev.Pixel.Y < ms.min_y*ms.cell_height || ev.Pixel.Y > (ms.max_y+1)*ms.cell_height
- }
- func (ms *MouseSelection) DragScrollTick(timer_id loop.IdType, lp *loop.Loop, callback loop.TimerCallback, do_scroll func(int, *loop.MouseEvent) error) error {
- if !ms.is_active || ms.drag_scroll.timer_id != timer_id || ms.drag_scroll.pixel_gap == 0 {
- return nil
- }
- amt := 1
- if ms.drag_scroll.pixel_gap < 0 {
- amt *= -1
- }
- err := do_scroll(amt, &ms.drag_scroll.mouse_event)
- if err == nil {
- ms.drag_scroll.timer_id, _ = lp.AddTimer(50*time.Millisecond, false, callback)
- }
- return err
- }
- func (ms *MouseSelection) DragScroll(ev *loop.MouseEvent, lp *loop.Loop, callback loop.TimerCallback) {
- if !ms.is_active {
- return
- }
- upper := ms.min_y * ms.cell_height
- lower := (ms.max_y + 1) * ms.cell_height
- if ev.Pixel.Y < upper {
- ms.drag_scroll.pixel_gap = ev.Pixel.Y - upper
- } else if ev.Pixel.Y > lower {
- ms.drag_scroll.pixel_gap = ev.Pixel.Y - lower
- }
- if ms.drag_scroll.timer_id == 0 && ms.drag_scroll.pixel_gap != 0 {
- ms.drag_scroll.timer_id, _ = lp.AddTimer(50*time.Millisecond, false, callback)
- }
- ms.drag_scroll.mouse_event = *ev
- }
- type CellRegion struct {
- TopLeft, BottomRight struct{ X, Y int }
- Id string
- OnClick []func(id string) error
- }
- func (c CellRegion) Contains(x, y int) bool { // 0-based
- if c.TopLeft.Y > y || c.BottomRight.Y < y {
- return false
- }
- 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))
- }
- type MouseState struct {
- Cell, Pixel struct{ X, Y int }
- Pressed struct{ Left, Right, Middle, Fourth, Fifth, Sixth, Seventh bool }
- regions []*CellRegion
- region_id_map map[string][]*CellRegion
- hovered_ids *utils.Set[string]
- default_url_style struct {
- value string
- loaded bool
- }
- }
- func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion {
- 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}
- m.regions = append(m.regions, &cr)
- if m.region_id_map == nil {
- m.region_id_map = make(map[string][]*CellRegion)
- }
- m.region_id_map[id] = append(m.region_id_map[id], &cr)
- return &cr
- }
- func (m *MouseState) ClearCellRegions() {
- m.regions = nil
- m.region_id_map = nil
- m.hovered_ids = nil
- }
- func (m *MouseState) UpdateHoveredIds() (changed bool) {
- h := utils.NewSet[string]()
- for _, r := range m.regions {
- if r.Contains(m.Cell.X, m.Cell.Y) {
- h.Add(r.Id)
- }
- }
- changed = !h.Equal(m.hovered_ids)
- m.hovered_ids = h
- return
- }
- func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) {
- if m.hovered_ids == nil {
- return
- }
- hs := ""
- if len(style) == 0 {
- if !m.default_url_style.loaded {
- m.default_url_style.loaded = true
- conf := filepath.Join(utils.ConfigDir(), "kitty.conf")
- color, style := kitty.DefaultUrlColor, kitty.DefaultUrlStyle
- cp := config.ConfigParser{LineHandler: func(key, val string) error {
- switch key {
- case "url_color":
- color = val
- case "url_style":
- style = val
- }
- return nil
- },
- }
- _ = cp.ParseFiles(conf) // ignore errors and use defaults
- if style != "none" && style != "" {
- m.default_url_style.value = fmt.Sprintf("u=%s uc=%s", style, color)
- }
- }
- hs = m.default_url_style.value
- } else {
- hs = style[0]
- }
- is_hovered := false
- for id := range m.hovered_ids.Iterable() {
- for _, r := range m.region_id_map[id] {
- lp.StyleRegion(hs, r.TopLeft.X, r.TopLeft.Y, r.BottomRight.X, r.BottomRight.Y)
- is_hovered = true
- }
- }
- if is_hovered {
- if s, has := lp.CurrentPointerShape(); !has || s != loop.POINTER_POINTER {
- lp.PushPointerShape(loop.POINTER_POINTER)
- }
- } else {
- lp.ClearPointerShapes()
- }
- }
- func (m *MouseState) ClickHoveredRegions() error {
- seen := utils.NewSet[string]()
- for id := range m.hovered_ids.Iterable() {
- for _, r := range m.region_id_map[id] {
- if seen.Has(r.Id) {
- continue
- }
- seen.Add(r.Id)
- for _, f := range r.OnClick {
- if err := f(r.Id); err != nil {
- return err
- }
- }
- }
- }
- return nil
- }
- func (m *MouseState) UpdateState(ev *loop.MouseEvent) (hovered_ids_changed bool) {
- m.Cell = ev.Cell
- m.Pixel = ev.Pixel
- if ev.Event_type == loop.MOUSE_PRESS || ev.Event_type == loop.MOUSE_RELEASE {
- pressed := ev.Event_type == loop.MOUSE_PRESS
- if ev.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
- m.Pressed.Left = pressed
- }
- if ev.Buttons&loop.RIGHT_MOUSE_BUTTON != 0 {
- m.Pressed.Right = pressed
- }
- if ev.Buttons&loop.MIDDLE_MOUSE_BUTTON != 0 {
- m.Pressed.Middle = pressed
- }
- if ev.Buttons&loop.FOURTH_MOUSE_BUTTON != 0 {
- m.Pressed.Fourth = pressed
- }
- if ev.Buttons&loop.FIFTH_MOUSE_BUTTON != 0 {
- m.Pressed.Fifth = pressed
- }
- if ev.Buttons&loop.SIXTH_MOUSE_BUTTON != 0 {
- m.Pressed.Sixth = pressed
- }
- if ev.Buttons&loop.SEVENTH_MOUSE_BUTTON != 0 {
- m.Pressed.Seventh = pressed
- }
- }
- return m.UpdateHoveredIds()
- }
|