Files
kitty/kittens/choose_fonts/ui.go
2024-06-24 07:54:14 +05:30

393 lines
9.3 KiB
Go

package choose_fonts
import (
"fmt"
"strconv"
"strings"
"sync"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
type State int
const (
SCANNING_FAMILIES State = iota
LISTING_FAMILIES
CHOOSING_FACES
)
type TextStyle struct {
Font_sz float64 `json:"font_size"`
Dpi_x float64 `json:"dpi_x"`
Dpi_y float64 `json:"dpi_y"`
Foreground string `json:"foreground"`
Background string `json:"background"`
}
type handler struct {
lp *loop.Loop
fonts map[string][]ListedFont
state State
err_mutex sync.Mutex
err_in_worker_thread error
mouse_state tui.MouseState
render_count uint
render_lines tui.RenderLines
text_style TextStyle
// Listing
rl *readline.Readline
family_list FamilyList
variable_data_requested_for *utils.Set[string]
}
func (h *handler) set_worker_error(err error) {
h.err_mutex.Lock()
defer h.err_mutex.Unlock()
h.err_in_worker_thread = err
}
func (h *handler) get_worker_error() error {
h.err_mutex.Lock()
defer h.err_mutex.Unlock()
return h.err_in_worker_thread
}
// Listing families {{{
func (h *handler) draw_search_bar() {
h.lp.SetCursorVisible(true)
h.lp.SetCursorShape(loop.BAR_CURSOR, true)
sz, err := h.lp.ScreenSize()
if err != nil {
return
}
h.lp.MoveCursorTo(1, int(sz.HeightCells))
h.lp.ClearToEndOfLine()
h.rl.RedrawNonAtomic()
}
const SEPARATOR = "║"
func center_string(x string, width int) string {
l := wcswidth.Stringwidth(x)
spaces := int(float64(width-l) / 2)
return strings.Repeat(" ", utils.Max(0, spaces)) + x
}
func (h *handler) draw_family_summary(start_x int, sz loop.ScreenSize) (err error) {
family := h.family_list.CurrentFamily()
if family == "" || int(sz.WidthCells) < start_x+2 {
return nil
}
lines := []string{
h.lp.SprintStyled("fg=green bold", center_string(family, int(sz.WidthCells)-start_x)),
"",
}
width := int(sz.WidthCells) - start_x - 1
add_line := func(x string) {
lines = append(lines, style.WrapTextAsLines(x, width, style.WrapOptions{})...)
}
fonts := h.fonts[family]
if len(fonts) == 0 {
return fmt.Errorf("The family: %s has no fonts", family)
}
if has_variable_data_for_font(fonts[0]) {
s := styles_in_family(family, fonts)
for _, sg := range s.style_groups {
styles := sg.name + ": " + strings.Join(sg.styles, ", ")
add_line(styles)
add_line("")
}
if s.has_variable_faces {
add_line(fmt.Sprintf("This font is %s allowing for finer style control", h.lp.SprintStyled("fg=magenta", "variable")))
}
add_line(fmt.Sprintf("Press the %s key to choose this family", h.lp.SprintStyled("fg=yellow", "Enter")))
} else {
lines = append(lines, "Reading font data, please wait…")
key := fonts[0].cache_key()
if !h.variable_data_requested_for.Has(key) {
h.variable_data_requested_for.Add(key)
go func() {
h.set_worker_error(ensure_variable_data_for_fonts(fonts...))
h.lp.WakeupMainThread()
}()
}
}
for i, line := range lines {
if i >= int(sz.HeightCells)-1 {
break
}
h.lp.MoveCursorTo(start_x+1, i+1)
h.lp.QueueWriteString(line)
}
return
}
func (h *handler) draw_listing_screen() (err error) {
sz, err := h.lp.ScreenSize()
if err != nil {
return err
}
num_rows := max(0, int(sz.HeightCells)-1)
mw := h.family_list.max_width + 1
green_fg, _, _ := strings.Cut(h.lp.SprintStyled("fg=green", "|"), "|")
lines := make([]string, 0, num_rows)
for _, l := range h.family_list.Lines(num_rows) {
line := l.text
if l.is_current {
line = strings.ReplaceAll(line, MARK_AFTER, green_fg)
line = h.lp.SprintStyled("fg=green", ">") + h.lp.SprintStyled("fg=green bold", line)
} else {
line = " " + line
}
lines = append(lines, line)
}
_, _, str := h.render_lines.InRectangle(lines, 0, 0, 0, num_rows, &h.mouse_state, h.handle_click)
h.lp.QueueWriteString(str)
seps := strings.Repeat(SEPARATOR, num_rows)
seps = strings.TrimSpace(seps)
_, _, str = h.render_lines.InRectangle(strings.Split(seps, ""), mw+1, 0, 0, num_rows, &h.mouse_state)
h.lp.QueueWriteString(str)
if h.family_list.Len() > 0 {
if err = h.draw_family_summary(mw+3, sz); err != nil {
return err
}
}
h.draw_search_bar()
return
}
func (h *handler) handle_click(id string) error {
which, data, found := strings.Cut(id, ":")
if !found {
return fmt.Errorf("Not a valid click id: %s", id)
}
switch which {
case "family-chosen":
if h.state == LISTING_FAMILIES {
if h.family_list.Select(data) {
h.draw_screen()
} else {
h.lp.Beep()
}
}
}
return nil
}
func (h *handler) update_family_search() {
text := h.rl.AllText()
if h.family_list.UpdateSearch(text) {
h.draw_screen()
} else {
h.draw_search_bar()
}
}
func (h *handler) next(delta int, allow_wrapping bool) {
if h.family_list.Next(delta, allow_wrapping) {
h.draw_screen()
} else {
h.lp.Beep()
}
}
func (h *handler) handle_listing_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
if h.rl.AllText() != "" {
h.rl.ResetText()
h.update_family_search()
h.draw_screen()
} else {
return fmt.Errorf("canceled by user")
}
return
}
ev := event
if ev.MatchesPressOrRepeat("down") {
h.next(1, true)
ev.Handled = true
return nil
}
if ev.MatchesPressOrRepeat("up") {
h.next(-1, true)
ev.Handled = true
return nil
}
if ev.MatchesPressOrRepeat("page_down") {
ev.Handled = true
sz, err := h.lp.ScreenSize()
if err == nil {
h.next(int(sz.HeightCells)-3, false)
}
return nil
}
if ev.MatchesPressOrRepeat("page_up") {
ev.Handled = true
sz, err := h.lp.ScreenSize()
if err == nil {
h.next(3-int(sz.HeightCells), false)
}
return nil
}
if err = h.rl.OnKeyEvent(event); err != nil {
if err == readline.ErrAcceptInput {
return nil
}
return err
}
if event.Handled {
h.update_family_search()
}
h.draw_search_bar()
return
}
func (h *handler) handle_listing_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err != nil {
return err
}
h.update_family_search()
return
}
// }}}
// Events {{{
func (h *handler) initialize() {
h.lp.SetCursorVisible(false)
h.lp.OnQueryResponse = h.on_query_response
h.lp.QueryTerminal("font_size", "dpi_x", "dpi_y", "foreground", "background")
h.rl = readline.New(h.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "Family: "})
h.variable_data_requested_for = utils.NewSet[string](256)
h.draw_screen()
initialize_variable_data_cache()
go func() {
h.set_worker_error(kitty_font_backend.query("list_monospaced_fonts", nil, &h.fonts))
h.lp.WakeupMainThread()
}()
}
func (h *handler) finalize() {
h.lp.SetCursorVisible(true)
h.lp.SetCursorShape(loop.BLOCK_CURSOR, true)
}
func (h *handler) on_query_response(key, val string, valid bool) error {
if !valid {
return fmt.Errorf("Terminal does not support querying the: %s", key)
}
set_float := func(k, v string, dest *float64) error {
if fs, err := strconv.ParseFloat(v, 64); err == nil {
*dest = fs
} else {
return fmt.Errorf("Invalid response from terminal to %s query: %#v", k, v)
}
return nil
}
switch key {
case "font_size":
if err := set_float(key, val, &h.text_style.Font_sz); err != nil {
return err
}
case "dpi_x":
if err := set_float(key, val, &h.text_style.Dpi_x); err != nil {
return err
}
case "dpi_y":
if err := set_float(key, val, &h.text_style.Dpi_y); err != nil {
return err
}
case "foreground":
h.text_style.Foreground = val
case "background":
h.text_style.Background = val
}
return nil
}
func (h *handler) draw_screen() (err error) {
h.render_count++
h.lp.StartAtomicUpdate()
defer func() {
h.mouse_state.UpdateHoveredIds()
h.mouse_state.ApplyHoverStyles(h.lp)
h.lp.EndAtomicUpdate()
}()
h.lp.ClearScreen()
h.lp.AllowLineWrapping(false)
h.mouse_state.ClearCellRegions()
switch h.state {
case SCANNING_FAMILIES:
h.lp.Println("Scanning system for fonts, please wait...")
case LISTING_FAMILIES:
return h.draw_listing_screen()
}
return
}
func (h *handler) on_wakeup() (err error) {
if err = h.get_worker_error(); err != nil {
return
}
switch h.state {
case SCANNING_FAMILIES:
h.state = LISTING_FAMILIES
h.family_list.UpdateFamilies(utils.StableSortWithKey(utils.Keys(h.fonts), strings.ToLower))
case LISTING_FAMILIES:
}
return h.draw_screen()
}
func (h *handler) on_mouse_event(event *loop.MouseEvent) (err error) {
rc := h.render_count
redraw_needed := false
if h.mouse_state.UpdateState(event) {
redraw_needed = true
}
if event.Event_type == loop.MOUSE_CLICK && event.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
if err = h.mouse_state.ClickHoveredRegions(); err != nil {
return
}
}
if redraw_needed && rc == h.render_count {
err = h.draw_screen()
}
return
}
func (h *handler) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("ctrl+c") {
event.Handled = true
return fmt.Errorf("canceled by user")
}
switch h.state {
case LISTING_FAMILIES:
return h.handle_listing_key_event(event)
}
return
}
func (h *handler) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
switch h.state {
case LISTING_FAMILIES:
return h.handle_listing_text(text, from_key_event, in_bracketed_paste)
}
return
}
// }}}