diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md index 3cae97a9e..cec33ccb9 100644 --- a/.claude/rules/frontend.md +++ b/.claude/rules/frontend.md @@ -20,7 +20,7 @@ Frontends subclass `implementation` and implement `lem-if:*` generics. (lem-if:set-view-pos impl view x y) ;; Rendering -(lem-if:render-line impl view x y objects height) +(lem-if:render-row impl view row) ; ROW is a laid-out lem-core/display:row (lem-if:clear-to-end-of-window impl view y) ``` diff --git a/extensions/pixel-demo/pixel-demo.lisp b/extensions/pixel-demo/pixel-demo.lisp index 9013df18b..9bbb53dac 100644 --- a/extensions/pixel-demo/pixel-demo.lisp +++ b/extensions/pixel-demo/pixel-demo.lisp @@ -64,8 +64,8 @@ (defun animation-step () "Perform one animation step." (when (and *demo-window* (eq *demo-mode* :animate)) - (let* ((char-width (lem-if:get-char-width (implementation))) - (char-height (lem-if:get-char-height (implementation))) + (let* ((char-width (lem-if:cell-width (implementation))) + (char-height (lem-if:cell-height (implementation))) (display-width (* (display-width) char-width)) (display-height (* (display-height) char-height)) (win-width (or (floating-window-pixel-width *demo-window*) @@ -177,8 +177,8 @@ A floating window follows your mouse cursor at pixel precision." "Update coordinate debug display." (when (and *demo-window* (eq *demo-mode* :debug)) (let* ((mouse-event (lem-core::last-mouse-event)) - (char-width (lem-if:get-char-width (implementation))) - (char-height (lem-if:get-char-height (implementation)))) + (char-width (lem-if:cell-width (implementation))) + (char-height (lem-if:cell-height (implementation)))) (multiple-value-bind (win-px win-py win-pw win-ph) (floating-window-pixel-bounds *demo-window*) (let ((content @@ -230,8 +230,8 @@ Shows real-time pixel and character coordinate information." (defun compare-animation-step () "Animate both windows for comparison." (when (eq *demo-mode* :compare) - (let* ((char-width (lem-if:get-char-width (implementation))) - (char-height (lem-if:get-char-height (implementation))) + (let* ((char-width (lem-if:cell-width (implementation))) + (char-height (lem-if:cell-height (implementation))) (display-width (* (display-width) char-width)) (display-height (* (display-height) char-height)) (win-width (* 20 char-width))) diff --git a/frontends/fake-interface/fake-interface.lisp b/frontends/fake-interface/fake-interface.lisp index 1f9aa94d0..241004004 100644 --- a/frontends/fake-interface/fake-interface.lisp +++ b/frontends/fake-interface/fake-interface.lisp @@ -97,24 +97,22 @@ (defmethod lem-if:object-height ((implementation fake-interface) object) 1) -(defmethod lem-if:render-line ((implementation fake-interface) view x y objects height) +(defmethod lem-if:render-row ((implementation fake-interface) view row) nil) (defmethod lem-if:clear-to-end-of-window ((implementation fake-interface) view y) nil) -(defmethod lem-if:get-char-width ((implementation fake-interface)) +(defmethod lem-if:cell-width ((implementation fake-interface)) 1) -(defmethod lem-if:get-char-height ((implementation fake-interface)) +(defmethod lem-if:cell-height ((implementation fake-interface)) 1) -(defmethod lem-if:render-line-on-modeline ((implementation fake-interface) - view - left-objects - right-objects - default-attribute - height) +(defmethod lem-if:render-modeline-row ((implementation fake-interface) + view + row + default-attribute) nil) (defmacro with-fake-interface (() &body body) diff --git a/frontends/ncurses/drawing-object.lisp b/frontends/ncurses/drawing-object.lisp deleted file mode 100644 index b0e92122d..000000000 --- a/frontends/ncurses/drawing-object.lisp +++ /dev/null @@ -1,29 +0,0 @@ -(defpackage :lem-ncurses/drawing-object - (:use :cl - :lem-core/display) - (:export :object-width - :object-height)) -(in-package :lem-ncurses/drawing-object) - -(defgeneric object-width (drawing-object)) - -(defmethod object-width ((drawing-object void-object)) - 0) - -(defmethod object-width ((drawing-object text-object)) - (lem-core:string-width (text-object-string drawing-object))) - -(defmethod object-width ((drawing-object eol-cursor-object)) - 0) - -(defmethod object-width ((drawing-object extend-to-eol-object)) - 0) - -(defmethod object-width ((drawing-object line-end-object)) - (lem-core:string-width (text-object-string drawing-object))) - -(defmethod object-width ((drawing-object image-object)) - 0) - -(defmethod object-height (drawing-object) - 1) diff --git a/frontends/ncurses/lem-ncurses.asd b/frontends/ncurses/lem-ncurses.asd index 1c33af19f..2036ca22f 100644 --- a/frontends/ncurses/lem-ncurses.asd +++ b/frontends/ncurses/lem-ncurses.asd @@ -12,7 +12,6 @@ (:file "style") (:file "key") (:file "attribute") - (:file "drawing-object") (:file "view") (:file "render") (:file "input") diff --git a/frontends/ncurses/ncurses.lisp b/frontends/ncurses/ncurses.lisp index 5f1c60bd3..3c1060820 100644 --- a/frontends/ncurses/ncurses.lisp +++ b/frontends/ncurses/ncurses.lisp @@ -85,28 +85,31 @@ (defmethod lem-if:view-height ((implementation ncurses) view) (lem-ncurses/view:view-height view)) -(defmethod lem-if:render-line ((implementation ncurses) - view x y objects height) - (lem-ncurses/render:render-line view x y objects)) +(defmethod lem-if:render-row ((implementation ncurses) view row) + (lem-ncurses/render:render-row view row)) -(defmethod lem-if:render-line-on-modeline ((implementation ncurses) - view - left-objects - right-objects - default-attribute - height) - (lem-ncurses/render:render-line-on-modeline view left-objects right-objects default-attribute)) +(defmethod lem-if:render-modeline-row ((implementation ncurses) view row default-attribute) + (lem-ncurses/render:render-modeline-row view row default-attribute)) -(defmethod lem-if:object-width ((implementation ncurses) drawing-object) - (lem-ncurses/drawing-object:object-width drawing-object)) +(defmethod lem-if:object-width ((implementation ncurses) + (drawing-object lem-core/display:image-object)) + 0) -(defmethod lem-if:object-height ((implementation ncurses) drawing-object) - (lem-ncurses/drawing-object:object-height drawing-object)) +(defmethod lem-if:object-height ((implementation ncurses) + (drawing-object lem-core/display:image-object)) + 1) + +(defmethod lem-if:object-ascent ((implementation ncurses) + (drawing-object lem-core/display:image-object)) + 1) (defmethod lem-if:clear-to-end-of-window ((implementation ncurses) view y) (lem-ncurses/render:clear-to-end-of-window view y)) -(defmethod lem-if:get-char-width ((implementation ncurses)) +(defmethod lem-if:cell-width ((implementation ncurses)) + 1) + +(defmethod lem-if:cell-height ((implementation ncurses)) 1) ;; for mouse control diff --git a/frontends/ncurses/render.lisp b/frontends/ncurses/render.lisp index e67f4387b..cd96fef35 100644 --- a/frontends/ncurses/render.lisp +++ b/frontends/ncurses/render.lisp @@ -1,8 +1,8 @@ (defpackage :lem-ncurses/render (:use :cl :lem-core/display) - (:export :render-line - :render-line-on-modeline + (:export :render-row + :render-modeline-row :clear-to-end-of-window)) (in-package :lem-ncurses/render) @@ -37,17 +37,6 @@ " " (lem:make-attribute :foreground (lem:color-to-hex-string (eol-cursor-object-color object))))) -(defmethod draw-object ((object extend-to-eol-object) x y view scrwin) - (let ((width (lem-if:view-width (lem:implementation) view))) - (when (< x width) - (print-string - scrwin - x - y - (make-string (- width x) :initial-element #\space) - (lem:make-attribute :background - (lem:color-to-hex-string (extend-to-eol-object-color object))))))) - (defmethod draw-object ((object line-end-object) x y view scrwin) (let ((string (text-object-string object)) (attribute (text-object-attribute object))) @@ -61,40 +50,43 @@ (defmethod draw-object ((object image-object) x y view scrwin) (values)) -(defun render-line-from-behind (view y objects scrwin) - (loop :with current-x := (lem-if:view-width (lem:implementation) view) - :for object :in objects - :do (decf current-x (lem-ncurses/drawing-object:object-width object)) - (draw-object object current-x y view scrwin))) - (defun clear-line (view x y) (charms/ll:wmove (lem-ncurses/view:view-scrwin view) y x) (charms/ll:wclrtoeol (lem-ncurses/view:view-scrwin view))) -(defun %render-line (view x y objects scrwin) - (loop :for object :in objects - :do (draw-object object x y view scrwin) - (incf x (lem-ncurses/drawing-object:object-width object)))) +(defun draw-row (view row scrwin) + "Draw ROW's background fill, then everything placed on it. +The fill is spaces carrying the background color, since a terminal cell only takes a color by +having a character written into it." + (let ((width (lem-if:view-width (lem:implementation) view))) + (when (and (row-fill-color row) + (< (row-fill-x row) width)) + (print-string scrwin + (row-fill-x row) + (row-top row) + (make-string (- width (row-fill-x row)) :initial-element #\space) + (lem:make-attribute :background + (lem:color-to-hex-string (row-fill-color row)))))) + (loop :for placement :in (row-placements row) + :do (draw-object (placement-object placement) + (placement-x placement) + (placement-top placement) + view + scrwin))) -(defun render-line (view x y objects) - (clear-line view x y) - (%render-line view x y objects (lem-ncurses/view:view-scrwin view))) +(defun render-row (view row) + (clear-line view 0 (row-top row)) + (draw-row view row (lem-ncurses/view:view-scrwin view))) -(defun render-line-on-modeline (view - left-objects - right-objects - default-attribute) +(defun render-modeline-row (view row default-attribute) + ;; the modeline gets its own curses window, so its row (laid out at top 0) needs no translation. (print-string (lem-ncurses/view:view-modeline-scrwin view) 0 - 0 + (row-top row) (make-string (lem-ncurses/view:view-width view) :initial-element #\space) default-attribute) - (%render-line view 0 0 left-objects (lem-ncurses/view:view-modeline-scrwin view)) - (render-line-from-behind view - 0 - right-objects - (lem-ncurses/view:view-modeline-scrwin view))) + (draw-row view row (lem-ncurses/view:view-modeline-scrwin view))) (defun clear-to-end-of-window (view y) (let ((win (lem-ncurses/view:view-scrwin view))) diff --git a/frontends/ncurses/view.lisp b/frontends/ncurses/view.lisp index b5d31c674..25225f85c 100644 --- a/frontends/ncurses/view.lisp +++ b/frontends/ncurses/view.lisp @@ -16,8 +16,8 @@ :set-view-pos :redraw-view-after :redraw-display-after - :render-line - :render-line-on-modeline + :render-row + :render-modeline-row :clear-to-end-of-window :update-display :set-last-print-cursor)) diff --git a/frontends/sdl2/display.lisp b/frontends/sdl2/display.lisp index ff3255866..b48955dbf 100644 --- a/frontends/sdl2/display.lisp +++ b/frontends/sdl2/display.lisp @@ -112,7 +112,7 @@ Retina to a 1x display).") :documentation "Pre-allocated SDL_Rect reused across all render calls to avoid heap-allocating a new rect + Lisp wrapper on every draw-rect, -fill-to-end-of-line, render-texture, etc. Mutated in-place by +fill-row, render-texture, etc. Mutated in-place by `call-with-scratch-rect' / `with-scratch-rect'; do not rely on its contents outside the dynamic extent of that call."))) diff --git a/frontends/sdl2/drawing.lisp b/frontends/sdl2/drawing.lisp index e941f850f..ddfb30837 100644 --- a/frontends/sdl2/drawing.lisp +++ b/frontends/sdl2/drawing.lisp @@ -65,102 +65,25 @@ Uses a sentinel key so it participates in the normal cache lifecycle "__folder_icon__" nil :folder surface) surface))) -(defgeneric object-width (drawing-object display)) +(defmethod lem-if:image-natural-size ((implementation lem-sdl2/sdl2:sdl2) image) + (values (sdl2:surface-width image) (sdl2:surface-height image))) -(defmethod object-width ((drawing-object void-object) display) - 0) - -(defun text-cell-width (drawing-object display) - "Cell-aligned pixel width of a text-object: string-width × char-width. -Mirrors lem-ncurses/drawing-object:object-width semantics (logical -column width) so SDL2 text aligns on the character grid regardless of -per-string SDL_ttf surface-width drift." - (* (lem-core:string-width (text-object-string drawing-object)) - (display:display-char-width display))) - -(defmethod object-width ((drawing-object text-object) display) - (text-cell-width drawing-object display)) - -(defmethod object-width ((drawing-object control-character-object) display) - (* 2 (display:display-char-width display))) - -(defmethod object-width ((drawing-object icon-object) display) - ;; Cell-aligned advance (typically 2 * char-width). The icon font's natural - ;; glyph surface is usually wider than this; draw-object scales it to fit. - (text-cell-width drawing-object display)) - -(defmethod object-width ((drawing-object folder-object) display) - (* 2 (display:display-char-width display))) - -(defmethod object-width ((drawing-object emoji-object) display) - (* (display:display-char-width display) 2 (length (text-object-string drawing-object)))) - -(defmethod object-width ((drawing-object eol-cursor-object) display) - 0) - -(defmethod object-width ((drawing-object extend-to-eol-object) display) - 0) - -(defmethod object-width ((drawing-object line-end-object) display) - (text-cell-width drawing-object display)) - -(defmethod object-width ((drawing-object image-object) display) - (or (image-object-width drawing-object) - (sdl2:surface-width (image-object-image drawing-object)))) - - -(defgeneric object-height (drawing-object display)) - -(defmethod object-height ((drawing-object void-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object text-object) display) - ;; Use the stable row cell-height (derived from font metrics at the - ;; display level) rather than the per-string SDL_ttf surface height. - ;; SDL_ttf can return slightly different surface heights for different - ;; strings (e.g. ones containing descenders like `p'/`g'/`y' versus - ;; ones without), which would otherwise leak into the background - ;; rectangle drawn by `draw-text-glyph-surface', producing the - ;; uneven-extent "padding around the problem letters" artefact at - ;; attribute boundaries on a highlighted row. The natural surface - ;; height is still read inside `draw-text-glyph-surface' directly - ;; from the surface for baseline-anchored glyph blitting. - (display:display-char-height display)) - -(defmethod object-height ((drawing-object icon-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object control-character-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object folder-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object emoji-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object eol-cursor-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object extend-to-eol-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object line-end-object) display) - (display:display-char-height display)) - -(defmethod object-height ((drawing-object image-object) display) - (or (image-object-height drawing-object) - (sdl2:surface-height (image-object-image drawing-object)))) - -(defmethod lem-if:object-width ((implementation lem-sdl2/sdl2:sdl2) drawing-object) +(defmethod lem-if:object-width ((implementation lem-sdl2/sdl2:sdl2) + (drawing-object folder-object)) (display:with-display (display) - (object-width drawing-object display))) + (* 2 (display:display-char-width display)))) -(defmethod lem-if:object-height ((implementation lem-sdl2/sdl2:sdl2) drawing-object) +(defmethod lem-if:object-width ((implementation lem-sdl2/sdl2:sdl2) + (drawing-object emoji-object)) (display:with-display (display) - (object-height drawing-object display))) + (* (display:display-char-width display) 2 (length (text-object-string drawing-object))))) + +(defgeneric draw-object (drawing-object x top display view) + (:documentation "Draw DRAWING-OBJECT into VIEW with its top-left corner at (X, TOP). +Returns the pixel width it occupied. +`lem-core/display:layout-row' already chose TOP as the row's baseline minus this object's ascent.")) -(defmethod draw-object ((drawing-object void-object) x bottom-y display view) +(defmethod draw-object ((drawing-object void-object) x top display view) 0) (defun draw-rect (display x y width height color) @@ -177,10 +100,10 @@ per-string SDL_ttf surface-width drift." (:underline (draw-rect display x (+ y surface-height -1) surface-width 1 background)))) -(defun draw-text-glyph-surface (drawing-object x bottom-y display view cell-width +(defun draw-text-glyph-surface (drawing-object x top display view cell-width &key clip (phase :both)) "Draws DRAWING-OBJECT's cached SDL surface in a (CELL-WIDTH × cell-height) slot -at (X, BOTTOM-Y). Cell-height is taken from the drawing-object's OBJECT-HEIGHT so +at (X, TOP). Cell-height is taken from the drawing-object's OBJECT-HEIGHT so non-text surfaces (folder PNG, icon font, emoji font) get scaled to the editor's character row height instead of being placed at their natural pixel height. @@ -208,18 +131,18 @@ is not erased by the next glyph's background fill." (let* ((surface (get-surface drawing-object display)) (surface-width (sdl2:surface-width surface)) (surface-height (sdl2:surface-height surface)) - (cell-height (object-height drawing-object display)) + (cell-height (object-height drawing-object)) (attribute (text-object-attribute drawing-object)) (background (lem-core:attribute-background-with-reverse attribute)) - (y (- bottom-y cell-height)) + (bottom-y (+ top cell-height)) (draw-width (if clip surface-width (min surface-width cell-width))) (draw-height (min surface-height cell-height))) (when (member phase '(:bg :both)) (cond ((and attribute (lem-core:cursor-attribute-p attribute)) - (lem-sdl2/view:set-cursor-position view x y) - (draw-cursor display x y cell-width cell-height background)) + (lem-sdl2/view:set-cursor-position view x top) + (draw-cursor display x top cell-width cell-height background)) (t - (draw-rect display x y cell-width cell-height background)))) + (draw-rect display x top cell-width cell-height background)))) (when (member phase '(:glyph :both)) (let ((texture (lem-sdl2/text-surface-cache:get-or-create-texture (display:display-renderer display) @@ -255,7 +178,7 @@ is not erased by the next glyph's background fill." :dest-rect dst-rect :flip (list :none))))) (t - (display:with-scratch-rect (dst-rect display x y draw-width draw-height) + (display:with-scratch-rect (dst-rect display x top draw-width draw-height) (sdl2:render-copy-ex (display:display-renderer display) texture :source-rect nil @@ -266,47 +189,47 @@ is not erased by the next glyph's background fill." (lem:attribute-underline attribute)) (display:render-line display x - (1- (+ y cell-height)) + (1- bottom-y) (+ x cell-width) - (1- (+ y cell-height)) + (1- bottom-y) :color (let ((underline (lem:attribute-underline attribute))) (if (eq underline t) (lem-core:attribute-foreground-color attribute) (or (lem:parse-color underline) (lem-core:attribute-foreground-color attribute)))))))) -(defun text-object-letter-objects-and-widths (drawing-object display) +(defun text-object-letter-objects-and-widths (drawing-object) "Return two parallel lists: per-character letter-objects and their cell widths, for the multi-character text run DRAWING-OBJECT." (let ((attribute (text-object-attribute drawing-object))) (loop :for c :across (text-object-string drawing-object) :for letter := (make-letter-object c attribute) :collect letter :into letters - :collect (object-width letter display) :into widths + :collect (object-width letter) :into widths :finally (return (values letters widths))))) -(defun draw-text-object-phase (drawing-object x bottom-y display view phase) +(defun draw-text-object-phase (drawing-object x top display view phase) "Render the text-object DRAWING-OBJECT for one of the two-pass phases (:BG or :GLYPH). PHASE :BG paints backgrounds, cursors and underlines for every cell of the run; PHASE :GLYPH blits each glyph at its natural surface width so the rasterizer's right-edge anti-aliasing tail is preserved." (let ((string (text-object-string drawing-object))) (cond ((<= (length string) 1) - (draw-text-glyph-surface drawing-object x bottom-y display view - (object-width drawing-object display) + (draw-text-glyph-surface drawing-object x top display view + (object-width drawing-object) :clip t :phase phase)) (t (multiple-value-bind (letter-objects letter-widths) - (text-object-letter-objects-and-widths drawing-object display) + (text-object-letter-objects-and-widths drawing-object) (loop :with current-x := x :for letter-object :in letter-objects :for letter-width :in letter-widths - :do (draw-text-glyph-surface letter-object current-x bottom-y + :do (draw-text-glyph-surface letter-object current-x top display view letter-width :clip t :phase phase) (incf current-x letter-width))))))) -(defmethod draw-object ((drawing-object text-object) x bottom-y display view) +(defmethod draw-object ((drawing-object text-object) x top display view) ;; Render each character individually on the cell grid. SDL_ttf's blended ;; surface for a multi-character string has metrics that do not equal the ;; sum of its per-character metrics, so a glyph would otherwise land on a @@ -317,81 +240,84 @@ width so the rasterizer's right-edge anti-aliasing tail is preserved." ;; ;; This per-text-object two-pass preserves the AA overhang within the run. ;; The cross-text-object equivalent (an adjacent text-object's :bg erasing - ;; the previous text-object's AA tail) is handled by `redraw-physical-line', - ;; which lifts the two-pass to span the entire physical line. - (let ((total-width (object-width drawing-object display))) - (draw-text-object-phase drawing-object x bottom-y display view :bg) - (draw-text-object-phase drawing-object x bottom-y display view :glyph) + ;; the previous text-object's AA tail) is handled by `draw-row-objects', + ;; which lifts the two-pass to span the entire row. + (let ((total-width (object-width drawing-object))) + (draw-text-object-phase drawing-object x top display view :bg) + (draw-text-object-phase drawing-object x top display view :glyph) total-width)) -(defmethod draw-object ((drawing-object icon-object) x bottom-y display view) +(defmethod draw-object ((drawing-object icon-object) x top display view) ;; Icon font glyphs typically render much wider than the 2-cell column the ;; layout reserves for them; draw-text-glyph-surface scales them down to fit. - (let ((cell-width (object-width drawing-object display))) - (draw-text-glyph-surface drawing-object x bottom-y display view cell-width) + (let ((cell-width (object-width drawing-object))) + (draw-text-glyph-surface drawing-object x top display view cell-width) cell-width)) -(defmethod draw-object ((drawing-object folder-object) x bottom-y display view) +(defmethod draw-object ((drawing-object folder-object) x top display view) ;; Folder PNG surface is much wider than 2 cells; render once and scale to fit. ;; Overrides the text-object per-character loop so the icon stays atomic. - (let ((cell-width (object-width drawing-object display))) - (draw-text-glyph-surface drawing-object x bottom-y display view cell-width) + (let ((cell-width (object-width drawing-object))) + (draw-text-glyph-surface drawing-object x top display view cell-width) cell-width)) -(defmethod draw-object ((drawing-object emoji-object) x bottom-y display view) +(defmethod draw-object ((drawing-object emoji-object) x top display view) ;; Emoji strings may span multiple codepoints (base + variation selector, ;; ZWJ sequences, ...) that must be rendered as one composed glyph. Use the ;; single-surface path with cell-aligned scaling so we neither split the ;; sequence per-codepoint nor let an oversized emoji surface spill over. - (let ((cell-width (object-width drawing-object display))) - (draw-text-glyph-surface drawing-object x bottom-y display view cell-width) + (let ((cell-width (object-width drawing-object))) + (draw-text-glyph-surface drawing-object x top display view cell-width) cell-width)) -(defmethod draw-object ((drawing-object eol-cursor-object) x bottom-y display view) +(defmethod draw-object ((drawing-object eol-cursor-object) x top display view) (display:set-render-color display (eol-cursor-object-color drawing-object)) - (let ((y (- bottom-y (object-height drawing-object display)))) - (lem-sdl2/view:set-cursor-position view x y) - (draw-cursor display - x - y - (display:display-char-width display) - (object-height drawing-object display) - (eol-cursor-object-color drawing-object))) - (object-width drawing-object display)) - -(defmethod draw-object ((drawing-object extend-to-eol-object) x bottom-y display view) - (display:set-render-color display (extend-to-eol-object-color drawing-object)) - (display:with-scratch-rect (rect display - x - (- bottom-y (display:display-char-height display)) - (- (lem-if:view-width (lem-core:implementation) view) x) - (display:display-char-height display)) - (sdl2:render-fill-rect (display:display-renderer display) rect)) - (object-width drawing-object display)) - -(defmethod draw-object ((drawing-object line-end-object) x bottom-y display view) + (lem-sdl2/view:set-cursor-position view x top) + (draw-cursor display + x + top + (display:display-char-width display) + (object-height drawing-object) + (eol-cursor-object-color drawing-object)) + (object-width drawing-object)) + +(defmethod draw-object ((drawing-object line-end-object) x top display view) (call-next-method drawing-object (+ x (* (line-end-object-offset drawing-object) (display:display-char-width display))) - bottom-y + top display view)) -(defmethod draw-object ((drawing-object image-object) x bottom-y display view) - (let* ((surface-width (object-width drawing-object display)) - (surface-height (object-height drawing-object display)) - (texture (sdl2:create-texture-from-surface (display:display-renderer display) - (image-object-image drawing-object))) - (y (- bottom-y surface-height))) - (display:with-scratch-rect (dest-rect display x y surface-width surface-height) - (sdl2:render-copy-ex (display:display-renderer display) - texture - :source-rect nil - :dest-rect dest-rect - :flip (list :none))) +(defmethod draw-object ((drawing-object image-object) x top display view) + (let* ((draw-width (max 1 (image-draw-width (lem-core:implementation) drawing-object))) + (visible-width (object-width drawing-object)) + (surface-height (object-height drawing-object)) + (surface (image-object-image drawing-object)) + (texture (sdl2:create-texture-from-surface (display:display-renderer display) surface))) + (if (< visible-width draw-width) + ;; copy the leading fraction of the source into a dest that wide + (sdl2:with-rects ((dest-rect x top visible-width surface-height) + (source-rect 0 + 0 + (max 1 (round (* (sdl2:surface-width surface) + visible-width) + draw-width)) + (sdl2:surface-height surface))) + (sdl2:render-copy-ex (display:display-renderer display) + texture + :source-rect source-rect + :dest-rect dest-rect + :flip (list :none))) + (display:with-scratch-rect (dest-rect display x top visible-width surface-height) + (sdl2:render-copy-ex (display:display-renderer display) + texture + :source-rect nil + :dest-rect dest-rect + :flip (list :none)))) (sdl2:destroy-texture texture) - surface-width)) + visible-width)) (defun plain-text-object-p (object) "True when OBJECT is an instance of the base `text-object' class (and not @@ -400,93 +326,85 @@ one of its specialised subclasses `icon-object', `folder-object', or rather than the row-wide two-pass)." (eq (class-of object) (find-class 'text-object))) -(defun redraw-physical-line (display view x y objects height) - ;; Two-pass over the whole physical line: paint every plain text-object's - ;; backgrounds first, then blit every plain text-object's glyphs. This - ;; preserves the 1-pixel right-edge AA tail at attribute boundaries (e.g. - ;; on the dashboard's highlighted row, where a `p' or `g' at the end of - ;; one attribute run would otherwise be eroded by the next text-object's +(defun draw-row-objects (display view row) + ;; Two-pass over the whole row: paint every plain text-object's backgrounds + ;; first, then blit every plain text-object's glyphs. This preserves the + ;; 1-pixel right-edge AA tail at attribute boundaries (e.g. on the + ;; dashboard's highlighted row, where a `p' or `g' at the end of one + ;; attribute run would otherwise be eroded by the next text-object's ;; full-width background fill). Everything else (icon / folder / emoji - ;; text-object subclasses, images, eol-cursor, extend-to-eol) draws fully - ;; in the first pass via its own `draw-object' method — those don't have - ;; AA overhang to preserve and need their bespoke rendering (scale-to-fit - ;; for icon/folder/emoji surfaces, fill for extend-to-eol, etc.). - (let* ((bottom-y (+ y height)) - (display-width (round (* (display:display-window-width display) + ;; text-object subclasses, images, eol-cursor) draws fully in the first pass + ;; via its own `draw-object' method — those don't have AA overhang to + ;; preserve and need their bespoke rendering (scale-to-fit for + ;; icon/folder/emoji surfaces, etc.). + (let* ((display-width (round (* (display:display-window-width display) (first (display:display-scale display))))) - (placed (loop :with current-x := x - :for object :in objects - :while (< current-x display-width) - :collect (cons object current-x) - :do (incf current-x (object-width object display))))) - (flet ((draw-text-pass (object obj-x phase) - ;; Honour the wrap-to-letters branch the old code used when a - ;; text-object would extend past the display width. - (cond ((< display-width - (+ obj-x (object-width object display))) - (loop :with current-x := obj-x - :for c :across (text-object-string object) - :while (< current-x display-width) - :for letter := (make-letter-object - c (text-object-attribute object)) - :for letter-width := (object-width letter display) - :do (draw-text-glyph-surface letter current-x bottom-y - display view letter-width - :clip t :phase phase) - (incf current-x letter-width))) - (t - (draw-text-object-phase object obj-x bottom-y - display view phase))))) + (placements (remove-if (lambda (placement) + (<= display-width (placement-x placement))) + (row-placements row)))) + (flet ((draw-text-pass (placement phase) + (let* ((object (placement-object placement)) + (x (placement-x placement)) + (top (placement-top placement))) + (cond ((< display-width (+ x (object-width object))) + ;; a run reaching past the edge is drawn letter by letter, as far as it fits. + (loop :with current-x := x + :for c :across (text-object-string object) + :while (< current-x display-width) + :for letter := (make-letter-object + c (text-object-attribute object)) + :for letter-width := (object-width letter) + :do (draw-text-glyph-surface letter current-x top + display view letter-width + :clip t :phase phase) + (incf current-x letter-width))) + (t + (draw-text-object-phase object x top display view phase)))))) ;; Pass 1: plain-text backgrounds + everything else (subclassed text- ;; objects and non-text objects) in normal order. - (loop :for (object . obj-x) :in placed - :do (if (plain-text-object-p object) - (draw-text-pass object obj-x :bg) - (draw-object object obj-x bottom-y display view))) + (loop :for placement :in placements + :do (if (plain-text-object-p (placement-object placement)) + (draw-text-pass placement :bg) + (draw-object (placement-object placement) + (placement-x placement) + (placement-top placement) + display + view))) ;; Pass 2: plain-text glyphs. - (loop :for (object . obj-x) :in placed - :when (plain-text-object-p object) - :do (draw-text-pass object obj-x :glyph))))) - -(defun redraw-physical-line-from-behind (display view objects) - (loop :with current-x := (lem-if:view-width (lem-core:implementation) view) - :and y := (lem-if:view-height (lem-core:implementation) view) - :for object :in objects - :do (decf current-x (object-width object display)) - (draw-object object current-x y display view))) - -(defun fill-to-end-of-line (display view x y height &optional default-attribute) - (display:with-scratch-rect (rect display x y (- (lem-if:view-width (lem-core:implementation) view) x) height) - (display:set-render-color display - (lem-core:attribute-background-color default-attribute)) + (loop :for placement :in placements + :when (plain-text-object-p (placement-object placement)) + :do (draw-text-pass placement :glyph))))) + +(defun fill-row (display view row x color) + "Paint COLOR from X to the right edge of VIEW, over ROW's full height." + (display:with-scratch-rect (rect display + x + (row-top row) + (- (lem-if:view-width (lem-core:implementation) view) x) + (row-height row)) + (display:set-render-color display color) (sdl2:render-fill-rect (display:display-renderer display) rect))) -(defmethod lem-if:render-line ((implementation lem-sdl2/sdl2:sdl2) view x y objects height) +(defun draw-row (display view row background) + "Blank ROW to BACKGROUND, paint whatever fill it carries, then draw everything placed on it. +Both fills cover the row's full height, which may exceed a single text line's height when a +tall object (e.g. an image) sits on the row." + (fill-row display view row 0 background) + (when (row-fill-color row) + (fill-row display view row (row-fill-x row) (row-fill-color row))) + (draw-row-objects display view row)) + +(defmethod lem-if:render-row ((implementation lem-sdl2/sdl2:sdl2) view row) (display:with-display (display) - (fill-to-end-of-line display view x y height) - (redraw-physical-line display view x y objects height))) - -(defmethod lem-if:render-line-on-modeline ((implementation lem-sdl2/sdl2:sdl2) - view - left-objects - right-objects - default-attribute - height) + (draw-row display view row (lem-core:attribute-background-color nil)))) + +(defmethod lem-if:render-modeline-row ((implementation lem-sdl2/sdl2:sdl2) view row + default-attribute) (display:with-display (display) - (fill-to-end-of-line display - view - 0 - (- (lem-if:view-height (lem-core:implementation) view) height) - height - default-attribute) - (redraw-physical-line display - view - 0 - (- (lem-if:view-height (lem-core:implementation) view) - (display:display-char-height display)) - left-objects - height) - (redraw-physical-line-from-behind display view right-objects))) + (draw-row display + view + (translate-row row (- (lem-if:view-height implementation view) (row-height row))) + (lem-core:attribute-background-color default-attribute)))) (defmethod lem-if:clear-to-end-of-window ((implementation lem-sdl2/sdl2:sdl2) view y) (display:with-display (display) diff --git a/frontends/sdl2/main.lisp b/frontends/sdl2/main.lisp index 3940e6ef3..5f46e8071 100644 --- a/frontends/sdl2/main.lisp +++ b/frontends/sdl2/main.lisp @@ -470,14 +470,27 @@ (values (display:scaled-char-width display x) (display:scaled-char-height display y)))))) -(defmethod lem-if:get-char-width ((implementation sdl2)) +(defmethod lem-if:cell-width ((implementation sdl2)) (display:with-display (display) (display:display-char-width display))) -(defmethod lem-if:get-char-height ((implementation sdl2)) +(defmethod lem-if:cell-height ((implementation sdl2)) (display:with-display (display) (display:display-char-height display))) +(defmethod lem-if:cell-pixel-size ((implementation sdl2)) + (display:with-display (display) + (values (display:display-char-width display) + (display:display-char-height display) + (display:display-font-ascent display)))) + +(defmethod lem-if:font-em-pixels ((implementation sdl2)) + (display:with-display (display) + ;; a high dpi display opens the font at a multiple of the configured size, and the cell + ;; metrics are measured from the font as opened, so this has to be that size and not the + ;; configured one. + (font-config-size (display:display-font-config display)))) + (defmethod lem-if:view-width ((implementation sdl2) view) (display:with-display (display) (* (display:display-char-width display) diff --git a/frontends/sdl2/sdl2.lisp b/frontends/sdl2/sdl2.lisp index 0ed1b3333..50bf770c5 100644 --- a/frontends/sdl2/sdl2.lisp +++ b/frontends/sdl2/sdl2.lisp @@ -8,6 +8,7 @@ (:default-initargs :name :sdl2 :redraw-after-modifying-floating-window nil - :underline-color-support t)) + :underline-color-support t + :image-support t)) (pushnew :lem-sdl2 *features*) diff --git a/frontends/sdl2/tree.lisp b/frontends/sdl2/tree.lisp index 998635296..036e17d22 100644 --- a/frontends/sdl2/tree.lisp +++ b/frontends/sdl2/tree.lisp @@ -29,7 +29,7 @@ (defmethod tree-view-scroll-vertically ((buffer tree-view-buffer) window n) (incf (tree-view-buffer-scroll-y buffer) n) (let* ((height (* (1- (window-height window)) - (lem-if:get-char-height (implementation)))) + (lem-if:cell-height (implementation)))) (last-y (max 0 (- (tree-view-buffer-height buffer) height)))) (cond ((< last-y (tree-view-buffer-scroll-y buffer)) diff --git a/frontends/server/frontend/dist/assets/index.css b/frontends/server/frontend/dist/assets/index.css index 3dfcc503b..d88bed8c2 100644 --- a/frontends/server/frontend/dist/assets/index.css +++ b/frontends/server/frontend/dist/assets/index.css @@ -1 +1 @@ -@keyframes lem-cursor-blink{0%,to{opacity:1}50%{opacity:0}}.lem-cursor{position:absolute;pointer-events:none;z-index:300;animation:lem-cursor-blink 1s step-end infinite;overflow:hidden;white-space:pre;line-height:1;box-sizing:border-box}.lem-editor__floating-window--bordered{padding:10px;border:none;border-radius:8px;box-shadow:4px 4px 16px #000,0 0 0 1px #80808033;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.lem-editor__mode-line{border:none;border-radius:4px;box-shadow:0 0 0 1px #80808080}.lem-editor__vertical-border{width:10px;background:linear-gradient(to right,transparent 4px,rgba(128,128,128,.5) 4px 6px,transparent 5px)}.lem-editor__horizontal-border{height:10px;background:linear-gradient(to bottom,transparent 0px,rgba(128,128,128,.5) 3px 5px,transparent 2px)} +@keyframes lem-cursor-blink{0%,to{opacity:1}50%{opacity:0}}.lem-cursor{pointer-events:none;z-index:300;white-space:pre;box-sizing:border-box;line-height:1;animation:1s step-end infinite lem-cursor-blink;position:absolute;overflow:hidden}.lem-editor__floating-window--bordered{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:none;border-radius:8px;padding:10px;box-shadow:4px 4px 16px #000,0 0 0 1px #80808033}.lem-editor__mode-line{border:none;border-radius:4px;box-shadow:0 0 0 1px #80808080}.lem-editor__vertical-border{background:linear-gradient(90deg,#0000 4px,#80808080 4px 6px,#0000 5px);width:10px}.lem-editor__horizontal-border{background:linear-gradient(#0000 0,#80808080 3px 5px,#0000 2px);height:10px} diff --git a/frontends/server/frontend/dist/assets/index.js b/frontends/server/frontend/dist/assets/index.js index a036fd0e9..1c50a8893 100644 --- a/frontends/server/frontend/dist/assets/index.js +++ b/frontends/server/frontend/dist/assets/index.js @@ -1 +1 @@ -(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))i(n);new MutationObserver(n=>{for(const s of n)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&i(r)}).observe(document,{childList:!0,subtree:!0});function t(n){const s={};return n.integrity&&(s.integrity=n.integrity),n.referrerPolicy&&(s.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?s.credentials="include":n.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function i(n){if(n.ep)return;n.ep=!0;const s=t(n);fetch(n.href,s)}})();var dist={},client={},models={},hasRequiredModels;function requireModels(){return hasRequiredModels||(hasRequiredModels=1,function(o){var e=models&&models.__extends||function(){var u=function(h,a){return u=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(c,p){c.__proto__=p}||function(c,p){for(var w in p)Object.prototype.hasOwnProperty.call(p,w)&&(c[w]=p[w])},u(h,a)};return function(h,a){if(typeof a!="function"&&a!==null)throw new TypeError("Class extends value "+String(a)+" is not a constructor or null");u(h,a);function c(){this.constructor=h}h.prototype=a===null?Object.create(a):(c.prototype=a.prototype,new c)}}();Object.defineProperty(o,"__esModule",{value:!0}),o.createJSONRPCNotification=o.createJSONRPCRequest=o.createJSONRPCSuccessResponse=o.createJSONRPCErrorResponse=o.JSONRPCErrorCode=o.JSONRPCErrorException=o.isJSONRPCResponses=o.isJSONRPCResponse=o.isJSONRPCRequests=o.isJSONRPCRequest=o.isJSONRPCID=o.JSONRPC=void 0,o.JSONRPC="2.0";var t=function(u){return typeof u=="string"||typeof u=="number"||u===null};o.isJSONRPCID=t;var i=function(u){return u.jsonrpc===o.JSONRPC&&u.method!==void 0&&u.result===void 0&&u.error===void 0};o.isJSONRPCRequest=i;var n=function(u){return Array.isArray(u)&&u.every(o.isJSONRPCRequest)};o.isJSONRPCRequests=n;var s=function(u){return u.jsonrpc===o.JSONRPC&&u.id!==void 0&&(u.result!==void 0||u.error!==void 0)};o.isJSONRPCResponse=s;var r=function(u){return Array.isArray(u)&&u.every(o.isJSONRPCResponse)};o.isJSONRPCResponses=r;var l=function(u,h,a){var c={code:u,message:h};return a!=null&&(c.data=a),c},f=function(u){e(h,u);function h(a,c,p){var w=u.call(this,a)||this;return Object.setPrototypeOf(w,h.prototype),w.code=c,w.data=p,w}return h.prototype.toObject=function(){return l(this.code,this.message,this.data)},h}(Error);o.JSONRPCErrorException=f,function(u){u[u.ParseError=-32700]="ParseError",u[u.InvalidRequest=-32600]="InvalidRequest",u[u.MethodNotFound=-32601]="MethodNotFound",u[u.InvalidParams=-32602]="InvalidParams",u[u.InternalError=-32603]="InternalError"}(o.JSONRPCErrorCode||(o.JSONRPCErrorCode={}));var d=function(u,h,a,c){return{jsonrpc:o.JSONRPC,id:u,error:l(h,a,c)}};o.createJSONRPCErrorResponse=d;var y=function(u,h){return{jsonrpc:o.JSONRPC,id:u,result:h??null}};o.createJSONRPCSuccessResponse=y;var v=function(u,h,a){return{jsonrpc:o.JSONRPC,id:u,method:h,params:a}};o.createJSONRPCRequest=v;var N=function(u,h){return{jsonrpc:o.JSONRPC,method:u,params:h}};o.createJSONRPCNotification=N}(models)),models}var internal={},hasRequiredInternal;function requireInternal(){return hasRequiredInternal||(hasRequiredInternal=1,Object.defineProperty(internal,"__esModule",{value:!0}),internal.DefaultErrorCode=void 0,internal.DefaultErrorCode=0),internal}var hasRequiredClient;function requireClient(){if(hasRequiredClient)return client;hasRequiredClient=1;var o=client&&client.__awaiter||function(r,l,f,d){function y(v){return v instanceof f?v:new f(function(N){N(v)})}return new(f||(f=Promise))(function(v,N){function u(c){try{a(d.next(c))}catch(p){N(p)}}function h(c){try{a(d.throw(c))}catch(p){N(p)}}function a(c){c.done?v(c.value):y(c.value).then(u,h)}a((d=d.apply(r,l||[])).next())})},e=client&&client.__generator||function(r,l){var f={label:0,sent:function(){if(v[0]&1)throw v[1];return v[1]},trys:[],ops:[]},d,y,v,N;return N={next:u(0),throw:u(1),return:u(2)},typeof Symbol=="function"&&(N[Symbol.iterator]=function(){return this}),N;function u(a){return function(c){return h([a,c])}}function h(a){if(d)throw new TypeError("Generator is already executing.");for(;N&&(N=0,a[0]&&(f=0)),f;)try{if(d=1,y&&(v=a[0]&2?y.return:a[0]?y.throw||((v=y.return)&&v.call(y),0):y.next)&&!(v=v.call(y,a[1])).done)return v;switch(y=0,v&&(a=[a[0]&2,v.value]),a[0]){case 0:case 1:v=a;break;case 4:return f.label++,{value:a[1],done:!1};case 5:f.label++,y=a[1],a=[0];continue;case 7:a=f.ops.pop(),f.trys.pop();continue;default:if(v=f.trys,!(v=v.length>0&&v[v.length-1])&&(a[0]===6||a[0]===2)){f=0;continue}if(a[0]===3&&(!v||a[1]>v[0]&&a[1]0&&m[m.length-1])&&(A[0]===6||A[0]===2)){p=0;continue}if(A[0]===3&&(!m||A[1]>m[0]&&A[1]0&&d[d.length-1])&&(u[0]===6||u[0]===2)){r=0;continue}if(u[0]===3&&(!d||u[1]>d[0]&&u[1]{const[t,i,n]=e;this.requestInternal(t,i,n)}),this.messageQueue=[]}request(e,t,i){this.webSocket.readyState===WebSocket.OPEN?this.requestInternal(e,t,i):this.messageQueue.push([e,t,i])}notify(e,t){switch(this.webSocket.readyState){case WebSocket.OPEN:this.serverAndClient.notify(e,t);break}}connect(e){this.closed||(console.log("connect",this.url),this.webSocket=new WebSocket(this.url),this.serverAndClient||(this.serverAndClient=new distExports.JSONRPCServerAndClient(new distExports.JSONRPCServer,new distExports.JSONRPCClient(t=>{try{return this.webSocket.send(JSON.stringify(t)),Promise.resolve()}catch(i){return Promise.reject(i)}}))),this.webSocket.onmessage=t=>{this.serverAndClient.receiveAndSend(JSON.parse(t.data.toString()))},this.webSocket.onopen=()=>{console.log("WebSocket connection established"),this.connectionEstablished=!0,this.onConnected&&this.onConnected(),this.requestMessageQueue()},this.webSocket.onclose=t=>{console.error("WebScoket closed",t),this.serverAndClient.rejectAllPendingRequests(`Connection is closed (${t.reason}).`),this.connectionEstablished&&this.onClosed(),this.timerId=setTimeout(()=>{this.connect()},3e3)},this.webSocket.onerror=t=>{console.error("WebSocket error:",t),this.webSocket.close()})}}const modifierKeys=["Shift","Control","Alt","Meta","CapsLock"],convertKeyTable={Enter:"Return",ArrowRight:"Right",ArrowLeft:"Left",ArrowUp:"Up",ArrowDown:"Down","¡":"1","™":"2","£":"3","¢":"4","∞":"5","§":"6","¶":"7","•":"8",ª:"9",º:"0","–":"-","≠":"=","“":"[","‘":"]","«":"\\","…":";",æ:"'","≤":",","≥":".","÷":"/","⁄":"!","€":"@","‹":"#","›":"$",fi:"%",fl:"^","‡":"&","°":"*","·":"(","‚":")","—":"_","±":"+","”":"{","’":"}","»":"|",Ú:":",Æ:'"',"¯":"<","˘":">","¿":"?",œ:"q","∑":"w","´":"e","®":"r","†":"t","¥":"y","¨":"u","ˆ":"i",ø:"o",π:"p",å:"a",ß:"s","∂":"d",ƒ:"f","©":"g","˙":"h","∆":"j","˚":"k","¬":"l",Ω:"z","≈":"x",ç:"c","√":"v","∫":"b","˜":"n",µ:"m",Œ:"Q","„":"W","´":"E","‰":"R","ˇ":"T",Á:"Y","¨":"U","ˆ":"I",Ø:"O","∏":"P",Å:"A",Í:"S",Î:"D",Ï:"F","˝":"G",Ó:"H",Ô:"J","":"K",Ò:"L","¸":"Z","˛":"X",Ç:"C","◊":"V",ı:"B","˜":"N",Â:"M"};function getKey(o){return o.altKey?convertKeyTable[o.key]||(o.code.startsWith("Key")?o.code[3].toLowerCase():null)||o.key:convertKeyTable[o.key]||o.key}function convertKeyEvent(o){return modifierKeys.indexOf(o.key)!==-1?null:{key:getKey(o),ctrl:o.ctrlKey,meta:o.altKey,super:o.metaKey,shift:o.shiftKey}}var defs=[[0,31,"N"],[32,126,"Na"],[127,160,"N"],[161,161,"A"],[162,163,"Na"],[164,164,"A"],[165,166,"Na"],[167,168,"A"],[169,169,"N"],[170,170,"A"],[171,171,"N"],[172,172,"Na"],[173,174,"A"],[175,175,"Na"],[176,180,"A"],[181,181,"N"],[182,186,"A"],[187,187,"N"],[188,191,"A"],[192,197,"N"],[198,198,"A"],[199,207,"N"],[208,208,"A"],[209,214,"N"],[215,216,"A"],[217,221,"N"],[222,225,"A"],[226,229,"N"],[230,230,"A"],[231,231,"N"],[232,234,"A"],[235,235,"N"],[236,237,"A"],[238,239,"N"],[240,240,"A"],[241,241,"N"],[242,243,"A"],[244,246,"N"],[247,250,"A"],[251,251,"N"],[252,252,"A"],[253,253,"N"],[254,254,"A"],[255,256,"N"],[257,257,"A"],[258,272,"N"],[273,273,"A"],[274,274,"N"],[275,275,"A"],[276,282,"N"],[283,283,"A"],[284,293,"N"],[294,295,"A"],[296,298,"N"],[299,299,"A"],[300,304,"N"],[305,307,"A"],[308,311,"N"],[312,312,"A"],[313,318,"N"],[319,322,"A"],[323,323,"N"],[324,324,"A"],[325,327,"N"],[328,331,"A"],[332,332,"N"],[333,333,"A"],[334,337,"N"],[338,339,"A"],[340,357,"N"],[358,359,"A"],[360,362,"N"],[363,363,"A"],[364,461,"N"],[462,462,"A"],[463,463,"N"],[464,464,"A"],[465,465,"N"],[466,466,"A"],[467,467,"N"],[468,468,"A"],[469,469,"N"],[470,470,"A"],[471,471,"N"],[472,472,"A"],[473,473,"N"],[474,474,"A"],[475,475,"N"],[476,476,"A"],[477,592,"N"],[593,593,"A"],[594,608,"N"],[609,609,"A"],[610,707,"N"],[708,708,"A"],[709,710,"N"],[711,711,"A"],[712,712,"N"],[713,715,"A"],[716,716,"N"],[717,717,"A"],[718,719,"N"],[720,720,"A"],[721,727,"N"],[728,731,"A"],[732,732,"N"],[733,733,"A"],[734,734,"N"],[735,735,"A"],[736,767,"N"],[768,879,"A"],[880,912,"N"],[913,929,"A"],[930,930,"N"],[931,937,"A"],[938,944,"N"],[945,961,"A"],[962,962,"N"],[963,969,"A"],[970,1024,"N"],[1025,1025,"A"],[1026,1039,"N"],[1040,1103,"A"],[1104,1104,"N"],[1105,1105,"A"],[1106,4351,"N"],[4352,4447,"W"],[4448,8207,"N"],[8208,8208,"A"],[8209,8210,"N"],[8211,8214,"A"],[8215,8215,"N"],[8216,8217,"A"],[8218,8219,"N"],[8220,8221,"A"],[8222,8223,"N"],[8224,8226,"A"],[8227,8227,"N"],[8228,8231,"A"],[8232,8239,"N"],[8240,8240,"A"],[8241,8241,"N"],[8242,8243,"A"],[8244,8244,"N"],[8245,8245,"A"],[8246,8250,"N"],[8251,8251,"A"],[8252,8253,"N"],[8254,8254,"A"],[8255,8307,"N"],[8308,8308,"A"],[8309,8318,"N"],[8319,8319,"A"],[8320,8320,"N"],[8321,8324,"A"],[8325,8360,"N"],[8361,8361,"H"],[8362,8363,"N"],[8364,8364,"A"],[8365,8450,"N"],[8451,8451,"A"],[8452,8452,"N"],[8453,8453,"A"],[8454,8456,"N"],[8457,8457,"A"],[8458,8466,"N"],[8467,8467,"A"],[8468,8469,"N"],[8470,8470,"A"],[8471,8480,"N"],[8481,8482,"A"],[8483,8485,"N"],[8486,8486,"A"],[8487,8490,"N"],[8491,8491,"A"],[8492,8530,"N"],[8531,8532,"A"],[8533,8538,"N"],[8539,8542,"A"],[8543,8543,"N"],[8544,8555,"A"],[8556,8559,"N"],[8560,8569,"A"],[8570,8584,"N"],[8585,8585,"A"],[8586,8591,"N"],[8592,8601,"A"],[8602,8631,"N"],[8632,8633,"A"],[8634,8657,"N"],[8658,8658,"A"],[8659,8659,"N"],[8660,8660,"A"],[8661,8678,"N"],[8679,8679,"A"],[8680,8703,"N"],[8704,8704,"A"],[8705,8705,"N"],[8706,8707,"A"],[8708,8710,"N"],[8711,8712,"A"],[8713,8714,"N"],[8715,8715,"A"],[8716,8718,"N"],[8719,8719,"A"],[8720,8720,"N"],[8721,8721,"A"],[8722,8724,"N"],[8725,8725,"A"],[8726,8729,"N"],[8730,8730,"A"],[8731,8732,"N"],[8733,8736,"A"],[8737,8738,"N"],[8739,8739,"A"],[8740,8740,"N"],[8741,8741,"A"],[8742,8742,"N"],[8743,8748,"A"],[8749,8749,"N"],[8750,8750,"A"],[8751,8755,"N"],[8756,8759,"A"],[8760,8763,"N"],[8764,8765,"A"],[8766,8775,"N"],[8776,8776,"A"],[8777,8779,"N"],[8780,8780,"A"],[8781,8785,"N"],[8786,8786,"A"],[8787,8799,"N"],[8800,8801,"A"],[8802,8803,"N"],[8804,8807,"A"],[8808,8809,"N"],[8810,8811,"A"],[8812,8813,"N"],[8814,8815,"A"],[8816,8833,"N"],[8834,8835,"A"],[8836,8837,"N"],[8838,8839,"A"],[8840,8852,"N"],[8853,8853,"A"],[8854,8856,"N"],[8857,8857,"A"],[8858,8868,"N"],[8869,8869,"A"],[8870,8894,"N"],[8895,8895,"A"],[8896,8977,"N"],[8978,8978,"A"],[8979,8985,"N"],[8986,8987,"W"],[8988,9e3,"N"],[9001,9002,"W"],[9003,9192,"N"],[9193,9196,"W"],[9197,9199,"N"],[9200,9200,"W"],[9201,9202,"N"],[9203,9203,"W"],[9204,9311,"N"],[9312,9449,"A"],[9450,9450,"N"],[9451,9547,"A"],[9548,9551,"N"],[9552,9587,"A"],[9588,9599,"N"],[9600,9615,"A"],[9616,9617,"N"],[9618,9621,"A"],[9622,9631,"N"],[9632,9633,"A"],[9634,9634,"N"],[9635,9641,"A"],[9642,9649,"N"],[9650,9651,"A"],[9652,9653,"N"],[9654,9655,"A"],[9656,9659,"N"],[9660,9661,"A"],[9662,9663,"N"],[9664,9665,"A"],[9666,9669,"N"],[9670,9672,"A"],[9673,9674,"N"],[9675,9675,"A"],[9676,9677,"N"],[9678,9681,"A"],[9682,9697,"N"],[9698,9701,"A"],[9702,9710,"N"],[9711,9711,"A"],[9712,9724,"N"],[9725,9726,"W"],[9727,9732,"N"],[9733,9734,"A"],[9735,9736,"N"],[9737,9737,"A"],[9738,9741,"N"],[9742,9743,"A"],[9744,9747,"N"],[9748,9749,"W"],[9750,9755,"N"],[9756,9756,"A"],[9757,9757,"N"],[9758,9758,"A"],[9759,9791,"N"],[9792,9792,"A"],[9793,9793,"N"],[9794,9794,"A"],[9795,9799,"N"],[9800,9811,"W"],[9812,9823,"N"],[9824,9825,"A"],[9826,9826,"N"],[9827,9829,"A"],[9830,9830,"N"],[9831,9834,"A"],[9835,9835,"N"],[9836,9837,"A"],[9838,9838,"N"],[9839,9839,"A"],[9840,9854,"N"],[9855,9855,"W"],[9856,9874,"N"],[9875,9875,"W"],[9876,9885,"N"],[9886,9887,"A"],[9888,9888,"N"],[9889,9889,"W"],[9890,9897,"N"],[9898,9899,"W"],[9900,9916,"N"],[9917,9918,"W"],[9919,9919,"A"],[9920,9923,"N"],[9924,9925,"W"],[9926,9933,"A"],[9934,9934,"W"],[9935,9939,"A"],[9940,9940,"W"],[9941,9953,"A"],[9954,9954,"N"],[9955,9955,"A"],[9956,9959,"N"],[9960,9961,"A"],[9962,9962,"W"],[9963,9969,"A"],[9970,9971,"W"],[9972,9972,"A"],[9973,9973,"W"],[9974,9977,"A"],[9978,9978,"W"],[9979,9980,"A"],[9981,9981,"W"],[9982,9983,"A"],[9984,9988,"N"],[9989,9989,"W"],[9990,9993,"N"],[9994,9995,"W"],[9996,10023,"N"],[10024,10024,"W"],[10025,10044,"N"],[10045,10045,"A"],[10046,10059,"N"],[10060,10060,"W"],[10061,10061,"N"],[10062,10062,"W"],[10063,10066,"N"],[10067,10069,"W"],[10070,10070,"N"],[10071,10071,"W"],[10072,10101,"N"],[10102,10111,"A"],[10112,10132,"N"],[10133,10135,"W"],[10136,10159,"N"],[10160,10160,"W"],[10161,10174,"N"],[10175,10175,"W"],[10176,10213,"N"],[10214,10221,"Na"],[10222,10628,"N"],[10629,10630,"Na"],[10631,11034,"N"],[11035,11036,"W"],[11037,11087,"N"],[11088,11088,"W"],[11089,11092,"N"],[11093,11093,"W"],[11094,11097,"A"],[11098,11903,"N"],[11904,11929,"W"],[11930,11930,"N"],[11931,12019,"W"],[12020,12031,"N"],[12032,12245,"W"],[12246,12271,"N"],[12272,12287,"W"],[12288,12288,"F"],[12289,12350,"W"],[12351,12352,"N"],[12353,12438,"W"],[12439,12440,"N"],[12441,12543,"W"],[12544,12548,"N"],[12549,12591,"W"],[12592,12592,"N"],[12593,12686,"W"],[12687,12687,"N"],[12688,12771,"W"],[12772,12782,"N"],[12783,12830,"W"],[12831,12831,"N"],[12832,12871,"W"],[12872,12879,"A"],[12880,19903,"W"],[19904,19967,"N"],[19968,42124,"W"],[42125,42127,"N"],[42128,42182,"W"],[42183,43359,"N"],[43360,43388,"W"],[43389,44031,"N"],[44032,55203,"W"],[55204,57343,"N"],[57344,63743,"A"],[63744,64255,"W"],[64256,65023,"N"],[65024,65039,"A"],[65040,65049,"W"],[65050,65071,"N"],[65072,65106,"W"],[65107,65107,"N"],[65108,65126,"W"],[65127,65127,"N"],[65128,65131,"W"],[65132,65280,"N"],[65281,65376,"F"],[65377,65470,"H"],[65471,65473,"N"],[65474,65479,"H"],[65480,65481,"N"],[65482,65487,"H"],[65488,65489,"N"],[65490,65495,"H"],[65496,65497,"N"],[65498,65500,"H"],[65501,65503,"N"],[65504,65510,"F"],[65511,65511,"N"],[65512,65518,"H"],[65519,65532,"N"],[65533,65533,"A"],[65534,94175,"N"],[94176,94180,"W"],[94181,94191,"N"],[94192,94193,"W"],[94194,94207,"N"],[94208,100343,"W"],[100344,100351,"N"],[100352,101589,"W"],[101590,101631,"N"],[101632,101640,"W"],[101641,110575,"N"],[110576,110579,"W"],[110580,110580,"N"],[110581,110587,"W"],[110588,110588,"N"],[110589,110590,"W"],[110591,110591,"N"],[110592,110882,"W"],[110883,110897,"N"],[110898,110898,"W"],[110899,110927,"N"],[110928,110930,"W"],[110931,110932,"N"],[110933,110933,"W"],[110934,110947,"N"],[110948,110951,"W"],[110952,110959,"N"],[110960,111355,"W"],[111356,126979,"N"],[126980,126980,"W"],[126981,127182,"N"],[127183,127183,"W"],[127184,127231,"N"],[127232,127242,"A"],[127243,127247,"N"],[127248,127277,"A"],[127278,127279,"N"],[127280,127337,"A"],[127338,127343,"N"],[127344,127373,"A"],[127374,127374,"W"],[127375,127376,"A"],[127377,127386,"W"],[127387,127404,"A"],[127405,127487,"N"],[127488,127490,"W"],[127491,127503,"N"],[127504,127547,"W"],[127548,127551,"N"],[127552,127560,"W"],[127561,127567,"N"],[127568,127569,"W"],[127570,127583,"N"],[127584,127589,"W"],[127590,127743,"N"],[127744,127776,"W"],[127777,127788,"N"],[127789,127797,"W"],[127798,127798,"N"],[127799,127868,"W"],[127869,127869,"N"],[127870,127891,"W"],[127892,127903,"N"],[127904,127946,"W"],[127947,127950,"N"],[127951,127955,"W"],[127956,127967,"N"],[127968,127984,"W"],[127985,127987,"N"],[127988,127988,"W"],[127989,127991,"N"],[127992,128062,"W"],[128063,128063,"N"],[128064,128064,"W"],[128065,128065,"N"],[128066,128252,"W"],[128253,128254,"N"],[128255,128317,"W"],[128318,128330,"N"],[128331,128334,"W"],[128335,128335,"N"],[128336,128359,"W"],[128360,128377,"N"],[128378,128378,"W"],[128379,128404,"N"],[128405,128406,"W"],[128407,128419,"N"],[128420,128420,"W"],[128421,128506,"N"],[128507,128591,"W"],[128592,128639,"N"],[128640,128709,"W"],[128710,128715,"N"],[128716,128716,"W"],[128717,128719,"N"],[128720,128722,"W"],[128723,128724,"N"],[128725,128727,"W"],[128728,128731,"N"],[128732,128735,"W"],[128736,128746,"N"],[128747,128748,"W"],[128749,128755,"N"],[128756,128764,"W"],[128765,128991,"N"],[128992,129003,"W"],[129004,129007,"N"],[129008,129008,"W"],[129009,129291,"N"],[129292,129338,"W"],[129339,129339,"N"],[129340,129349,"W"],[129350,129350,"N"],[129351,129535,"W"],[129536,129647,"N"],[129648,129660,"W"],[129661,129663,"N"],[129664,129672,"W"],[129673,129679,"N"],[129680,129725,"W"],[129726,129726,"N"],[129727,129733,"W"],[129734,129741,"N"],[129742,129755,"W"],[129756,129759,"N"],[129760,129768,"W"],[129769,129775,"N"],[129776,129784,"W"],[129785,131071,"N"],[131072,196605,"W"],[196606,196607,"N"],[196608,262141,"W"],[262142,917759,"N"],[917760,917999,"A"],[918e3,983039,"N"],[983040,1048573,"A"],[1048574,1048575,"N"],[1048576,1114109,"A"],[1114110,1114111,"N"]];function getEAWOfCodePoint(o){let e=0,t=defs.length-1;for(;e!==t;){const i=e+(t-e>>1),[n,s,r]=defs[i];if(os)e=i+1;else return r}return defs[e][2]}function getEAW(o,e=0){const t=o.codePointAt(e);if(t!==void 0)return getEAWOfCodePoint(t)}const textOffsetY=5,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function isWideChar(o){switch(getEAW(o)){case"F":case"W":return!0;case"A":default:return!1}}function isMacOS(){return window.navigator.userAgent.indexOf("Mac OS X")!==-1}function computeFontSize(o){const t=document.createElement("canvas").getContext("2d");t.font=o;const i=t.measureText("W");return[Math.floor(i.width),Math.round(i.fontBoundingBoxAscent+textOffsetY+(i.emHeightDescent||0))]}function drawBlock({ctx:o,x:e,y:t,width:i,height:n,style:s}){o.fillStyle=s,o.fillRect(e,t,i,n)}function drawText({ctx:o,x:e,y:t,text:i,font:n,style:s,option:r}){t+=Math.round(textOffsetY),o.fillStyle=s,o.font=n,o.textBaseline="top";for(const l of i)isWideChar(l)?(o.fillText(l,e,t,r.fontWidth*2),e+=r.fontWidth*2):(o.fillText(l,e,t,r.fontWidth),e+=r.fontWidth)}function drawHorizontalLine({ctx:o,x:e,y:t,width:i,style:n,lineWidth:s=1}){o.strokeStyle=n,o.lineWidth=s,o.setLineDash=[],o.beginPath(),o.moveTo(e,t),o.lineTo(e+i,t),o.stroke()}class Option{constructor({fontName:e,fontSize:t}){this.setFont(e,t),this.foreground="#cccccc",this.background="#2d2d2d"}setFont(e,t){const i=t+"px "+e,[n,s]=computeFontSize(i);this.fontName=e,this.fontSize=t,this.fontWidth=n,this.fontHeight=s,this.font=i}}function getLemEditorElement(){return document.getElementById("lem-editor")}function normalizeWheelDelta(o,e,t,i){switch(t){case 0:return{dx:o/i,dy:e/i};case 2:return{dx:o*20,dy:e*20};default:return{dx:o,dy:e}}}function extractWholeLines(o,e){const t=Math.trunc(o),i=Math.trunc(e);return{scrollX:t,scrollY:i,remainderX:o-t,remainderY:e-i}}function cursorPosition(o,e){const[t,i]=e.getDisplayRectangle(),n=o.clientX-t,s=o.clientY-i;return{pixelX:n,pixelY:s,x:Math.floor(n/e.option.fontWidth),y:Math.floor(s/e.option.fontHeight)}}function makeWheelHandler(o){let e={x:0,y:0},t=!1,i={pixelX:0,pixelY:0,x:0,y:0};return n=>{n.preventDefault(),i=cursorPosition(n,o);const{dx:s,dy:r}=normalizeWheelDelta(n.deltaX,n.deltaY,n.deltaMode,o.option.fontHeight);e={x:e.x+s,y:e.y+r},t||(t=!0,requestAnimationFrame(()=>{t=!1;const{scrollX:l,scrollY:f,remainderX:d,remainderY:y}=extractWholeLines(e.x,e.y);e={x:d,y},(l!==0||f!==0)&&o.jsonrpc.notify("input",{kind:"wheel",value:{...i,wheelX:-l,wheelY:-f}})}))}}function addMouseEventListeners({dom:o,editor:e,isDraggable:t,draggableStyle:i}){o.addEventListener("contextmenu",r=>{r.preventDefault()});const n=(r,l)=>{r.preventDefault();const[f,d]=e.getDisplayRectangle(),y=r.clientX-f,v=r.clientY-d,N=Math.floor(y/e.option.fontWidth),u=Math.floor(v/e.option.fontHeight);e.jsonrpc.notify("input",{kind:l,value:{x:N,y:u,pixelX:y,pixelY:v,button:r.button,clicks:r.detail}})};o.addEventListener("mousedown",r=>{t&&(document.body.style.cursor=i),e.focusHiddenInput(),n(r,"mousedown")}),o.addEventListener("mouseup",r=>{t&&(document.body.style.cursor="default"),n(r,"mouseup")});let s=0;o.addEventListener("mousemove",r=>{r.preventDefault();const l=Date.now();if(l-s>50){s=l;const[f,d]=e.getDisplayRectangle(),y=r.clientX-f,v=r.clientY-d,N=Math.floor(y/e.option.fontWidth),u=Math.floor(v/e.option.fontHeight);e.jsonrpc.notify("input",{kind:"mousemove",value:{x:N,y:u,pixelX:y,pixelY:v,button:r.buttons===0?null:r.buttons-1}})}}),t&&(o.addEventListener("mouseover",()=>{document.body.style.cursor=i}),o.addEventListener("mouseout",r=>{r.buttons!==1&&(document.body.style.cursor="default")})),o.addEventListener("wheel",makeWheelHandler(e))}const zIndexTable={"floating-window":200,modeline:100,"vertical-border":100,"horizontal-border":101};function zindex(o){return zIndexTable[o]||0}const borderOffsetX=5,borderOffsetY=10;class BaseSurface{constructor({editor:o}){this.editor=o,this.mainDOM=null,this.wrapper=null}delete(){this.wrapper?getLemEditorElement().removeChild(this.wrapper):getLemEditorElement().removeChild(this.mainDOM)}setupDOM({dom:o,isFloating:e,border:t,cssClassName:i}){this.mainDOM=o,e&&t?(this.wrapper=document.createElement("div"),i&&(this.wrapper.className=i),this.wrapper.style.position="absolute",this.wrapper.style.backgroundColor=this.editor.option.background,this.wrapper.style.zIndex=zindex("floating-window"),this.wrapper.appendChild(o),getLemEditorElement().appendChild(this.wrapper)):(i&&(o.className=i),getLemEditorElement().appendChild(o))}move(o,e,t,i){const[n,s]=this.editor.getDisplayRectangle(),r=t!=null?Math.floor(n+t):Math.floor(n+o*this.editor.option.fontWidth),l=i!=null?Math.floor(s+i):Math.floor(s+e*this.editor.option.fontHeight);this.wrapper?(this.wrapper.style.left=r-borderOffsetX+"px",this.wrapper.style.top=l-borderOffsetY+"px",this.mainDOM.style.left=borderOffsetX+"px",this.mainDOM.style.top=borderOffsetY+"px"):(this.mainDOM.style.left=r+"px",this.mainDOM.style.top=l+"px")}_resize(o,e,t,i){const n=window.devicePixelRatio||1,s=t??o*this.editor.option.fontWidth,r=i??e*this.editor.option.fontHeight;this.mainDOM.width=s*n,this.mainDOM.height=r*n,this.mainDOM.style.width=s+"px",this.mainDOM.style.height=r+"px",this.wrapper&&(this.wrapper.style.width=s+borderOffsetX*2+"px",this.wrapper.style.height=r+borderOffsetY*2+"px")}drawBlock(o,e,t,i,n){}drawText(o,e,t,i,n){}touch(){}evalIn(code){return eval(code)}}class CanvasSurface extends BaseSurface{constructor({editor:e,view:t,x:i,y:n,width:s,height:r,styles:l,isFloating:f,border:d,cssClassName:y}){super({editor:e});const v=this.setupCanvas(l);this.setupDOM({dom:v,isFloating:f,border:d,cssClassName:y}),this.move(i,n),this.resize(s,r),this.drawingQueue=[],addMouseEventListeners({dom:v,editor:e})}setupCanvas(e){const t=document.createElement("canvas");if(t.style.position="absolute",e)for(let i in e)t.style[i]=e[i];return t}resize(e,t,i,n){this._resize(e,t,i,n);const s=window.devicePixelRatio||1;this.mainDOM.getContext("2d").scale(s,s)}drawBlock(e,t,i,n,s){const r=this.editor.option;this.drawingQueue.push(function(l){drawBlock({ctx:l,x:e*r.fontWidth,y:t*r.fontHeight,width:i*r.fontWidth,height:n*r.fontHeight,style:s})})}drawText(e,t,i,n,s,r){const l=this.editor.option;this.drawingQueue.push(function(f){if(r=r?`${l.fontSize}px ${r}`:l.font,!s)drawBlock({ctx:f,x:e*l.fontWidth,y:t*l.fontHeight,width:n*l.fontWidth,height:l.fontHeight,style:l.background}),drawText({ctx:f,x:e*l.fontWidth,y:t*l.fontHeight,text:i,style:l.foreground,font:r,option:l});else{let{foreground:d,background:y,bold:v,reverse:N,underline:u,cursor:h}=s;if(d||(d=l.foreground),y||(y=l.background),N){const p=y;y=d,d=p}h&&(y=l.background);const a=e*l.fontWidth,c=t*l.fontHeight;drawBlock({ctx:f,x:a,y:c,width:n*l.fontWidth,height:l.fontHeight,style:y}),drawText({ctx:f,x:a,y:c,text:i,style:d,font:v?"bold "+r:r,option:l}),u&&drawHorizontalLine({ctx:f,x:a,y:c+l.fontHeight-2,width:n*l.fontWidth,style:typeof u=="string"?u:d,lineWidth:2})}})}touch(){const e=this.mainDOM.getContext("2d");for(let t of this.drawingQueue)t(e);this.drawingQueue=[]}activate(){this.mainDOM.dataset.store="active"}deactivate(){this.mainDOM.dataset.store="inactive"}}class HTMLSurface extends BaseSurface{constructor({editor:e,x:t,y:i,width:n,height:s,styles:r,option:l,isFloating:f,border:d,html:y}){super({editor:e});const v=document.createElement("iframe");this.setupDOM({dom:v,isFloating:f,border:d}),v.style.position="absolute",v.style.backgroundColor=l.background,v.setAttribute("sandbox","allow-scripts allow-same-origin"),v.srcdoc=y,v.addEventListener("load",()=>{const N=v.contentWindow;N.invokeLem=(u,h)=>parent.postMessage({type:"invoke-lem",method:u,args:h})}),this.iframe=v,this.move(t,i),this.resize(n,s)}resize(e,t,i,n){this._resize(e,t,i,n)}update(e){const t=this.iframe.contentWindow.scrollY;this.iframe.srcdoc=e,this.iframe.onload=()=>{this.iframe.onload=null,this.iframe.contentWindow.scrollTo(0,t)}}evalIn(e){return this.iframe.contentWindow.eval(e)}}class VerticalBorder{constructor({x:e,y:t,height:i,option:n,editor:s}){this.option=n,this.editor=s,this.line=document.createElement("div"),this.line.className="lem-editor__vertical-border",this.line.style.height=i*n.fontHeight+"px",this.line.style.position="absolute",this.line.style.zIndex=zindex("vertical-border"),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:s,isDraggable:!0,draggableStyle:"col-resize"})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.line.style.left=Math.floor(i+e*this.option.fontWidth-this.option.fontWidth/2)+"px",this.line.style.top=n+t*this.option.fontHeight+"px"}resize(e){this.line.style.height=e*this.option.fontHeight+"px"}}class HorizontalBorder{constructor({x:e,y:t,width:i,option:n,editor:s}){this.option=n,this.editor=s,this.line=document.createElement("div"),this.line.className="lem-editor__horizontal-border",this.line.style.width=i*n.fontWidth+"px",this.line.style.position="absolute",this.line.style.zIndex=zindex("horizontal-border"),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:s,isDraggable:!0,draggableStyle:"row-resize"})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.line.style.left=i+e*this.option.fontWidth+"px",this.line.style.top=Math.floor(n+t*this.option.fontHeight-4)+"px"}resize(e){this.line.style.width=e*this.option.fontWidth+"px"}}const viewStyles={header:()=>{},tile:()=>{},floating:o=>({boxSizing:"border-box",borderColor:o.foreground,backgroundColor:o.background})};function getViewStyle(o,e){return viewStyles[o](e)||{}}class View{constructor({id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,useModeline:y,kind:v,type:N,content:u,border:h,borderShape:a,option:c,editor:p}){switch(this.option=c,this.id=e,this.x=t,this.y=i,this.width=n,this.height=s,this.pixelX=r,this.pixelY=l,this.pixelWidth=f,this.pixelHeight=d,this.useModeline=y,this.kind=v,this.type=N,this.border=h,this.borderShape=a,this.editor=p,this.bottomBar=null,this.leftsideBar=null,v){case"tile":this.mainSurface=this.makeSurface(N,u),this.leftSideBar=new VerticalBorder({x:t,y:i,height:s+(y?1:0),option:c,editor:p}),y||(this.bottomBar=new HorizontalBorder({x:t,y:i+s-1,width:n,option:c,editor:p}));break;case"header":this.mainSurface=this.makeSurface(N,u);break;case"floating":this.mainSurface=this.makeSurface(N,u),a==="left-border"&&(this.leftSideBar=new VerticalBorder({x:t,y:i,height:s,option:c,editor:p}));break}this.modelineSurface=y?this.makeModelineSurface():null,v==="floating"&&(r!=null||l!=null)&&this.move(t,i,r,l)}delete(){this.mainSurface.delete(),this.modelineSurface&&this.modelineSurface.delete(),this.leftSideBar&&this.leftSideBar.delete(),this.bottomBar&&this.bottomBar.delete()}move(e,t,i,n){if(this.x=e,this.y=t,this.pixelX=i,this.pixelY=n,this.mainSurface.move(e,t,i,n),this.modelineSurface){const s=n!=null&&this.pixelHeight!=null?n+this.pixelHeight:null;this.modelineSurface.move(e,t+this.height,i,s)}this.leftSideBar&&this.leftSideBar.move(e,t),this.bottomBar&&this.bottomBar.move(e,t+this.height)}resize(e,t,i,n){if(this.width=e,this.height=t,this.pixelWidth=i,this.pixelHeight=n,this.mainSurface.resize(e,t,i,n),this.modelineSurface){const s=this.pixelY!=null&&n!=null?this.pixelY+n:null;this.modelineSurface.move(this.x,this.y+this.height,this.pixelX,s),this.modelineSurface.resize(e,1)}this.leftSideBar&&this.leftSideBar.resize(t+(this.modelineSurface?1:0)),this.bottomBar&&this.bottomBar.resize(e)}clear(){this.mainSurface.drawBlock(0,0,this.width,this.height,this.option.background)}clearEol(e,t){this.mainSurface.drawBlock(e,t,this.width-e,1,this.option.background)}clearEob(e,t){this.mainSurface.drawBlock(e,t,this.width,this.height-t,this.option.background)}print(e,t,i,n,s,r){this.mainSurface.drawText(e,t,i,n,s,r)}printToModeline(e,t,i,n,s){this.modelineSurface&&this.modelineSurface.drawText(e,t,i,n,s)}touch(e){this.mainSurface.touch(),this.modelineSurface&&(this.modelineSurface.touch(),e?this.modelineSurface.activate():this.modelineSurface.deactivate())}makeSurface(e,t){switch(e){case"html":return this.makeHTMLSurface(t);case"editor":return this.makeEditorSurface();default:console.error(`unknown type: ${e}`)}}makeHTMLSurface(e){return new HTMLSurface({editor:this.editor,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),option:this.option,isFloating:this.kind==="floating",border:this.border,html:e})}makeEditorSurface(){const e=this.borderShape==="left-border"?0:this.border,t=this.kind==="floating";return new CanvasSurface({option:this.editor.option,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),editor:this.editor,border:e,isFloating:t,view:this,cssClassName:t&&e?"lem-editor__floating-window--bordered":null})}makeModelineSurface(){const e=new CanvasSurface({option:this.editor.option,x:this.x,y:this.y+this.height,width:this.width,height:1,editor:this.editor,view:this,styles:{zIndex:zindex("modeline")},cssClassName:"lem-editor__mode-line"});return addMouseEventListeners({dom:e.mainDOM,editor:this.editor,isDraggable:!0,draggableStyle:"row-resize"}),e}changeToHTMLContent(e){this.mainSurface.constructor.name==="HTMLSurface"?this.mainSurface.update(e):(this.mainSurface.delete(),this.mainSurface=this.makeHTMLSurface(e))}changeToEditorContent(){this.mainSurface.delete(),this.mainSurface=this.makeEditorSurface()}evalIn(e){return this.mainSurface.evalIn(e)}}function isPasteKeyEvent(o){return isMacOS()?o.metaKey&&o.key==="v":o.ctrlKey&&o.shiftKey&&o.key==="V"}class Input{constructor(e){const t=e.option;this.editor=e,this.composition=!1,this.ignoreKeydownAfterCompositionend=!1,this.span=document.createElement("span"),this.span.style.color=t.foreground,this.span.style.backgroundColor=t.background,this.span.style.position="absolute",this.span.style.zIndex=1e6,this.span.style.top="0",this.span.style.left="0",this.span.style.font=t.font,this.input=document.createElement("input"),this.input.style.backgroundColor="transparent",this.input.style.color="transparent",this.input.style.width="0",this.input.style.padding="0",this.input.style.margin="0",this.input.style.border="none",this.input.style.position="absolute",this.input.style.zIndex="-10",this.input.style.top="0",this.input.style.left="0",this.input.style.font=t.font,this.input.addEventListener("blur",i=>{this.input.focus()}),this.input.addEventListener("input",i=>{this.composition===!1&&(this.input.value="",this.span.innerHTML="",this.input.style.width="0",isMacOS()||this.editor.emitInputString(i.data))}),this.input.addEventListener("paste",async i=>{i.preventDefault();const n=i.clipboardData||window.Clipboard.data,s=n?.getData("text")??n?.getData("text/plain");if(s&&s.length>0){this.editor.emitInputString(s);return}try{if(navigator.clipboard?.readText){const r=await navigator.clipboard.readText();if(r&&r.length>0){this.editor.emitInputString(r);return}}}catch(r){console.warn("clipboard.readText() failed:",r)}alert("Paste failed (permission/environment restriction")}),this.input.addEventListener("keydown",i=>{if(!isPasteKeyEvent(i)&&!(i.isComposing||this.composition)&&i.key!=="Process"){if(this.ignoreKeydownAfterCompositionend&&(isSafari||isMacOS())){i.preventDefault(),this.ignoreKeydownAfterCompositionend=!1;return}if(!(!isMacOS()&&!i.ctrlKey&&!i.altKey&&i.key.length===1)&&(i.preventDefault(),i.isComposing!==!0&&i.code!==""))return setTimeout(()=>{this.composition||(this.editor.emitInput(i),this.input.value="")},0),!1}}),this.input.addEventListener("compositionstart",i=>{this.composition=!0,this.span.innerHTML=this.input.value,this.input.style.width=this.span.offsetWidth+"px"}),this.input.addEventListener("compositionupdate",i=>{this.span.innerHTML=i.data,this.input.style.width=this.span.offsetWidth+"px"}),this.input.addEventListener("compositionend",i=>{this.composition=!1,this.editor.emitInputString(this.input.value),this.input.value="",this.span.innerHTML=this.input.value,this.input.style.width="0",this.ignoreKeydownAfterCompositionend=!0}),document.body.appendChild(this.input),document.body.appendChild(this.span),this.input.focus()}finalize(){document.body.removeChild(this.input),document.body.removeChild(this.span)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.span.style.top=n+t+"px",this.span.style.left=i+e+"px",this.input.style.top=this.span.offsetTop+"px",this.input.style.left=this.span.offsetLeft+"px"}updateForeground(e){this.span.style.color=e}updateBackground(e){this.span.style.backgroundColor=e}}class MessageTable{constructor(){this.map=new Map}register(e,t){for(const i in t){const n=t[i];this.map.set(i,n),e.on(i,n)}}get(e){return this.map.get(e)}}function getDisplayRectangleDefault(){return[0,0,window.innerWidth,window.innerHeight]}class Editor{constructor({getDisplayRectangle:e=getDisplayRectangleDefault,fontName:t,fontSize:i,url:n,onExit:s,onClosed:r}){this.getDisplayRectangle=e,this.option=new Option({fontName:t,fontSize:i}),this.onExit=s,this.input=new Input(this),this.cursors=new Map,this.cursorOverlay=document.createElement("div"),this.cursorOverlay.className="lem-cursor",this.cursorOverlay.style.width=this.option.fontWidth+"px",this.cursorOverlay.style.height=this.option.fontHeight+"px",this.cursorOverlay.style.backgroundColor="#ffffff",this.cursorType="box",this.viewMap=new Map,this.jsonrpc=new JSONRPC(n,{onClosed:()=>{r()}}),this.messageTable=new MessageTable,this.messageTable.register(this.jsonrpc,{"update-foreground":this.updateForeground.bind(this),"update-background":this.updateBackground.bind(this),"make-view":this.makeView.bind(this),"delete-view":this.deleteView.bind(this),"resize-view":this.resize.bind(this),"move-view":this.move.bind(this),"redraw-view-after":this.redrawViewAfter.bind(this),clear:this.clear.bind(this),"clear-eol":this.clearEol.bind(this),"clear-eob":this.clearEob.bind(this),put:this.put.bind(this),"modeline-put":this.modelinePut.bind(this),"update-display":this.updateDisplay.bind(this),"move-cursor":this.moveCursor.bind(this),"change-view":this.changeView.bind(this),"resize-display":this.resizeDisplay.bind(this),bulk:this.bulk.bind(this),exit:this.exitEditor.bind(this),"get-clipboard-text":this.getClipboardText.bind(this),"set-clipboard-text":this.setClipboardText.bind(this),"js-eval":this.jsEval.bind(this),"set-font":this.setFont.bind(this),"get-font":this.getFont.bind(this),"get-display-size":this.getDisplaySize.bind(this),"load-css":this.loadCSS.bind(this),"update-cursor-shape":this.updateCursorShape.bind(this)}),this.login(),this.boundedHandleResize=this.handleResize.bind(this),this.focusHiddenInput=this.focusHiddenInput.bind(this)}init(){window.addEventListener("resize",this.boundedHandleResize),document.getElementsByTagName("html")[0].style["background-color"]="#333",getLemEditorElement().appendChild(this.cursorOverlay)}finalize(){window.removeEventListener("resize",this.boundedHandleResize),this.input.finalize(),this.cursorOverlay.parentNode&&this.cursorOverlay.parentNode.removeChild(this.cursorOverlay)}closeConnection(){this.jsonrpc.close()}emitInput(e){const t=convertKeyEvent(e);if(t){if(t.key==="]"&&t.ctrl&&!t.meta&&!t.super&&!t.shift){this.jsonrpc.notify("input",{kind:"abort"});return}t.key!=="Unidentified"&&this.jsonrpc.notify("input",{kind:"key",value:t})}}emitInputString(e){e?this.jsonrpc.notify("input",{kind:"input-string",value:e}):console.error("unexpected argument",e)}handleResize(e){this.jsonrpc.notify("redraw",{size:this.getDisplaySize()})}focusHiddenInput(){const e=this.input?.input;if(e){try{window.focus()}catch{}requestAnimationFrame(()=>{setTimeout(()=>{e.focus({preventScroll:!0})},0)})}}sendNotification(e,t){this.jsonrpc.notify(e,t)}request(e,t,i){this.jsonrpc.request(e,t,i)}getDisplaySize(){const[e,t,i,n]=this.getDisplayRectangle(),s=Math.floor(i/this.option.fontWidth),r=Math.floor(n/this.option.fontHeight);return{width:s,height:r}}callMessage(e,t){this.messageTable.get(e)(t)}findViewById(e){return this.viewMap.get(e)}login(){this.jsonrpc.request("login",{size:this.getDisplaySize(),foreground:this.option.foreground,background:this.option.background},e=>{if(this.updateForeground(e.foreground),this.updateBackground(e.background),e.views)for(const t of e.views)this.makeView(t);this.jsonrpc.notify("redraw",{size:this.getDisplaySize()})})}updateForeground(e){e!=null&&(this.option.foreground=e,this.input.updateForeground(e))}updateBackground(e){if(e==null)return;this.option.background=e,this.input.updateBackground(e);const t=getLemEditorElement();t.style.backgroundColor=e}makeView({id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,use_modeline:y,kind:v,type:N,content:u,border:h,border_shape:a}){const c=new View({option:this.option,id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,useModeline:y,kind:v,type:N,content:u,border:h,borderShape:a,editor:this});this.viewMap.set(e,c)}deleteView({viewInfo:{id:e}}){this.findViewById(e).delete(),this.viewMap.delete(e)}resize({viewInfo:{id:e},width:t,height:i,pixelWidth:n,pixelHeight:s}){const r=this.findViewById(e);r?r.resize(t,i,n,s):console.warn(`resize: view not found for id ${e}`)}move({viewInfo:{id:e},x:t,y:i,pixelX:n,pixelY:s}){const r=this.findViewById(e);r?r.move(t,i,n,s):console.warn(`move: view not found for id ${e}`)}redrawViewAfter({viewInfo:{id:e},isActive:t}){this.findViewById(e).touch(t)}clear({viewInfo:{id:e}}){this.findViewById(e).clear()}clearEol({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEol(t,i)}clearEob({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEob(t,i)}put({viewInfo:{id:e},x:t,y:i,text:n,textWidth:s,attribute:r,font:l}){this.findViewById(e).print(t,i,n,s,r,l)}modelinePut({viewInfo:{id:e},x:t,y:i,text:n,textWidth:s,attribute:r}){this.findViewById(e).printToModeline(t,i,n,s,r)}updateDisplay(){}moveCursor({viewInfo:{id:e},x:t,y:i,color:n,cursorText:s,cursorForeground:r}){const l=this.findViewById(e),[f,d]=this.getDisplayRectangle(),y=l.x*this.option.fontWidth+t*this.option.fontWidth,v=l.y*this.option.fontHeight+i*this.option.fontHeight;this.input.move(y,v);const N=n||this.option.foreground,u=r||this.option.background,h=this.cursorOverlay;switch(this.cursorType){case"bar":h.style.left=f+y+"px",h.style.top=d+v+"px",h.style.width="2px",h.style.height=this.option.fontHeight+"px",h.style.backgroundColor=N,h.textContent="",h.style.color="",h.style.font="",h.style.paddingTop="";break;case"underline":h.style.left=f+y+"px",h.style.top=d+v+this.option.fontHeight-2+"px",h.style.width=this.option.fontWidth+"px",h.style.height="2px",h.style.backgroundColor=N,h.textContent="",h.style.color="",h.style.font="",h.style.paddingTop="";break;case"box":default:h.style.left=f+y+"px",h.style.top=d+v+"px",h.style.width=this.option.fontWidth+"px",h.style.height=this.option.fontHeight+"px",h.style.backgroundColor=N,h.style.font=this.option.font,h.style.paddingTop=textOffsetY+"px",h.textContent=s||"",h.style.color=u;break}h.style.animation="none",h.offsetHeight,h.style.animation=""}updateCursorShape({cursorType:e}){this.cursorType=e||"box"}changeView({viewInfo:{id:e},type:t,content:i}){const n=this.findViewById(e);switch(t){case"html":n.changeToHTMLContent(i);break;case"editor":n.changeToEditorContent();break}}resizeDisplay({width:e,height:t}){const i=getLemEditorElement();i.style.width=Math.floor(e*this.option.fontWidth)+"px",i.style.height=Math.floor(t*this.option.fontHeight)+"px"}bulk(e){for(const{method:t,argument:i}of e)this.callMessage(t,i)}exitEditor(){this.onExit&&this.onExit()}getClipboardText(){navigator.clipboard?.readText().then(e=>{this.jsonrpc.notify("got-clipboard-text",{text:e})})}setClipboardText({text:e}){navigator.clipboard&&navigator.clipboard.writeText(e)}jsEval({viewInfo:{id:e},code:t}){const n=this.findViewById(e).evalIn(t);return n&&n.toString()}setFont({fontName:e,fontSize:t}){this.option.setFont(e||this.option.fontName,t||this.option.fontSize)}getFont(){return{name:this.option.fontName,size:this.option.fontSize}}loadCSS({content:e}){const t=document.createElement("style");t.textContent=e,document.head.appendChild(t)}notifyToServer(e,t){this.jsonrpc.notify("invoke",{method:e,args:t})}}const canvas=document.querySelector("#editor");async function main(){await Promise.all([document.fonts.load("19px file-icons"),document.fonts.load("19px AllTheIcons"),document.fonts.load("19px fontawesome"),document.fonts.load("19px material-design-icons"),document.fonts.load("19px octicons")]),await document.fonts.ready;const o=window.location.protocol==="https:"?"wss":"ws",e=new Editor({canvas,fontName:"Monospace",fontSize:18,url:`${o}://${window.location.hostname}:${window.location.port}`,onExit:null,onClosed:null});window.addEventListener("message",t=>{t.data.type==="invoke-lem"&&e.notifyToServer(t.data.method,t.data.args)}),e.init()}main(); +var __defProp=Object.defineProperty,__commonJSMin=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),__exportAll=(e,t)=>{let n={};for(var r in e)__defProp(n,r,{get:e[r],enumerable:!0});return t||__defProp(n,Symbol.toStringTag,{value:`Module`}),n};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var require_models=__commonJSMin((e=>{var t=e&&e.__extends||(function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if(typeof n!=`function`&&n!==null)throw TypeError(`Class extends value `+String(n)+` is not a constructor or null`);e(t,n);function r(){this.constructor=t}t.prototype=n===null?Object.create(n):(r.prototype=n.prototype,new r)}})();Object.defineProperty(e,"__esModule",{value:!0}),e.createJSONRPCNotification=e.createJSONRPCRequest=e.createJSONRPCSuccessResponse=e.createJSONRPCErrorResponse=e.JSONRPCErrorCode=e.JSONRPCErrorException=e.isJSONRPCResponses=e.isJSONRPCResponse=e.isJSONRPCRequests=e.isJSONRPCRequest=e.isJSONRPCID=e.JSONRPC=void 0,e.JSONRPC=`2.0`,e.isJSONRPCID=function(e){return typeof e==`string`||typeof e==`number`||e===null},e.isJSONRPCRequest=function(t){return t.jsonrpc===e.JSONRPC&&t.method!==void 0&&t.result===void 0&&t.error===void 0},e.isJSONRPCRequests=function(t){return Array.isArray(t)&&t.every(e.isJSONRPCRequest)},e.isJSONRPCResponse=function(t){return t.jsonrpc===e.JSONRPC&&t.id!==void 0&&(t.result!==void 0||t.error!==void 0)},e.isJSONRPCResponses=function(t){return Array.isArray(t)&&t.every(e.isJSONRPCResponse)};var n=function(e,t,n){var r={code:e,message:t};return n!=null&&(r.data=n),r};e.JSONRPCErrorException=function(e){t(r,e);function r(t,n,i){var a=e.call(this,t)||this;return Object.setPrototypeOf(a,r.prototype),a.code=n,a.data=i,a}return r.prototype.toObject=function(){return n(this.code,this.message,this.data)},r}(Error),(function(e){e[e.ParseError=-32700]=`ParseError`,e[e.InvalidRequest=-32600]=`InvalidRequest`,e[e.MethodNotFound=-32601]=`MethodNotFound`,e[e.InvalidParams=-32602]=`InvalidParams`,e[e.InternalError=-32603]=`InternalError`})(e.JSONRPCErrorCode||={}),e.createJSONRPCErrorResponse=function(t,r,i,a){return{jsonrpc:e.JSONRPC,id:t,error:n(r,i,a)}},e.createJSONRPCSuccessResponse=function(t,n){return{jsonrpc:e.JSONRPC,id:t,result:n??null}},e.createJSONRPCRequest=function(t,n,r){return{jsonrpc:e.JSONRPC,id:t,method:n,params:r}},e.createJSONRPCNotification=function(t,n){return{jsonrpc:e.JSONRPC,method:t,params:n}}})),require_internal=__commonJSMin((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.DefaultErrorCode=void 0,e.DefaultErrorCode=0})),require_client=__commonJSMin((e=>{var t=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})},n=e&&e.__generator||function(e,t){var n={label:0,sent:function(){if(a[0]&1)throw a[1];return a[1]},trys:[],ops:[]},r,i,a,o;return o={next:s(0),throw:s(1),return:s(2)},typeof Symbol==`function`&&(o[Symbol.iterator]=function(){return this}),o;function s(e){return function(t){return c([e,t])}}function c(s){if(r)throw TypeError(`Generator is already executing.`);for(;o&&(o=0,s[0]&&(n=0)),n;)try{if(r=1,i&&(a=s[0]&2?i.return:s[0]?i.throw||((a=i.return)&&a.call(i),0):i.next)&&!(a=a.call(i,s[1])).done)return a;switch(i=0,a&&(s=[s[0]&2,a.value]),s[0]){case 0:case 1:a=s;break;case 4:return n.label++,{value:s[1],done:!1};case 5:n.label++,i=s[1],s=[0];continue;case 7:s=n.ops.pop(),n.trys.pop();continue;default:if((a=n.trys,!(a=a.length>0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{Object.defineProperty(e,"__esModule",{value:!0})})),require_server=__commonJSMin((e=>{var t=e&&e.__assign||function(){return t=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{var t=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})},n=e&&e.__generator||function(e,t){var n={label:0,sent:function(){if(a[0]&1)throw a[1];return a[1]},trys:[],ops:[]},r,i,a,o;return o={next:s(0),throw:s(1),return:s(2)},typeof Symbol==`function`&&(o[Symbol.iterator]=function(){return this}),o;function s(e){return function(t){return c([e,t])}}function c(s){if(r)throw TypeError(`Generator is already executing.`);for(;o&&(o=0,s[0]&&(n=0)),n;)try{if(r=1,i&&(a=s[0]&2?i.return:s[0]?i.throw||((a=i.return)&&a.call(i),0):i.next)&&!(a=a.call(i,s[1])).done)return a;switch(i=0,a&&(s=[s[0]&2,a.value]),s[0]){case 0:case 1:a=s;break;case 4:return n.label++,{value:s[1],done:!1};case 5:n.label++,i=s[1],s=[0];continue;case 7:s=n.ops.pop(),n.trys.pop();continue;default:if((a=n.trys,!(a=a.length>0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{var t=e&&e.__createBinding||(Object.create?(function(e,t,n,r){r===void 0&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);(!i||(`get`in i?!t.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}):(function(e,t,n,r){r===void 0&&(r=n),e[r]=t[n]})),n=e&&e.__exportStar||function(e,n){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(n,r)&&t(n,e,r)};Object.defineProperty(e,"__esModule",{value:!0}),n(require_client(),e),n(require_interfaces(),e),n(require_models(),e),n(require_server(),e),n(require_server_and_client(),e)})),import_dist=require_dist(),JSONRPC=class{constructor(e,{onConnected:t,onClosed:n}){this.url=e,this.onConnected=t,this.onClosed=n,this.messageQueue=[],this.serverAndClient=null,this.connect(),this.connectionEstablished=!1,this.timerId=null,this.closed=!1}close(){this.timerId&&clearTimeout(this.timerId),this.webSocket.close(),this.closed=!0}on(e,t){this.serverAndClient.addMethod(e,t)}async requestInternal(e,t,n){let r=await this.serverAndClient.request(e,t);n&&n(r)}requestMessageQueue(){this.messageQueue.forEach(e=>{let[t,n,r]=e;this.requestInternal(t,n,r)}),this.messageQueue=[]}request(e,t,n){this.webSocket.readyState===WebSocket.OPEN?this.requestInternal(e,t,n):this.messageQueue.push([e,t,n])}notify(e,t){switch(this.webSocket.readyState){case WebSocket.OPEN:this.serverAndClient.notify(e,t);break;case WebSocket.CLOSED:break}}connect(e){this.closed||(console.log(`connect`,this.url),this.webSocket=new WebSocket(this.url),this.serverAndClient||=new import_dist.JSONRPCServerAndClient(new import_dist.JSONRPCServer,new import_dist.JSONRPCClient(e=>{try{return this.webSocket.send(JSON.stringify(e)),Promise.resolve()}catch(e){return Promise.reject(e)}})),this.webSocket.onmessage=e=>{this.serverAndClient.receiveAndSend(JSON.parse(e.data.toString()))},this.webSocket.onopen=()=>{console.log(`WebSocket connection established`),this.connectionEstablished=!0,this.onConnected&&this.onConnected(),this.requestMessageQueue()},this.webSocket.onclose=e=>{console.error(`WebScoket closed`,e),this.serverAndClient.rejectAllPendingRequests(`Connection is closed (${e.reason}).`),this.connectionEstablished&&this.onClosed(),this.timerId=setTimeout(()=>{this.connect()},3e3)},this.webSocket.onerror=e=>{console.error(`WebSocket error:`,e),this.webSocket.close()})}},keyevent_exports=__exportAll({convertKeyEvent:()=>convertKeyEvent}),modifierKeys=[`Shift`,`Control`,`Alt`,`Meta`,`CapsLock`],convertKeyTable={Enter:`Return`,ArrowRight:`Right`,ArrowLeft:`Left`,ArrowUp:`Up`,ArrowDown:`Down`,"¡":`1`,"™":`2`,"£":`3`,"¢":`4`,"∞":`5`,"§":`6`,"¶":`7`,"•":`8`,ª:`9`,º:`0`,"–":`-`,"≠":`=`,"“":`[`,"‘":`]`,"«":`\\`,"…":`;`,æ:`'`,"≤":`,`,"≥":`.`,"÷":`/`,"⁄":`!`,"€":`@`,"‹":`#`,"›":`$`,fi:`%`,fl:`^`,"‡":`&`,"°":`*`,"·":`(`,"‚":`)`,"—":`_`,"±":`+`,"”":`{`,"’":`}`,"»":`|`,Ú:`:`,Æ:`"`,"¯":`<`,"˘":`>`,"¿":`?`,œ:`q`,"∑":`w`,"´":`e`,"®":`r`,"†":`t`,"¥":`y`,"¨":`u`,ˆ:`i`,ø:`o`,π:`p`,å:`a`,ß:`s`,"∂":`d`,ƒ:`f`,"©":`g`,"˙":`h`,"∆":`j`,"˚":`k`,"¬":`l`,Ω:`z`,"≈":`x`,ç:`c`,"√":`v`,"∫":`b`,"˜":`n`,µ:`m`,Œ:`Q`,"„":`W`,"´":`E`,"‰":`R`,ˇ:`T`,Á:`Y`,"¨":`U`,ˆ:`I`,Ø:`O`,"∏":`P`,Å:`A`,Í:`S`,Î:`D`,Ï:`F`,"˝":`G`,Ó:`H`,Ô:`J`,"":`K`,Ò:`L`,"¸":`Z`,"˛":`X`,Ç:`C`,"◊":`V`,ı:`B`,"˜":`N`,Â:`M`};function getKey(e){return e.altKey?convertKeyTable[e.key]||(e.code.startsWith(`Key`)?e.code[3].toLowerCase():null)||e.key:convertKeyTable[e.key]||e.key}function convertKeyEvent(e){return modifierKeys.indexOf(e.key)===-1?{key:getKey(e),ctrl:e.ctrlKey,meta:e.altKey,super:e.metaKey,shift:e.shiftKey}:null}var lib_exports=__exportAll({computeWidth:()=>computeWidth,eawVersion:()=>version,getEAW:()=>getEAW}),defs=[[0,31,`N`],[32,126,`Na`],[127,160,`N`],[161,161,`A`],[162,163,`Na`],[164,164,`A`],[165,166,`Na`],[167,168,`A`],[169,169,`N`],[170,170,`A`],[171,171,`N`],[172,172,`Na`],[173,174,`A`],[175,175,`Na`],[176,180,`A`],[181,181,`N`],[182,186,`A`],[187,187,`N`],[188,191,`A`],[192,197,`N`],[198,198,`A`],[199,207,`N`],[208,208,`A`],[209,214,`N`],[215,216,`A`],[217,221,`N`],[222,225,`A`],[226,229,`N`],[230,230,`A`],[231,231,`N`],[232,234,`A`],[235,235,`N`],[236,237,`A`],[238,239,`N`],[240,240,`A`],[241,241,`N`],[242,243,`A`],[244,246,`N`],[247,250,`A`],[251,251,`N`],[252,252,`A`],[253,253,`N`],[254,254,`A`],[255,256,`N`],[257,257,`A`],[258,272,`N`],[273,273,`A`],[274,274,`N`],[275,275,`A`],[276,282,`N`],[283,283,`A`],[284,293,`N`],[294,295,`A`],[296,298,`N`],[299,299,`A`],[300,304,`N`],[305,307,`A`],[308,311,`N`],[312,312,`A`],[313,318,`N`],[319,322,`A`],[323,323,`N`],[324,324,`A`],[325,327,`N`],[328,331,`A`],[332,332,`N`],[333,333,`A`],[334,337,`N`],[338,339,`A`],[340,357,`N`],[358,359,`A`],[360,362,`N`],[363,363,`A`],[364,461,`N`],[462,462,`A`],[463,463,`N`],[464,464,`A`],[465,465,`N`],[466,466,`A`],[467,467,`N`],[468,468,`A`],[469,469,`N`],[470,470,`A`],[471,471,`N`],[472,472,`A`],[473,473,`N`],[474,474,`A`],[475,475,`N`],[476,476,`A`],[477,592,`N`],[593,593,`A`],[594,608,`N`],[609,609,`A`],[610,707,`N`],[708,708,`A`],[709,710,`N`],[711,711,`A`],[712,712,`N`],[713,715,`A`],[716,716,`N`],[717,717,`A`],[718,719,`N`],[720,720,`A`],[721,727,`N`],[728,731,`A`],[732,732,`N`],[733,733,`A`],[734,734,`N`],[735,735,`A`],[736,767,`N`],[768,879,`A`],[880,912,`N`],[913,929,`A`],[930,930,`N`],[931,937,`A`],[938,944,`N`],[945,961,`A`],[962,962,`N`],[963,969,`A`],[970,1024,`N`],[1025,1025,`A`],[1026,1039,`N`],[1040,1103,`A`],[1104,1104,`N`],[1105,1105,`A`],[1106,4351,`N`],[4352,4447,`W`],[4448,8207,`N`],[8208,8208,`A`],[8209,8210,`N`],[8211,8214,`A`],[8215,8215,`N`],[8216,8217,`A`],[8218,8219,`N`],[8220,8221,`A`],[8222,8223,`N`],[8224,8226,`A`],[8227,8227,`N`],[8228,8231,`A`],[8232,8239,`N`],[8240,8240,`A`],[8241,8241,`N`],[8242,8243,`A`],[8244,8244,`N`],[8245,8245,`A`],[8246,8250,`N`],[8251,8251,`A`],[8252,8253,`N`],[8254,8254,`A`],[8255,8307,`N`],[8308,8308,`A`],[8309,8318,`N`],[8319,8319,`A`],[8320,8320,`N`],[8321,8324,`A`],[8325,8360,`N`],[8361,8361,`H`],[8362,8363,`N`],[8364,8364,`A`],[8365,8450,`N`],[8451,8451,`A`],[8452,8452,`N`],[8453,8453,`A`],[8454,8456,`N`],[8457,8457,`A`],[8458,8466,`N`],[8467,8467,`A`],[8468,8469,`N`],[8470,8470,`A`],[8471,8480,`N`],[8481,8482,`A`],[8483,8485,`N`],[8486,8486,`A`],[8487,8490,`N`],[8491,8491,`A`],[8492,8530,`N`],[8531,8532,`A`],[8533,8538,`N`],[8539,8542,`A`],[8543,8543,`N`],[8544,8555,`A`],[8556,8559,`N`],[8560,8569,`A`],[8570,8584,`N`],[8585,8585,`A`],[8586,8591,`N`],[8592,8601,`A`],[8602,8631,`N`],[8632,8633,`A`],[8634,8657,`N`],[8658,8658,`A`],[8659,8659,`N`],[8660,8660,`A`],[8661,8678,`N`],[8679,8679,`A`],[8680,8703,`N`],[8704,8704,`A`],[8705,8705,`N`],[8706,8707,`A`],[8708,8710,`N`],[8711,8712,`A`],[8713,8714,`N`],[8715,8715,`A`],[8716,8718,`N`],[8719,8719,`A`],[8720,8720,`N`],[8721,8721,`A`],[8722,8724,`N`],[8725,8725,`A`],[8726,8729,`N`],[8730,8730,`A`],[8731,8732,`N`],[8733,8736,`A`],[8737,8738,`N`],[8739,8739,`A`],[8740,8740,`N`],[8741,8741,`A`],[8742,8742,`N`],[8743,8748,`A`],[8749,8749,`N`],[8750,8750,`A`],[8751,8755,`N`],[8756,8759,`A`],[8760,8763,`N`],[8764,8765,`A`],[8766,8775,`N`],[8776,8776,`A`],[8777,8779,`N`],[8780,8780,`A`],[8781,8785,`N`],[8786,8786,`A`],[8787,8799,`N`],[8800,8801,`A`],[8802,8803,`N`],[8804,8807,`A`],[8808,8809,`N`],[8810,8811,`A`],[8812,8813,`N`],[8814,8815,`A`],[8816,8833,`N`],[8834,8835,`A`],[8836,8837,`N`],[8838,8839,`A`],[8840,8852,`N`],[8853,8853,`A`],[8854,8856,`N`],[8857,8857,`A`],[8858,8868,`N`],[8869,8869,`A`],[8870,8894,`N`],[8895,8895,`A`],[8896,8977,`N`],[8978,8978,`A`],[8979,8985,`N`],[8986,8987,`W`],[8988,9e3,`N`],[9001,9002,`W`],[9003,9192,`N`],[9193,9196,`W`],[9197,9199,`N`],[9200,9200,`W`],[9201,9202,`N`],[9203,9203,`W`],[9204,9311,`N`],[9312,9449,`A`],[9450,9450,`N`],[9451,9547,`A`],[9548,9551,`N`],[9552,9587,`A`],[9588,9599,`N`],[9600,9615,`A`],[9616,9617,`N`],[9618,9621,`A`],[9622,9631,`N`],[9632,9633,`A`],[9634,9634,`N`],[9635,9641,`A`],[9642,9649,`N`],[9650,9651,`A`],[9652,9653,`N`],[9654,9655,`A`],[9656,9659,`N`],[9660,9661,`A`],[9662,9663,`N`],[9664,9665,`A`],[9666,9669,`N`],[9670,9672,`A`],[9673,9674,`N`],[9675,9675,`A`],[9676,9677,`N`],[9678,9681,`A`],[9682,9697,`N`],[9698,9701,`A`],[9702,9710,`N`],[9711,9711,`A`],[9712,9724,`N`],[9725,9726,`W`],[9727,9732,`N`],[9733,9734,`A`],[9735,9736,`N`],[9737,9737,`A`],[9738,9741,`N`],[9742,9743,`A`],[9744,9747,`N`],[9748,9749,`W`],[9750,9755,`N`],[9756,9756,`A`],[9757,9757,`N`],[9758,9758,`A`],[9759,9791,`N`],[9792,9792,`A`],[9793,9793,`N`],[9794,9794,`A`],[9795,9799,`N`],[9800,9811,`W`],[9812,9823,`N`],[9824,9825,`A`],[9826,9826,`N`],[9827,9829,`A`],[9830,9830,`N`],[9831,9834,`A`],[9835,9835,`N`],[9836,9837,`A`],[9838,9838,`N`],[9839,9839,`A`],[9840,9854,`N`],[9855,9855,`W`],[9856,9874,`N`],[9875,9875,`W`],[9876,9885,`N`],[9886,9887,`A`],[9888,9888,`N`],[9889,9889,`W`],[9890,9897,`N`],[9898,9899,`W`],[9900,9916,`N`],[9917,9918,`W`],[9919,9919,`A`],[9920,9923,`N`],[9924,9925,`W`],[9926,9933,`A`],[9934,9934,`W`],[9935,9939,`A`],[9940,9940,`W`],[9941,9953,`A`],[9954,9954,`N`],[9955,9955,`A`],[9956,9959,`N`],[9960,9961,`A`],[9962,9962,`W`],[9963,9969,`A`],[9970,9971,`W`],[9972,9972,`A`],[9973,9973,`W`],[9974,9977,`A`],[9978,9978,`W`],[9979,9980,`A`],[9981,9981,`W`],[9982,9983,`A`],[9984,9988,`N`],[9989,9989,`W`],[9990,9993,`N`],[9994,9995,`W`],[9996,10023,`N`],[10024,10024,`W`],[10025,10044,`N`],[10045,10045,`A`],[10046,10059,`N`],[10060,10060,`W`],[10061,10061,`N`],[10062,10062,`W`],[10063,10066,`N`],[10067,10069,`W`],[10070,10070,`N`],[10071,10071,`W`],[10072,10101,`N`],[10102,10111,`A`],[10112,10132,`N`],[10133,10135,`W`],[10136,10159,`N`],[10160,10160,`W`],[10161,10174,`N`],[10175,10175,`W`],[10176,10213,`N`],[10214,10221,`Na`],[10222,10628,`N`],[10629,10630,`Na`],[10631,11034,`N`],[11035,11036,`W`],[11037,11087,`N`],[11088,11088,`W`],[11089,11092,`N`],[11093,11093,`W`],[11094,11097,`A`],[11098,11903,`N`],[11904,11929,`W`],[11930,11930,`N`],[11931,12019,`W`],[12020,12031,`N`],[12032,12245,`W`],[12246,12271,`N`],[12272,12287,`W`],[12288,12288,`F`],[12289,12350,`W`],[12351,12352,`N`],[12353,12438,`W`],[12439,12440,`N`],[12441,12543,`W`],[12544,12548,`N`],[12549,12591,`W`],[12592,12592,`N`],[12593,12686,`W`],[12687,12687,`N`],[12688,12771,`W`],[12772,12782,`N`],[12783,12830,`W`],[12831,12831,`N`],[12832,12871,`W`],[12872,12879,`A`],[12880,19903,`W`],[19904,19967,`N`],[19968,42124,`W`],[42125,42127,`N`],[42128,42182,`W`],[42183,43359,`N`],[43360,43388,`W`],[43389,44031,`N`],[44032,55203,`W`],[55204,57343,`N`],[57344,63743,`A`],[63744,64255,`W`],[64256,65023,`N`],[65024,65039,`A`],[65040,65049,`W`],[65050,65071,`N`],[65072,65106,`W`],[65107,65107,`N`],[65108,65126,`W`],[65127,65127,`N`],[65128,65131,`W`],[65132,65280,`N`],[65281,65376,`F`],[65377,65470,`H`],[65471,65473,`N`],[65474,65479,`H`],[65480,65481,`N`],[65482,65487,`H`],[65488,65489,`N`],[65490,65495,`H`],[65496,65497,`N`],[65498,65500,`H`],[65501,65503,`N`],[65504,65510,`F`],[65511,65511,`N`],[65512,65518,`H`],[65519,65532,`N`],[65533,65533,`A`],[65534,94175,`N`],[94176,94180,`W`],[94181,94191,`N`],[94192,94193,`W`],[94194,94207,`N`],[94208,100343,`W`],[100344,100351,`N`],[100352,101589,`W`],[101590,101631,`N`],[101632,101640,`W`],[101641,110575,`N`],[110576,110579,`W`],[110580,110580,`N`],[110581,110587,`W`],[110588,110588,`N`],[110589,110590,`W`],[110591,110591,`N`],[110592,110882,`W`],[110883,110897,`N`],[110898,110898,`W`],[110899,110927,`N`],[110928,110930,`W`],[110931,110932,`N`],[110933,110933,`W`],[110934,110947,`N`],[110948,110951,`W`],[110952,110959,`N`],[110960,111355,`W`],[111356,126979,`N`],[126980,126980,`W`],[126981,127182,`N`],[127183,127183,`W`],[127184,127231,`N`],[127232,127242,`A`],[127243,127247,`N`],[127248,127277,`A`],[127278,127279,`N`],[127280,127337,`A`],[127338,127343,`N`],[127344,127373,`A`],[127374,127374,`W`],[127375,127376,`A`],[127377,127386,`W`],[127387,127404,`A`],[127405,127487,`N`],[127488,127490,`W`],[127491,127503,`N`],[127504,127547,`W`],[127548,127551,`N`],[127552,127560,`W`],[127561,127567,`N`],[127568,127569,`W`],[127570,127583,`N`],[127584,127589,`W`],[127590,127743,`N`],[127744,127776,`W`],[127777,127788,`N`],[127789,127797,`W`],[127798,127798,`N`],[127799,127868,`W`],[127869,127869,`N`],[127870,127891,`W`],[127892,127903,`N`],[127904,127946,`W`],[127947,127950,`N`],[127951,127955,`W`],[127956,127967,`N`],[127968,127984,`W`],[127985,127987,`N`],[127988,127988,`W`],[127989,127991,`N`],[127992,128062,`W`],[128063,128063,`N`],[128064,128064,`W`],[128065,128065,`N`],[128066,128252,`W`],[128253,128254,`N`],[128255,128317,`W`],[128318,128330,`N`],[128331,128334,`W`],[128335,128335,`N`],[128336,128359,`W`],[128360,128377,`N`],[128378,128378,`W`],[128379,128404,`N`],[128405,128406,`W`],[128407,128419,`N`],[128420,128420,`W`],[128421,128506,`N`],[128507,128591,`W`],[128592,128639,`N`],[128640,128709,`W`],[128710,128715,`N`],[128716,128716,`W`],[128717,128719,`N`],[128720,128722,`W`],[128723,128724,`N`],[128725,128727,`W`],[128728,128731,`N`],[128732,128735,`W`],[128736,128746,`N`],[128747,128748,`W`],[128749,128755,`N`],[128756,128764,`W`],[128765,128991,`N`],[128992,129003,`W`],[129004,129007,`N`],[129008,129008,`W`],[129009,129291,`N`],[129292,129338,`W`],[129339,129339,`N`],[129340,129349,`W`],[129350,129350,`N`],[129351,129535,`W`],[129536,129647,`N`],[129648,129660,`W`],[129661,129663,`N`],[129664,129672,`W`],[129673,129679,`N`],[129680,129725,`W`],[129726,129726,`N`],[129727,129733,`W`],[129734,129741,`N`],[129742,129755,`W`],[129756,129759,`N`],[129760,129768,`W`],[129769,129775,`N`],[129776,129784,`W`],[129785,131071,`N`],[131072,196605,`W`],[196606,196607,`N`],[196608,262141,`W`],[262142,917759,`N`],[917760,917999,`A`],[918e3,983039,`N`],[983040,1048573,`A`],[1048574,1048575,`N`],[1048576,1114109,`A`],[1114110,1114111,`N`]],version=`15.1.0`;function getEAWOfCodePoint(e){let t=0,n=defs.length-1;for(;t!==n;){let r=t+(n-t>>1),[i,a,o]=defs[r];if(ea)t=r+1;else return o}return defs[t][2]}function getEAW(e,t=0){let n=e.codePointAt(t);if(n!==void 0)return getEAWOfCodePoint(n)}var defaultWidths={N:1,Na:1,W:2,F:2,H:1,A:1};function computeWidth(e,t){let n=0;for(let r of e){let e=getEAW(r);n+=t&&t[e]||defaultWidths[e]}return n}var textOffsetY=5,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function isWideChar(e){switch(getEAW(e)){case`F`:case`W`:return!0;default:return!1}}function isMacOS(){return window.navigator.userAgent.indexOf(`Mac OS X`)!==-1}function computeFontSize(e){let t=document.createElement(`canvas`).getContext(`2d`);t.font=e;let n=t.measureText(`W`);return[Math.floor(n.width),Math.round(n.fontBoundingBoxAscent+textOffsetY+(n.emHeightDescent||0)),Math.round(n.fontBoundingBoxAscent+textOffsetY)]}function drawBlock({ctx:e,x:t,y:n,width:r,height:i,style:a}){e.fillStyle=a,e.fillRect(t,n,r,i)}function drawText({ctx:e,x:t,y:n,text:r,font:i,style:a,option:o}){n+=Math.round(textOffsetY),e.fillStyle=a,e.font=i,e.textBaseline=`top`;for(let i of r)isWideChar(i)?(e.fillText(i,t,n,o.fontWidth*2),t+=o.fontWidth*2):(e.fillText(i,t,n,o.fontWidth),t+=o.fontWidth)}function drawHorizontalLine({ctx:e,x:t,y:n,width:r,style:i,lineWidth:a=1}){e.strokeStyle=i,e.lineWidth=a,e.setLineDash=[],e.beginPath(),e.moveTo(t,n),e.lineTo(t+r,n),e.stroke()}var Option=class{constructor({fontName:e,fontSize:t}){this.setFont(e,t),this.foreground=`#cccccc`,this.background=`#2d2d2d`}setFont(e,t){let n=t+`px `+e,[r,i,a]=computeFontSize(n);this.fontName=e,this.fontSize=t,this.fontWidth=r,this.fontHeight=i,this.fontAscent=a,this.font=n}};function getLemEditorElement(){return document.getElementById(`lem-editor`)}function normalizeWheelDelta(e,t,n,r){switch(n){case 0:return{dx:e/r,dy:t/r};case 2:return{dx:e*20,dy:t*20};default:return{dx:e,dy:t}}}function extractWholeLines(e,t){let n=Math.trunc(e),r=Math.trunc(t);return{scrollX:n,scrollY:r,remainderX:e-n,remainderY:t-r}}function cursorPosition(e,t){let[n,r]=t.getDisplayRectangle(),i=e.clientX-n,a=e.clientY-r;return{pixelX:i,pixelY:a,x:Math.floor(i/t.option.fontWidth),y:Math.floor(a/t.option.fontHeight)}}function makeWheelHandler(e){let t={x:0,y:0},n=!1,r={pixelX:0,pixelY:0,x:0,y:0};return i=>{i.preventDefault(),r=cursorPosition(i,e);let{dx:a,dy:o}=normalizeWheelDelta(i.deltaX,i.deltaY,i.deltaMode,e.option.fontHeight);t={x:t.x+a,y:t.y+o},n||(n=!0,requestAnimationFrame(()=>{n=!1;let{scrollX:i,scrollY:a,remainderX:o,remainderY:s}=extractWholeLines(t.x,t.y);t={x:o,y:s},(i!==0||a!==0)&&e.jsonrpc.notify(`input`,{kind:`wheel`,value:{...r,wheelX:-i,wheelY:-a}})}))}}function addMouseEventListeners({dom:e,editor:t,isDraggable:n,draggableStyle:r}){e.addEventListener(`contextmenu`,e=>{e.preventDefault()});let i=(e,n)=>{e.preventDefault();let[r,i]=t.getDisplayRectangle(),a=e.clientX-r,o=e.clientY-i,s=Math.floor(a/t.option.fontWidth),c=Math.floor(o/t.option.fontHeight);t.jsonrpc.notify(`input`,{kind:n,value:{x:s,y:c,pixelX:a,pixelY:o,button:e.button,clicks:e.detail}})};e.addEventListener(`mousedown`,e=>{n&&(document.body.style.cursor=r),t.focusHiddenInput(),i(e,`mousedown`)}),e.addEventListener(`mouseup`,e=>{n&&(document.body.style.cursor=`default`),i(e,`mouseup`)});let a=0;e.addEventListener(`mousemove`,e=>{e.preventDefault();let n=Date.now();if(n-a>50){a=n;let[r,i]=t.getDisplayRectangle(),o=e.clientX-r,s=e.clientY-i,c=Math.floor(o/t.option.fontWidth),l=Math.floor(s/t.option.fontHeight);t.jsonrpc.notify(`input`,{kind:`mousemove`,value:{x:c,y:l,pixelX:o,pixelY:s,button:e.buttons===0?null:e.buttons-1}})}}),n&&(e.addEventListener(`mouseover`,()=>{document.body.style.cursor=r}),e.addEventListener(`mouseout`,e=>{e.buttons!==1&&(document.body.style.cursor=`default`)})),e.addEventListener(`wheel`,makeWheelHandler(t))}var zIndexTable={"floating-window":200,modeline:100,"vertical-border":100,"horizontal-border":101};function zindex(e){return zIndexTable[e]||0}var borderOffsetX=5,borderOffsetY=10,BaseSurface=class{constructor({editor:e}){this.editor=e,this.mainDOM=null,this.wrapper=null}delete(){this.wrapper?getLemEditorElement().removeChild(this.wrapper):getLemEditorElement().removeChild(this.mainDOM)}setupDOM({dom:e,isFloating:t,border:n,cssClassName:r}){this.mainDOM=e,t&&n?(this.wrapper=document.createElement(`div`),r&&(this.wrapper.className=r),this.wrapper.style.position=`absolute`,this.wrapper.style.backgroundColor=this.editor.option.background,this.wrapper.style.zIndex=zindex(`floating-window`),this.wrapper.appendChild(e),getLemEditorElement().appendChild(this.wrapper)):(r&&(e.className=r),getLemEditorElement().appendChild(e))}move(e,t){let[n,r]=this.editor.getDisplayRectangle(),i=Math.floor(n+e),a=Math.floor(r+t);this.wrapper?(this.wrapper.style.left=i-borderOffsetX+`px`,this.wrapper.style.top=a-borderOffsetY+`px`,this.mainDOM.style.left=borderOffsetX+`px`,this.mainDOM.style.top=borderOffsetY+`px`):(this.mainDOM.style.left=i+`px`,this.mainDOM.style.top=a+`px`)}_resize(e,t){let n=window.devicePixelRatio||1;this.mainDOM.width=e*n,this.mainDOM.height=t*n,this.mainDOM.style.width=e+`px`,this.mainDOM.style.height=t+`px`,this.wrapper&&(this.wrapper.style.width=e+borderOffsetX*2+`px`,this.wrapper.style.height=t+borderOffsetY*2+`px`)}drawBlock(e,t,n,r,i){}drawText(e,t,n,r,i,a,o,s){}drawImage(e,t,n,r,i,a,o){}clearImages(e,t){}clearAllImages(){}touch(){}evalIn(code){return eval(code)}},CanvasSurface=class extends BaseSurface{constructor({editor:e,view:t,pixelX:n,pixelY:r,pixelWidth:i,pixelHeight:a,styles:o,isFloating:s,border:c,cssClassName:l}){super({editor:e});let u=this.setupCanvas(o);this.setupDOM({dom:u,isFloating:s,border:c,cssClassName:l}),this.move(n,r),this.resize(i,a),this.drawingQueue=[],addMouseEventListeners({dom:u,editor:e})}setupCanvas(e){let t=document.createElement(`canvas`);if(t.style.position=`absolute`,e)for(let n in e)t.style[n]=e[n];return t}resize(e,t){this._resize(e,t);let n=window.devicePixelRatio||1;this.mainDOM.getContext(`2d`).scale(n,n)}move(e,t){if(super.move(e,t),this.imageEls)for(let[,e]of this.imageEls)this.positionImage(e)}delete(){this.clearAllImages(),super.delete()}drawBlock(e,t,n,r,i){this.drawingQueue.push(function(a){drawBlock({ctx:a,x:e,y:t,width:n,height:r,style:i})})}drawText(e,t,n,r,i,a,o,s){let c=this.editor.option,l=o??t,u=s??c.fontHeight;this.drawingQueue.push(function(o){if(a=a?`${c.fontSize}px ${a}`:c.font,!i)drawBlock({ctx:o,x:e,y:l,width:r,height:u,style:c.background}),drawText({ctx:o,x:e,y:t,text:n,style:c.foreground,font:a,option:c});else{let{foreground:s,background:d,bold:f,reverse:p,underline:m,cursor:h}=i;if(s||=c.foreground,d||=c.background,p){let e=d;d=s,s=e}h&&(d=c.background),drawBlock({ctx:o,x:e,y:l,width:r,height:u,style:d}),drawText({ctx:o,x:e,y:t,text:n,style:s,font:f?`bold `+a:a,option:c}),m&&drawHorizontalLine({ctx:o,x:e,y:t+c.fontHeight-2,width:r,style:typeof m==`string`?m:s,lineWidth:2})}})}imageBaseLeft(){return parseFloat(this.mainDOM.style.left)||0}imageBaseTop(){return parseFloat(this.mainDOM.style.top)||0}drawImage(e,t,n,r,i,a,o){this.imageEls||=new Map;let s=e+`,`+t,c=this.imageEls.get(s);if(c&&c.url!==o&&(c.el.remove(),this.imageEls.delete(s),c=null),!c){let e=document.createElement(`img`);e.style.position=`absolute`,e.style.pointerEvents=`none`,e.style.zIndex=`1`,e.src=o,this.mainDOM.parentNode.appendChild(e),c={el:e,url:o},this.imageEls.set(s,c)}c.x=e,c.y=t,c.width=n,c.height=r,c.clipWidth=i,c.clipHeight=a,this.positionImage(c)}positionImage(e){e.el.style.left=this.imageBaseLeft()+e.x+`px`,e.el.style.top=this.imageBaseTop()+e.y+`px`,e.el.style.width=e.width+`px`,e.el.style.height=e.height+`px`;let t=e.clipWidth==null?0:Math.max(0,e.width-e.clipWidth),n=e.clipHeight==null?0:Math.max(0,e.height-e.clipHeight);e.el.style.clipPath=t>0||n>0?`inset(0px ${t}px ${n}px 0px)`:``}clearImages(e,t){if(this.imageEls)for(let[n,r]of this.imageEls){let i=r.y+(r.height||0);r.ye&&(r.el.remove(),this.imageEls.delete(n))}}clearAllImages(){if(this.imageEls){for(let[,e]of this.imageEls)e.el.remove();this.imageEls.clear()}}touch(){let e=this.mainDOM.getContext(`2d`);for(let t of this.drawingQueue)t(e);this.drawingQueue=[]}activate(){this.mainDOM.dataset.store=`active`}deactivate(){this.mainDOM.dataset.store=`inactive`}},HTMLSurface=class extends BaseSurface{constructor({editor:e,pixelX:t,pixelY:n,pixelWidth:r,pixelHeight:i,styles:a,option:o,isFloating:s,border:c,html:l}){super({editor:e});let u=document.createElement(`iframe`);this.setupDOM({dom:u,isFloating:s,border:c}),u.style.position=`absolute`,u.style.backgroundColor=o.background,u.setAttribute(`sandbox`,`allow-scripts allow-same-origin`),u.srcdoc=l,u.addEventListener(`load`,()=>{let e=u.contentWindow;e.invokeLem=(e,t)=>parent.postMessage({type:`invoke-lem`,method:e,args:t})}),this.iframe=u,this.move(t,n),this.resize(r,i)}resize(e,t){this._resize(e,t)}update(e){let t=this.iframe.contentWindow.scrollY;this.iframe.srcdoc=e,this.iframe.onload=()=>{this.iframe.onload=null,this.iframe.contentWindow.scrollTo(0,t)}}evalIn(e){return this.iframe.contentWindow.eval(e)}},VerticalBorder=class{constructor({x:e,y:t,height:n,option:r,editor:i}){this.option=r,this.editor=i,this.line=document.createElement(`div`),this.line.className=`lem-editor__vertical-border`,this.line.style.height=n+`px`,this.line.style.position=`absolute`,this.line.style.zIndex=zindex(`vertical-border`),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:i,isDraggable:!0,draggableStyle:`col-resize`})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.line.style.left=Math.floor(n+e-this.option.fontWidth/2)+`px`,this.line.style.top=r+t+`px`}resize(e){this.line.style.height=e+`px`}},HorizontalBorder=class{constructor({x:e,y:t,width:n,option:r,editor:i}){this.option=r,this.editor=i,this.line=document.createElement(`div`),this.line.className=`lem-editor__horizontal-border`,this.line.style.width=n+`px`,this.line.style.position=`absolute`,this.line.style.zIndex=zindex(`horizontal-border`),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:i,isDraggable:!0,draggableStyle:`row-resize`})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.line.style.left=n+e+`px`,this.line.style.top=Math.floor(r+t-4)+`px`}resize(e){this.line.style.width=e+`px`}},viewStyles={header:()=>{},tile:()=>{},floating:e=>({boxSizing:`border-box`,borderColor:e.foreground,backgroundColor:e.background})};function getViewStyle(e,t){return viewStyles[e](t)||{}}var View=class{constructor({id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,useModeline:l,kind:u,type:d,content:f,border:p,borderShape:m,option:h,editor:g}){switch(this.option=h,this.id=e,this.x=t,this.y=n,this.width=r,this.height=i,this.pixelX=a,this.pixelY=o,this.pixelWidth=s,this.pixelHeight=c,this.useModeline=l,this.kind=u,this.type=d,this.border=p,this.borderShape=m,this.editor=g,this.bottomBar=null,this.leftsideBar=null,u){case`tile`:this.mainSurface=this.makeSurface(d,f),this.leftSideBar=new VerticalBorder({x:a,y:o,height:c+(l?h.fontHeight:0),option:h,editor:g}),l||(this.bottomBar=new HorizontalBorder({x:a,y:o+c-h.fontHeight,width:s,option:h,editor:g}));break;case`header`:this.mainSurface=this.makeSurface(d,f);break;case`floating`:this.mainSurface=this.makeSurface(d,f),m===`left-border`&&(this.leftSideBar=new VerticalBorder({x:a,y:o,height:c,option:h,editor:g}));break}this.modelineSurface=l?this.makeModelineSurface():null}delete(){this.mainSurface.delete(),this.modelineSurface&&this.modelineSurface.delete(),this.leftSideBar&&this.leftSideBar.delete(),this.bottomBar&&this.bottomBar.delete()}move(e,t,n,r){this.x=e,this.y=t,this.pixelX=n,this.pixelY=r,this.mainSurface.move(n,r),this.modelineSurface&&this.modelineSurface.move(n,r+this.pixelHeight),this.leftSideBar&&this.leftSideBar.move(n,r),this.bottomBar&&this.bottomBar.move(n,r+this.pixelHeight)}resize(e,t,n,r){this.width=e,this.height=t,this.pixelWidth=n,this.pixelHeight=r,this.mainSurface.resize(n,r),this.modelineSurface&&(this.modelineSurface.move(this.pixelX,this.pixelY+r),this.modelineSurface.resize(n,this.option.fontHeight)),this.leftSideBar&&this.leftSideBar.resize(r+(this.modelineSurface?this.option.fontHeight:0)),this.bottomBar&&this.bottomBar.resize(n)}clear(){this.mainSurface.drawBlock(0,0,this.pixelWidth,this.pixelHeight,this.option.background),this.mainSurface.clearImages(0,this.pixelHeight)}clearEol(e,t,n){n??=this.option.fontHeight,this.mainSurface.drawBlock(e,t,this.pixelWidth-e,n,this.option.background),this.mainSurface.clearImages(t,t+n)}clearEob(e,t){this.mainSurface.drawBlock(e,t,this.pixelWidth,this.pixelHeight-t,this.option.background),this.mainSurface.clearImages(t,this.pixelHeight)}print(e,t,n,r,i,a,o,s){this.mainSurface.drawText(e,t,n,r,i,a,o,s)}drawBlock(e,t,n,r,i){this.mainSurface.drawBlock(e,t,n,r,i||this.option.background)}drawBlockOnModeline(e,t,n,r,i){this.modelineSurface&&this.modelineSurface.drawBlock(e,t,n,r,i||this.option.background)}printImage(e,t,n,r,i,a,o){this.mainSurface.drawImage(e,t,n,r,i,a,o)}printToModeline(e,t,n,r,i,a,o){this.modelineSurface&&this.modelineSurface.drawText(e,t,n,r,i,null,a,o)}touch(e){this.mainSurface.touch(),this.modelineSurface&&(this.modelineSurface.touch(),e?this.modelineSurface.activate():this.modelineSurface.deactivate())}makeSurface(e,t){switch(e){case`html`:return this.makeHTMLSurface(t);case`editor`:return this.makeEditorSurface();default:console.error(`unknown type: ${e}`)}}makeHTMLSurface(e){return new HTMLSurface({editor:this.editor,pixelX:this.pixelX,pixelY:this.pixelY,pixelWidth:this.pixelWidth,pixelHeight:this.pixelHeight,styles:getViewStyle(this.kind,this.option),option:this.option,isFloating:this.kind===`floating`,border:this.border,html:e})}makeEditorSurface(){let e=this.borderShape===`left-border`?0:this.border,t=this.kind===`floating`;return new CanvasSurface({option:this.editor.option,pixelX:this.pixelX,pixelY:this.pixelY,pixelWidth:this.pixelWidth,pixelHeight:this.pixelHeight,styles:getViewStyle(this.kind,this.option),editor:this.editor,border:e,isFloating:t,view:this,cssClassName:t&&e?`lem-editor__floating-window--bordered`:null})}makeModelineSurface(){let e=new CanvasSurface({option:this.editor.option,pixelX:this.pixelX,pixelY:this.pixelY+this.pixelHeight,pixelWidth:this.pixelWidth,pixelHeight:this.option.fontHeight,editor:this.editor,view:this,styles:{zIndex:zindex(`modeline`)},cssClassName:`lem-editor__mode-line`});return addMouseEventListeners({dom:e.mainDOM,editor:this.editor,isDraggable:!0,draggableStyle:`row-resize`}),e}changeToHTMLContent(e){this.mainSurface.constructor.name===`HTMLSurface`?this.mainSurface.update(e):(this.mainSurface.delete(),this.mainSurface=this.makeHTMLSurface(e))}changeToEditorContent(){this.mainSurface.delete(),this.mainSurface=this.makeEditorSurface()}evalIn(e){return this.mainSurface.evalIn(e)}};function isPasteKeyEvent(e){return isMacOS()?e.metaKey&&e.key===`v`:e.ctrlKey&&e.shiftKey&&e.key===`V`}var Input=class{constructor(e){let t=e.option;this.editor=e,this.composition=!1,this.ignoreKeydownAfterCompositionend=!1,this.span=document.createElement(`span`),this.span.style.color=t.foreground,this.span.style.backgroundColor=t.background,this.span.style.position=`absolute`,this.span.style.zIndex=1e6,this.span.style.top=`0`,this.span.style.left=`0`,this.span.style.font=t.font,this.input=document.createElement(`input`),this.input.style.backgroundColor=`transparent`,this.input.style.color=`transparent`,this.input.style.width=`0`,this.input.style.padding=`0`,this.input.style.margin=`0`,this.input.style.border=`none`,this.input.style.position=`absolute`,this.input.style.zIndex=`-10`,this.input.style.top=`0`,this.input.style.left=`0`,this.input.style.font=t.font,this.input.addEventListener(`blur`,e=>{this.input.focus()}),this.input.addEventListener(`input`,e=>{this.composition===!1&&(this.input.value=``,this.span.innerHTML=``,this.input.style.width=`0`,isMacOS()||this.editor.emitInputString(e.data))}),this.input.addEventListener(`paste`,async e=>{e.preventDefault();let t=e.clipboardData||window.Clipboard.data,n=t?.getData(`text`)??t?.getData(`text/plain`);if(n&&n.length>0){this.editor.emitInputString(n);return}try{if(navigator.clipboard?.readText){let e=await navigator.clipboard.readText();if(e&&e.length>0){this.editor.emitInputString(e);return}}}catch(e){console.warn(`clipboard.readText() failed:`,e)}alert(`Paste failed (permission/environment restriction`)}),this.input.addEventListener(`keydown`,e=>{if(!isPasteKeyEvent(e)&&!(e.isComposing||this.composition)&&e.key!==`Process`){if(this.ignoreKeydownAfterCompositionend&&(isSafari||isMacOS())){e.preventDefault(),this.ignoreKeydownAfterCompositionend=!1;return}if(!(!isMacOS()&&!e.ctrlKey&&!e.altKey&&e.key.length===1)&&(e.preventDefault(),e.isComposing!==!0&&e.code!==``))return setTimeout(()=>{this.composition||(this.editor.emitInput(e),this.input.value=``)},0),!1}}),this.input.addEventListener(`compositionstart`,e=>{this.composition=!0,this.span.innerHTML=this.input.value,this.input.style.width=this.span.offsetWidth+`px`}),this.input.addEventListener(`compositionupdate`,e=>{this.span.innerHTML=e.data,this.input.style.width=this.span.offsetWidth+`px`}),this.input.addEventListener(`compositionend`,e=>{this.composition=!1,this.editor.emitInputString(this.input.value),this.input.value=``,this.span.innerHTML=this.input.value,this.input.style.width=`0`,this.ignoreKeydownAfterCompositionend=!0}),document.body.appendChild(this.input),document.body.appendChild(this.span),this.input.focus()}finalize(){document.body.removeChild(this.input),document.body.removeChild(this.span)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.span.style.top=r+t+`px`,this.span.style.left=n+e+`px`,this.input.style.top=this.span.offsetTop+`px`,this.input.style.left=this.span.offsetLeft+`px`}updateForeground(e){this.span.style.color=e}updateBackground(e){this.span.style.backgroundColor=e}},MessageTable=class{constructor(){this.map=new Map}register(e,t){for(let n in t){let r=t[n];this.map.set(n,r),e.on(n,r)}}get(e){return this.map.get(e)}};function getDisplayRectangleDefault(){return[0,0,window.innerWidth,window.innerHeight]}var Editor=class{constructor({getDisplayRectangle:e=getDisplayRectangleDefault,fontName:t,fontSize:n,url:r,onExit:i,onClosed:a}){this.getDisplayRectangle=e,this.option=new Option({fontName:t,fontSize:n}),this.onExit=i,this.input=new Input(this),this.cursors=new Map,this.cursorOverlay=document.createElement(`div`),this.cursorOverlay.className=`lem-cursor`,this.cursorOverlay.style.width=this.option.fontWidth+`px`,this.cursorOverlay.style.height=this.option.fontHeight+`px`,this.cursorOverlay.style.backgroundColor=`#ffffff`,this.cursorType=`box`,this.viewMap=new Map,this.jsonrpc=new JSONRPC(r,{onClosed:()=>{a()}}),this.messageTable=new MessageTable,this.messageTable.register(this.jsonrpc,{"update-foreground":this.updateForeground.bind(this),"update-background":this.updateBackground.bind(this),"make-view":this.makeView.bind(this),"delete-view":this.deleteView.bind(this),"resize-view":this.resize.bind(this),"move-view":this.move.bind(this),"redraw-view-after":this.redrawViewAfter.bind(this),clear:this.clear.bind(this),"clear-eol":this.clearEol.bind(this),"clear-eob":this.clearEob.bind(this),put:this.put.bind(this),"put-image":this.putImage.bind(this),"modeline-put":this.modelinePut.bind(this),"draw-block":this.drawBlock.bind(this),"modeline-draw-block":this.modelineDrawBlock.bind(this),"update-display":this.updateDisplay.bind(this),"move-cursor":this.moveCursor.bind(this),"change-view":this.changeView.bind(this),"resize-display":this.resizeDisplay.bind(this),bulk:this.bulk.bind(this),exit:this.exitEditor.bind(this),"get-clipboard-text":this.getClipboardText.bind(this),"set-clipboard-text":this.setClipboardText.bind(this),"js-eval":this.jsEval.bind(this),"set-font":this.setFont.bind(this),"get-font":this.getFont.bind(this),"get-display-size":this.getDisplaySize.bind(this),"load-css":this.loadCSS.bind(this),"update-cursor-shape":this.updateCursorShape.bind(this)}),this.login(),this.boundedHandleResize=this.handleResize.bind(this),this.focusHiddenInput=this.focusHiddenInput.bind(this)}init(){window.addEventListener(`resize`,this.boundedHandleResize),document.getElementsByTagName(`html`)[0].style[`background-color`]=`#333`,getLemEditorElement().appendChild(this.cursorOverlay)}finalize(){window.removeEventListener(`resize`,this.boundedHandleResize),this.input.finalize(),this.cursorOverlay.parentNode&&this.cursorOverlay.parentNode.removeChild(this.cursorOverlay)}closeConnection(){this.jsonrpc.close()}emitInput(e){let t=convertKeyEvent(e);if(t){if(t.key===`]`&&t.ctrl&&!t.meta&&!t.super&&!t.shift){this.jsonrpc.notify(`input`,{kind:`abort`});return}t.key!==`Unidentified`&&this.jsonrpc.notify(`input`,{kind:`key`,value:t})}}emitInputString(e){e?this.jsonrpc.notify(`input`,{kind:`input-string`,value:e}):console.error(`unexpected argument`,e)}redrawParams(){return{size:this.getDisplaySize(),fontWidth:this.option.fontWidth,fontHeight:this.option.fontHeight,fontAscent:this.option.fontAscent,fontSize:this.option.fontSize}}handleResize(e){this.jsonrpc.notify(`redraw`,this.redrawParams())}focusHiddenInput(){let e=this.input?.input;if(e){try{window.focus()}catch{}requestAnimationFrame(()=>{setTimeout(()=>{e.focus({preventScroll:!0})},0)})}}sendNotification(e,t){this.jsonrpc.notify(e,t)}request(e,t,n){this.jsonrpc.request(e,t,n)}getDisplaySize(){let[e,t,n,r]=this.getDisplayRectangle();return{width:Math.floor(n/this.option.fontWidth),height:Math.floor(r/this.option.fontHeight)}}callMessage(e,t){this.messageTable.get(e)(t)}findViewById(e){return this.viewMap.get(e)}login(){this.jsonrpc.request(`login`,{size:this.getDisplaySize(),foreground:this.option.foreground,background:this.option.background,fontWidth:this.option.fontWidth,fontHeight:this.option.fontHeight,fontAscent:this.option.fontAscent,fontSize:this.option.fontSize},e=>{if(this.updateForeground(e.foreground),this.updateBackground(e.background),e.views)for(let t of e.views)this.makeView(t);this.jsonrpc.notify(`redraw`,this.redrawParams())})}updateForeground(e){e!=null&&(this.option.foreground=e,this.input.updateForeground(e))}updateBackground(e){if(e==null)return;this.option.background=e,this.input.updateBackground(e);let t=getLemEditorElement();t.style.backgroundColor=e}makeView({id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,use_modeline:l,kind:u,type:d,content:f,border:p,border_shape:m}){let h=new View({option:this.option,id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,useModeline:l,kind:u,type:d,content:f,border:p,borderShape:m,editor:this});this.viewMap.set(e,h)}deleteView({viewInfo:{id:e}}){this.findViewById(e).delete(),this.viewMap.delete(e)}resize({viewInfo:{id:e},width:t,height:n,pixelWidth:r,pixelHeight:i}){let a=this.findViewById(e);a?a.resize(t,n,r,i):console.warn(`resize: view not found for id ${e}`)}move({viewInfo:{id:e},x:t,y:n,pixelX:r,pixelY:i}){let a=this.findViewById(e);a?a.move(t,n,r,i):console.warn(`move: view not found for id ${e}`)}redrawViewAfter({viewInfo:{id:e},isActive:t}){this.findViewById(e).touch(t)}clear({viewInfo:{id:e}}){this.findViewById(e).clear()}clearEol({viewInfo:{id:e},x:t,y:n,height:r}){this.findViewById(e).clearEol(t,n,r)}clearEob({viewInfo:{id:e},x:t,y:n}){this.findViewById(e).clearEob(t,n)}put({viewInfo:{id:e},x:t,y:n,text:r,textWidth:i,attribute:a,font:o,backgroundY:s,backgroundHeight:c}){this.findViewById(e).print(t,n,r,i,a,o,s,c)}drawBlock({viewInfo:{id:e},x:t,y:n,width:r,height:i,color:a}){this.findViewById(e).drawBlock(t,n,r,i,a)}modelineDrawBlock({viewInfo:{id:e},x:t,y:n,width:r,height:i,color:a}){this.findViewById(e).drawBlockOnModeline(t,n,r,i,a)}putImage({viewInfo:{id:e},x:t,y:n,pixelWidth:r,pixelHeight:i,clipWidth:a,clipHeight:o,url:s}){this.findViewById(e).printImage(t,n,r,i,a,o,s)}modelinePut({viewInfo:{id:e},x:t,y:n,text:r,textWidth:i,attribute:a,backgroundY:o,backgroundHeight:s}){this.findViewById(e).printToModeline(t,n,r,i,a,o,s)}updateDisplay(){}moveCursor({viewInfo:{id:e},x:t,y:n,color:r,cursorText:i,cursorForeground:a}){let o=this.findViewById(e),[s,c]=this.getDisplayRectangle(),l=o.pixelX+t,u=o.pixelY+n;this.input.move(l,u);let d=r||this.option.foreground,f=a||this.option.background,p=this.cursorOverlay;switch(this.cursorType){case`bar`:p.style.left=s+l+`px`,p.style.top=c+u+`px`,p.style.width=`2px`,p.style.height=this.option.fontHeight+`px`,p.style.backgroundColor=d,p.textContent=``,p.style.color=``,p.style.font=``,p.style.paddingTop=``;break;case`underline`:p.style.left=s+l+`px`,p.style.top=c+u+this.option.fontHeight-2+`px`,p.style.width=this.option.fontWidth+`px`,p.style.height=`2px`,p.style.backgroundColor=d,p.textContent=``,p.style.color=``,p.style.font=``,p.style.paddingTop=``;break;default:p.style.left=s+l+`px`,p.style.top=c+u+`px`,p.style.width=this.option.fontWidth+`px`,p.style.height=this.option.fontHeight+`px`,p.style.backgroundColor=d,p.style.font=this.option.font,p.style.paddingTop=textOffsetY+`px`,p.textContent=i||``,p.style.color=f;break}p.style.animation=`none`,p.offsetHeight,p.style.animation=``}updateCursorShape({cursorType:e}){this.cursorType=e||`box`}changeView({viewInfo:{id:e},type:t,content:n}){let r=this.findViewById(e);switch(t){case`html`:r.changeToHTMLContent(n);break;case`editor`:r.changeToEditorContent();break}}resizeDisplay({width:e,height:t}){let n=getLemEditorElement();n.style.width=Math.floor(e*this.option.fontWidth)+`px`,n.style.height=Math.floor(t*this.option.fontHeight)+`px`}bulk(e){for(let{method:t,argument:n}of e)this.callMessage(t,n)}exitEditor(){this.onExit&&this.onExit()}getClipboardText(){navigator.clipboard?.readText().then(e=>{this.jsonrpc.notify(`got-clipboard-text`,{text:e})})}setClipboardText({text:e}){navigator.clipboard&&navigator.clipboard.writeText(e)}jsEval({viewInfo:{id:e},code:t}){let n=this.findViewById(e).evalIn(t);return n&&n.toString()}setFont({fontName:e,fontSize:t}){this.option.setFont(e||this.option.fontName,t||this.option.fontSize),this.cursorOverlay.style.width=this.option.fontWidth+`px`,this.cursorOverlay.style.height=this.option.fontHeight+`px`,this.jsonrpc.notify(`redraw`,this.redrawParams())}getFont(){return{name:this.option.fontName,size:this.option.fontSize}}loadCSS({content:e}){let t=document.createElement(`style`);t.textContent=e,document.head.appendChild(t)}notifyToServer(e,t){this.jsonrpc.notify(`invoke`,{method:e,args:t})}},canvas=document.querySelector(`#editor`);async function main(){await Promise.all([document.fonts.load(`19px file-icons`),document.fonts.load(`19px AllTheIcons`),document.fonts.load(`19px fontawesome`),document.fonts.load(`19px material-design-icons`),document.fonts.load(`19px octicons`)]),await document.fonts.ready;let e=new Editor({canvas,fontName:`Monospace`,fontSize:18,url:`${window.location.protocol===`https:`?`wss`:`ws`}://${window.location.hostname}:${window.location.port}`,onExit:null,onClosed:null});window.addEventListener(`message`,t=>{t.data.type===`invoke-lem`&&e.notifyToServer(t.data.method,t.data.args)}),e.init()}main(); \ No newline at end of file diff --git a/frontends/server/frontend/editor.js b/frontends/server/frontend/editor.js index 55027113a..bda811abd 100644 --- a/frontends/server/frontend/editor.js +++ b/frontends/server/frontend/editor.js @@ -36,6 +36,7 @@ function computeFontSize(font) { return [ Math.floor(textMetrics.width), Math.round(textMetrics.fontBoundingBoxAscent + textOffsetY + (textMetrics.emHeightDescent || 0)), + Math.round(textMetrics.fontBoundingBoxAscent + textOffsetY), ]; } @@ -80,11 +81,12 @@ class Option { setFont(fontName, fontSize) { const font = fontSize + 'px ' + fontName; - const [width, height] = computeFontSize(font); + const [width, height, ascent] = computeFontSize(font); this.fontName = fontName; this.fontSize = fontSize; this.fontWidth = width; this.fontHeight = height; + this.fontAscent = ascent; this.font = font; } } @@ -313,11 +315,11 @@ class BaseSurface { } } - move(x, y, pixelX, pixelY) { + // every coordinate and size in this class and its subclasses use pixels as a unit (not cells) + move(x, y) { const [x0, y0] = this.editor.getDisplayRectangle(); - // Use pixel coordinates if provided, otherwise calculate from character coordinates - const left = (pixelX != null) ? Math.floor(x0 + pixelX) : Math.floor(x0 + x * this.editor.option.fontWidth); - const top = (pixelY != null) ? Math.floor(y0 + pixelY) : Math.floor(y0 + y * this.editor.option.fontHeight); + const left = Math.floor(x0 + x); + const top = Math.floor(y0 + y); if (this.wrapper) { this.wrapper.style.left = left - borderOffsetX + 'px'; this.wrapper.style.top = top - borderOffsetY + 'px'; @@ -329,23 +331,25 @@ class BaseSurface { } } - _resize(width, height, pixelWidth, pixelHeight) { + _resize(width, height) { const ratio = window.devicePixelRatio || 1; - // Use pixel dimensions if provided, otherwise calculate from character dimensions - const actualWidth = (pixelWidth != null) ? pixelWidth : width * this.editor.option.fontWidth; - const actualHeight = (pixelHeight != null) ? pixelHeight : height * this.editor.option.fontHeight; - this.mainDOM.width = actualWidth * ratio; - this.mainDOM.height = actualHeight * ratio; - this.mainDOM.style.width = actualWidth + 'px'; - this.mainDOM.style.height = actualHeight + 'px'; + this.mainDOM.width = width * ratio; + this.mainDOM.height = height * ratio; + this.mainDOM.style.width = width + 'px'; + this.mainDOM.style.height = height + 'px'; if (this.wrapper) { - this.wrapper.style.width = actualWidth + borderOffsetX * 2 + 'px'; - this.wrapper.style.height = actualHeight + borderOffsetY * 2 + 'px'; + this.wrapper.style.width = width + borderOffsetX * 2 + 'px'; + this.wrapper.style.height = height + borderOffsetY * 2 + 'px'; } } + // drawing coordinates are relative to the surface's own top-left corner. drawBlock(x, y, width, height, color) { } - drawText(x, y, text, textWidth, attribute) { } + drawText(x, y, text, textWidth, attribute, font, backgroundY, backgroundHeight) { } + drawImage(x, y, width, height, clipWidth, clipHeight, url) { } + + clearImages(yStart, yEnd) { } + clearAllImages() { } touch() { return; @@ -357,13 +361,14 @@ class BaseSurface { } class CanvasSurface extends BaseSurface { - constructor({ editor, view, x, y, width, height, styles, isFloating, border, cssClassName }) { + constructor({ editor, view, pixelX, pixelY, pixelWidth, pixelHeight, + styles, isFloating, border, cssClassName }) { super({ editor }); const canvas = this.setupCanvas(styles); this.setupDOM({ dom: canvas, isFloating, border, cssClassName }); - this.move(x, y); - this.resize(width, height); + this.move(pixelX, pixelY); + this.resize(pixelWidth, pixelHeight); this.drawingQueue = []; @@ -381,44 +386,52 @@ class CanvasSurface extends BaseSurface { return canvas; } - resize(width, height, pixelWidth, pixelHeight) { - this._resize(width, height, pixelWidth, pixelHeight); + resize(width, height) { + this._resize(width, height); const ratio = window.devicePixelRatio || 1; const ctx = this.mainDOM.getContext('2d'); ctx.scale(ratio, ratio); } + move(x, y) { + super.move(x, y); + if (this.imageEls) { + for (const [, entry] of this.imageEls) this.positionImage(entry); + } + } + + delete() { + this.clearAllImages(); + super.delete(); + } + drawBlock(x, y, width, height, color) { - const option = this.editor.option; this.drawingQueue.push(function(ctx) { - drawBlock({ - ctx, - x: x * option.fontWidth, - y: y * option.fontHeight, - width: width * option.fontWidth, - height: height * option.fontHeight, - style: color, - }) + drawBlock({ ctx, x, y, width, height, style: color }) }); } - drawText(x, y, text, textWidth, attribute, font) { + // the background is filled first, then the text over it. it covers the row the text sits on, + // which an image can make taller, and defaults to one line at the text's own y. + drawText(x, y, text, textWidth, attribute, font, backgroundY, backgroundHeight) { const option = this.editor.option; + const blockY = backgroundY == null ? y : backgroundY; + const blockHeight = backgroundHeight == null ? option.fontHeight : backgroundHeight; this.drawingQueue.push(function(ctx) { font = font ? `${option.fontSize}px ${font}` : option.font; if (!attribute) { drawBlock({ ctx, - x: x * option.fontWidth, - y: y * option.fontHeight, - width: textWidth * option.fontWidth, - height: option.fontHeight, + x: x, + y: blockY, + width: textWidth, + height: blockHeight, style: option.background, }); drawText({ ctx, - x: x * option.fontWidth, - y: y * option.fontHeight, + x: x, + y: y, text: text, style: option.foreground, font: font, @@ -442,20 +455,18 @@ class CanvasSurface extends BaseSurface { // when the cursor overlay blinks off. background = option.background; } - const gx = x * option.fontWidth; - const gy = y * option.fontHeight; drawBlock({ ctx, - x: gx, - y: gy, - width: textWidth * option.fontWidth, - height: option.fontHeight, + x: x, + y: blockY, + width: textWidth, + height: blockHeight, style: background, }); drawText({ ctx, - x: gx, - y: gy, + x: x, + y: y, text: text, style: foreground, font: bold ? ('bold ' + font) : font, @@ -464,9 +475,9 @@ class CanvasSurface extends BaseSurface { if (underline) { drawHorizontalLine({ ctx, - x: gx, - y: gy + option.fontHeight - 2, - width: textWidth * option.fontWidth, + x: x, + y: y + option.fontHeight - 2, + width: textWidth, style: typeof (underline) === 'string' ? underline : foreground, lineWidth: 2 }); @@ -475,6 +486,75 @@ class CanvasSurface extends BaseSurface { }); } + // images are rendered as DOM elements on a layer above the canvas rather than into it. + imageBaseLeft() { return parseFloat(this.mainDOM.style.left) || 0; } + imageBaseTop() { return parseFloat(this.mainDOM.style.top) || 0; } + + drawImage(x, y, width, height, clipWidth, clipHeight, url) { + if (!this.imageEls) + // mapping "x,y" to { el, url, x, y, width, height, clipWidth, clipHeight } + this.imageEls = new Map(); + const key = x + ',' + y; + let entry = this.imageEls.get(key); + if (entry && entry.url !== url) { + entry.el.remove(); + this.imageEls.delete(key); + entry = null; + } + if (!entry) { + const el = document.createElement('img'); + el.style.position = 'absolute'; + el.style.pointerEvents = 'none'; + // above the surface's own canvas but below the modeline/floating windows. + el.style.zIndex = '1'; + el.src = url; + this.mainDOM.parentNode.appendChild(el); + entry = { el, url }; + this.imageEls.set(key, entry); + } + entry.x = x; + entry.y = y; + entry.width = width; + entry.height = height; + entry.clipWidth = clipWidth; + entry.clipHeight = clipHeight; + this.positionImage(entry); + } + + positionImage(entry) { + entry.el.style.left = (this.imageBaseLeft() + entry.x) + 'px'; + entry.el.style.top = (this.imageBaseTop() + entry.y) + 'px'; + entry.el.style.width = entry.width + 'px'; + entry.el.style.height = entry.height + 'px'; + // show only the part the server said is visible. the element keeps its full size and + // clip-path hides the rest, since shrinking it would squash the picture. + const clipRight = entry.clipWidth == null + ? 0 : Math.max(0, entry.width - entry.clipWidth); + const clipBottom = entry.clipHeight == null + ? 0 : Math.max(0, entry.height - entry.clipHeight); + entry.el.style.clipPath = (clipRight > 0 || clipBottom > 0) + ? `inset(0px ${clipRight}px ${clipBottom}px 0px)` + : ''; + } + + // remove image elements whose vertical span intersects [yStart, yEnd). + clearImages(yStart, yEnd) { + if (!this.imageEls) return; + for (const [key, entry] of this.imageEls) { + const bottom = entry.y + (entry.height || 0); + if (entry.y < yEnd && bottom > yStart) { + entry.el.remove(); + this.imageEls.delete(key); + } + } + } + + clearAllImages() { + if (!this.imageEls) return; + for (const [, entry] of this.imageEls) entry.el.remove(); + this.imageEls.clear(); + } + touch() { const ctx = this.mainDOM.getContext('2d'); for (let fn of this.drawingQueue) { @@ -493,7 +573,8 @@ class CanvasSurface extends BaseSurface { } class HTMLSurface extends BaseSurface { - constructor({ editor, x, y, width, height, styles, option, isFloating, border, html }) { + constructor({ editor, pixelX, pixelY, pixelWidth, pixelHeight, + styles, option, isFloating, border, html }) { super({ editor }); const iframe = document.createElement('iframe'); @@ -511,12 +592,12 @@ class HTMLSurface extends BaseSurface { this.iframe = iframe; - this.move(x, y); - this.resize(width, height); + this.move(pixelX, pixelY); + this.resize(pixelWidth, pixelHeight); } - resize(width, height, pixelWidth, pixelHeight) { - this._resize(width, height, pixelWidth, pixelHeight); + resize(width, height) { + this._resize(width, height); } update(content) { @@ -533,13 +614,14 @@ class HTMLSurface extends BaseSurface { } } +// x, y and height are in pixels. class VerticalBorder { constructor({ x, y, height, option, editor }) { this.option = option; this.editor = editor; this.line = document.createElement('div'); this.line.className = 'lem-editor__vertical-border'; - this.line.style.height = height * option.fontHeight + 'px'; + this.line.style.height = height + 'px'; this.line.style.position = 'absolute'; this.line.style.zIndex = zindex('vertical-border'); @@ -561,22 +643,23 @@ class VerticalBorder { move(x, y) { const [x0, y0] = this.editor.getDisplayRectangle(); - this.line.style.left = Math.floor(x0 + x * this.option.fontWidth - this.option.fontWidth / 2) + 'px'; - this.line.style.top = (y0 + y * this.option.fontHeight) + 'px'; + this.line.style.left = Math.floor(x0 + x - this.option.fontWidth / 2) + 'px'; + this.line.style.top = (y0 + y) + 'px'; } resize(height) { - this.line.style.height = height * this.option.fontHeight + 'px'; + this.line.style.height = height + 'px'; } } +// x, y and width are in pixels. class HorizontalBorder { constructor({ x, y, width, option, editor }) { this.option = option; this.editor = editor; this.line = document.createElement('div'); this.line.className = 'lem-editor__horizontal-border'; - this.line.style.width = width * option.fontWidth + 'px'; + this.line.style.width = width + 'px'; this.line.style.position = 'absolute'; this.line.style.zIndex = zindex('horizontal-border'); @@ -598,12 +681,12 @@ class HorizontalBorder { move(x, y) { const [x0, y0] = this.editor.getDisplayRectangle(); - this.line.style.left = (x0 + x * this.option.fontWidth) + 'px'; - this.line.style.top = Math.floor(y0 + y * this.option.fontHeight - 4) + 'px'; + this.line.style.left = (x0 + x) + 'px'; + this.line.style.top = Math.floor(y0 + y - 4) + 'px'; } resize(width) { - this.line.style.width = (width * this.option.fontWidth) + 'px'; + this.line.style.width = width + 'px'; } } @@ -665,17 +748,18 @@ class View { case 'tile': this.mainSurface = this.makeSurface(type, content); this.leftSideBar = new VerticalBorder({ - x: x, - y: y, - height: height + (useModeline ? 1 : 0), + x: pixelX, + y: pixelY, + height: pixelHeight + (useModeline ? option.fontHeight : 0), option: option, editor: editor, }); if (!useModeline) { this.bottomBar = new HorizontalBorder({ - x: x, - y: y + height - 1, - width: width, + x: pixelX, + // along the last row of the view, not below it. + y: pixelY + pixelHeight - option.fontHeight, + width: pixelWidth, option: option, editor: editor, }); @@ -688,9 +772,9 @@ class View { this.mainSurface = this.makeSurface(type, content); if (borderShape === 'left-border') { this.leftSideBar = new VerticalBorder({ - x: x, - y: y, - height: height, + x: pixelX, + y: pixelY, + height: pixelHeight, option: option, editor: editor, }); @@ -699,11 +783,6 @@ class View { } this.modelineSurface = useModeline ? this.makeModelineSurface() : null; - - // For floating windows with pixel coordinates, reposition using pixel coordinates - if (kind === 'floating' && (pixelX != null || pixelY != null)) { - this.move(x, y, pixelX, pixelY); - } } delete() { @@ -725,19 +804,15 @@ class View { this.pixelX = pixelX; this.pixelY = pixelY; - this.mainSurface.move(x, y, pixelX, pixelY); + this.mainSurface.move(pixelX, pixelY); if (this.modelineSurface) { - // Calculate modeline pixel position if pixel coordinates are provided - const modelinePixelY = (pixelY != null && this.pixelHeight != null) - ? pixelY + this.pixelHeight - : null; - this.modelineSurface.move(x, y + this.height, pixelX, modelinePixelY); + this.modelineSurface.move(pixelX, pixelY + this.pixelHeight); } if (this.leftSideBar) { - this.leftSideBar.move(x, y); + this.leftSideBar.move(pixelX, pixelY); } if (this.bottomBar) { - this.bottomBar.move(x, y + this.height); + this.bottomBar.move(pixelX, pixelY + this.pixelHeight); } } @@ -746,25 +821,16 @@ class View { this.height = height; this.pixelWidth = pixelWidth; this.pixelHeight = pixelHeight; - this.mainSurface.resize(width, height, pixelWidth, pixelHeight); + this.mainSurface.resize(pixelWidth, pixelHeight); if (this.modelineSurface) { - // Calculate modeline pixel position if pixel coordinates are provided - const modelinePixelY = (this.pixelY != null && pixelHeight != null) - ? this.pixelY + pixelHeight - : null; - this.modelineSurface.move( - this.x, - this.y + this.height, - this.pixelX, - modelinePixelY, - ); - this.modelineSurface.resize(width, 1); + this.modelineSurface.move(this.pixelX, this.pixelY + pixelHeight); + this.modelineSurface.resize(pixelWidth, this.option.fontHeight); } if (this.leftSideBar) { - this.leftSideBar.resize(height + (this.modelineSurface ? 1 : 0)); + this.leftSideBar.resize(pixelHeight + (this.modelineSurface ? this.option.fontHeight : 0)); } if (this.bottomBar) { - this.bottomBar.resize(width); + this.bottomBar.resize(pixelWidth); } } @@ -772,33 +838,37 @@ class View { this.mainSurface.drawBlock( 0, 0, - this.width, - this.height, + this.pixelWidth, + this.pixelHeight, this.option.background, ); + this.mainSurface.clearImages(0, this.pixelHeight); } - clearEol(x, y) { + clearEol(x, y, height) { + if (height == null) height = this.option.fontHeight; this.mainSurface.drawBlock( x, y, - this.width - x, - 1, + this.pixelWidth - x, + height, this.option.background, ); + this.mainSurface.clearImages(y, y + height); } clearEob(x, y) { this.mainSurface.drawBlock( x, // x === 0 y, - this.width, - this.height - y, + this.pixelWidth, + this.pixelHeight - y, this.option.background, ); + this.mainSurface.clearImages(y, this.pixelHeight); } - print(x, y, text, textWidth, attribute, font) { + print(x, y, text, textWidth, attribute, font, backgroundY, backgroundHeight) { this.mainSurface.drawText( x, y, @@ -806,10 +876,28 @@ class View { textWidth, attribute, font, + backgroundY, + backgroundHeight, ); } - printToModeline(x, y, text, textWidth, attribute) { + // a fill of its own, for a rectangle that is not one line of text tall. a missing color is the + // editor's default background, as an attribute with no background of its own would be. + drawBlock(x, y, width, height, color) { + this.mainSurface.drawBlock(x, y, width, height, color || this.option.background); + } + + drawBlockOnModeline(x, y, width, height, color) { + if (this.modelineSurface) { + this.modelineSurface.drawBlock(x, y, width, height, color || this.option.background); + } + } + + printImage(x, y, pixelWidth, pixelHeight, clipWidth, clipHeight, url) { + this.mainSurface.drawImage(x, y, pixelWidth, pixelHeight, clipWidth, clipHeight, url); + } + + printToModeline(x, y, text, textWidth, attribute, backgroundY, backgroundHeight) { if (this.modelineSurface) { this.modelineSurface.drawText( x, @@ -817,6 +905,9 @@ class View { text, textWidth, attribute, + null, + backgroundY, + backgroundHeight, ); } } @@ -847,10 +938,10 @@ class View { makeHTMLSurface(content) { return new HTMLSurface({ editor: this.editor, - x: this.x, - y: this.y, - width: this.width, - height: this.height, + pixelX: this.pixelX, + pixelY: this.pixelY, + pixelWidth: this.pixelWidth, + pixelHeight: this.pixelHeight, styles: getViewStyle(this.kind, this.option), option: this.option, isFloating: this.kind === 'floating', @@ -865,10 +956,10 @@ class View { return new CanvasSurface({ option: this.editor.option, - x: this.x, - y: this.y, - width: this.width, - height: this.height, + pixelX: this.pixelX, + pixelY: this.pixelY, + pixelWidth: this.pixelWidth, + pixelHeight: this.pixelHeight, styles: getViewStyle(this.kind, this.option), editor: this.editor, border, @@ -881,10 +972,10 @@ class View { makeModelineSurface() { const surface = new CanvasSurface({ option: this.editor.option, - x: this.x, - y: this.y + this.height, - width: this.width, - height: 1, + pixelX: this.pixelX, + pixelY: this.pixelY + this.pixelHeight, + pixelWidth: this.pixelWidth, + pixelHeight: this.option.fontHeight, editor: this.editor, view: this, styles: { zIndex: zindex('modeline') }, @@ -1166,7 +1257,10 @@ export class Editor { 'clear-eol': this.clearEol.bind(this), 'clear-eob': this.clearEob.bind(this), 'put': this.put.bind(this), + 'put-image': this.putImage.bind(this), 'modeline-put': this.modelinePut.bind(this), + 'draw-block': this.drawBlock.bind(this), + 'modeline-draw-block': this.modelineDrawBlock.bind(this), 'update-display': this.updateDisplay.bind(this), 'move-cursor': this.moveCursor.bind(this), 'change-view': this.changeView.bind(this), @@ -1230,13 +1324,20 @@ export class Editor { } } + // the server draws in pixels, so it needs our cell size. sent with every redraw, which is how a + // font change reaches it. + redrawParams() { + return { + size: this.getDisplaySize(), + fontWidth: this.option.fontWidth, + fontHeight: this.option.fontHeight, + fontAscent: this.option.fontAscent, + fontSize: this.option.fontSize, + }; + } + handleResize(event) { - const canResize = true; - if (canResize) { - this.jsonrpc.notify('redraw', { size: this.getDisplaySize() }); - } else { - this.jsonrpc.notify('redraw'); - } + this.jsonrpc.notify('redraw', this.redrawParams()); } focusHiddenInput() { @@ -1280,6 +1381,10 @@ export class Editor { size: this.getDisplaySize(), foreground: this.option.foreground, background: this.option.background, + fontWidth: this.option.fontWidth, + fontHeight: this.option.fontHeight, + fontAscent: this.option.fontAscent, + fontSize: this.option.fontSize, }, (response) => { this.updateForeground(response.foreground); this.updateBackground(response.background); @@ -1289,7 +1394,7 @@ export class Editor { } } - this.jsonrpc.notify('redraw', { size: this.getDisplaySize() }); + this.jsonrpc.notify('redraw', this.redrawParams()); }); } @@ -1364,9 +1469,9 @@ export class Editor { view.clear(); } - clearEol({ viewInfo: { id }, x, y }) { + clearEol({ viewInfo: { id }, x, y, height }) { const view = this.findViewById(id); - view.clearEol(x, y); + view.clearEol(x, y, height); } clearEob({ viewInfo: { id }, x, y }) { @@ -1374,14 +1479,29 @@ export class Editor { view.clearEob(x, y); } - put({ viewInfo: { id }, x, y, text, textWidth, attribute, font }) { + put({ viewInfo: { id }, x, y, text, textWidth, attribute, font, backgroundY, backgroundHeight }) { + const view = this.findViewById(id); + view.print(x, y, text, textWidth, attribute, font, backgroundY, backgroundHeight); + } + + drawBlock({ viewInfo: { id }, x, y, width, height, color }) { const view = this.findViewById(id); - view.print(x, y, text, textWidth, attribute, font); + view.drawBlock(x, y, width, height, color); } - modelinePut({ viewInfo: { id }, x, y, text, textWidth, attribute }) { + modelineDrawBlock({ viewInfo: { id }, x, y, width, height, color }) { const view = this.findViewById(id); - view.printToModeline(x, y, text, textWidth, attribute); + view.drawBlockOnModeline(x, y, width, height, color); + } + + putImage({ viewInfo: { id }, x, y, pixelWidth, pixelHeight, clipWidth, clipHeight, url }) { + const view = this.findViewById(id); + view.printImage(x, y, pixelWidth, pixelHeight, clipWidth, clipHeight, url); + } + + modelinePut({ viewInfo: { id }, x, y, text, textWidth, attribute, backgroundY, backgroundHeight }) { + const view = this.findViewById(id); + view.printToModeline(x, y, text, textWidth, attribute, backgroundY, backgroundHeight); } updateDisplay() { @@ -1390,8 +1510,9 @@ export class Editor { moveCursor({ viewInfo: { id }, x, y, color, cursorText, cursorForeground }) { const view = this.findViewById(id); const [x0, y0] = this.getDisplayRectangle(); - const left = view.x * this.option.fontWidth + x * this.option.fontWidth; - const top = view.y * this.option.fontHeight + y * this.option.fontHeight; + // x and y are pixels within the view. the view's own origin is in pixels too. + const left = view.pixelX + x; + const top = view.pixelY + y; this.input.move(left, top); const cursorColor = color || this.option.foreground; @@ -1503,6 +1624,11 @@ export class Editor { fontName || this.option.fontName, fontSize || this.option.fontSize, ); + this.cursorOverlay.style.width = this.option.fontWidth + 'px'; + this.cursorOverlay.style.height = this.option.fontHeight + 'px'; + // the cell size is the unit the server draws in, so nothing on screen is still correct, + // send the new params and let the server lay the display out again. + this.jsonrpc.notify('redraw', this.redrawParams()); } getFont() { diff --git a/frontends/server/main.lisp b/frontends/server/main.lisp index 22545579e..b1b376ed2 100644 --- a/frontends/server/main.lisp +++ b/frontends/server/main.lisp @@ -109,7 +109,19 @@ quits by itself afterwards.") (message-queue :initform (queue:make-queue) :reader jsonrpc-message-queue) (editor-thread :initform nil - :accessor jsonrpc-editor-thread)) + :accessor jsonrpc-editor-thread) + ;; pixel size of one character cell. multiplied into every coordinate we send, so never NIL: + ;; we guess, and the client corrects at login. + (cell-width :initform 8 + :accessor jsonrpc-cell-width) + (cell-height :initform 16 + :accessor jsonrpc-cell-height) + ;; how far below a cell's top the client puts the text baseline. + (cell-ascent :initform nil + :accessor jsonrpc-cell-ascent) + ;; the font's own size. the cell height is measured from the glyph bounding box, so it is larger. + (font-em :initform nil + :accessor jsonrpc-font-em)) (:default-initargs :name :jsonrpc :redraw-after-modifying-floating-window t @@ -118,7 +130,8 @@ quits by itself afterwards.") :html-support t :underline-color-support t :no-force-needed t - :support-pixel-positioning t)) + :support-pixel-positioning t + :image-support t)) (defun view-id-hash (view) "Return a minimal hash table containing only the view ID. @@ -160,6 +173,22 @@ the same immutable instance for every subsequent message." 'vector))) (notify jsonrpc "bulk" argument))) +(defun update-cell-metrics (jsonrpc params) + "take the client's font metrics out of PARAMS, if it sent any. +returns true when one of them changed, since nothing already measured survives a new cell size." + (let ((changed)) + (flet ((update (key accessor) + (alexandria:when-let ((value (gethash key params))) + (when (and (realp value) (plusp value) + (not (eql value (funcall accessor jsonrpc)))) + (funcall (fdefinition `(setf ,accessor)) value jsonrpc) + (setf changed t))))) + (update "fontWidth" 'jsonrpc-cell-width) + (update "fontHeight" 'jsonrpc-cell-height) + (update "fontAscent" 'jsonrpc-cell-ascent) + (update "fontSize" 'jsonrpc-font-em)) + changed)) + (defun handle-login (jsonrpc logged-in-callback params) (with-error-handler () (let* ((size (gethash "size" params)) @@ -170,6 +199,7 @@ the same immutable instance for every subsequent message." (let ((width (gethash "width" size)) (height (gethash "height" size))) (resize-display jsonrpc width height))) + (update-cell-metrics jsonrpc params) (when background (alexandria:when-let (color (lem:parse-color background)) (setf (jsonrpc-background-color jsonrpc) color))) @@ -191,14 +221,22 @@ the same immutable instance for every subsequent message." (defun redraw (args) (with-error-handler () - (let ((size (and args (gethash "size" args)))) + (let ((size (and args (gethash "size" args))) + ;; the client re-sends its font metrics here, so a font change reaches us by the same + ;; path as a resize instead of needing one of its own. + (metrics-changed (and args (update-cell-metrics (lem:implementation) args)))) (when size (let ((width (gethash "width" size)) (height (gethash "height" size))) (resize-display (lem:implementation) width height) (notify (lem:implementation) "resize-display" size))) (lem:send-event (lambda () + (when metrics-changed + ;; the scroll position was recorded in the old cell size + (dolist (window (lem:window-list)) + (setf (lem-core::horizontal-scroll-start window) 0))) (lem-core::adjust-all-window-size) + ;; :force clears the caches, whose widths are stale after a cell-size change (lem:redraw-display :force t)))))) (defvar *invoke-method-table* (make-hash-table :test 'equal)) @@ -252,10 +290,14 @@ the same immutable instance for every subsequent message." (defmethod lem-if:update-foreground ((jsonrpc jsonrpc) color-name) (with-error-handler () + (alexandria:when-let (color (lem:parse-color color-name)) + (setf (jsonrpc-foreground-color jsonrpc) color)) (notify jsonrpc "update-foreground" color-name))) (defmethod lem-if:update-background ((jsonrpc jsonrpc) color-name) (with-error-handler () + (alexandria:when-let (color (lem:parse-color color-name)) + (setf (jsonrpc-background-color jsonrpc) color)) (notify jsonrpc "update-background" color-name))) (defmethod lem-if:update-cursor-shape ((jsonrpc jsonrpc) cursor-type) @@ -315,10 +357,10 @@ the same immutable instance for every subsequent message." view)) (defmethod lem-if:view-width ((jsonrpc jsonrpc) view) - (view-width view)) + (view-px-width view)) (defmethod lem-if:view-height ((jsonrpc jsonrpc) view) - (view-height view)) + (view-px-height view)) (defmethod lem-if:delete-view ((jsonrpc jsonrpc) view) (with-error-handler () @@ -335,7 +377,9 @@ the same immutable instance for every subsequent message." "resize-view" (hash "viewInfo" (view-id-hash view) "width" width - "height" height)))) + "height" height + "pixelWidth" (view-px-width view) + "pixelHeight" (view-px-height view))))) (defmethod lem-if:set-view-pos ((jsonrpc jsonrpc) view x y) (with-error-handler () @@ -344,7 +388,9 @@ the same immutable instance for every subsequent message." "move-view" (hash "viewInfo" (view-id-hash view) "x" x - "y" y)))) + "y" y + "pixelX" (view-px-x view) + "pixelY" (view-px-y view))))) (defmethod lem-if:make-view-with-pixels ((jsonrpc jsonrpc) window x y width height pixel-x pixel-y pixel-width pixel-height @@ -380,8 +426,8 @@ the same immutable instance for every subsequent message." (hash "viewInfo" (view-id-hash view) "x" x "y" y - "pixelX" pixel-x - "pixelY" pixel-y)))) + "pixelX" (view-px-x view) + "pixelY" (view-px-y view))))) (defmethod lem-if:set-view-size-pixels ((jsonrpc jsonrpc) view width height pixel-width pixel-height) (with-error-handler () @@ -391,8 +437,8 @@ the same immutable instance for every subsequent message." (hash "viewInfo" (view-id-hash view) "width" width "height" height - "pixelWidth" pixel-width - "pixelHeight" pixel-height)))) + "pixelWidth" (view-px-width view) + "pixelHeight" (view-px-height view))))) (defmethod lem-if:redraw-view-before ((jsonrpc jsonrpc) view) ) @@ -469,12 +515,19 @@ the same immutable instance for every subsequent message." (defmethod lem-if:get-mouse-position ((jsonrpc jsonrpc)) (mouse:get-position)) -(defmethod lem-if:get-char-width ((jsonrpc jsonrpc)) - ;; TODO - 1) -(defmethod lem-if:get-char-height ((jsonrpc jsonrpc)) - ;; TODO - 1) +(defmethod lem-if:cell-width ((jsonrpc jsonrpc)) + (jsonrpc-cell-width jsonrpc)) + +(defmethod lem-if:cell-height ((jsonrpc jsonrpc)) + (jsonrpc-cell-height jsonrpc)) + +(defmethod lem-if:cell-pixel-size ((jsonrpc jsonrpc)) + (values (jsonrpc-cell-width jsonrpc) + (jsonrpc-cell-height jsonrpc) + (jsonrpc-cell-ascent jsonrpc))) + +(defmethod lem-if:font-em-pixels ((jsonrpc jsonrpc)) + (jsonrpc-font-em jsonrpc)) (defun call (method params) (let ((mailbox (sb-concurrency:make-mailbox :name "lem-server-call-async"))) @@ -574,29 +627,14 @@ the same immutable instance for every subsequent message." ;;; drawing -(defgeneric object-width (drawing-object)) -(defmethod object-width ((drawing-object display:void-object)) - 0) +(defgeneric draw-object (jsonrpc object x y view row) + (:documentation "draw OBJECT into VIEW with its top-left corner at pixel position X, Y. +`lem-core/display:layout-row' already positioned it, so ROW is only for what an object shares with +the rest of its row: the full height a background fills and where the row's text sits, which is +where the caret goes.")) -(defmethod object-width ((drawing-object display:text-object)) - (lem-core:string-width (display:text-object-string drawing-object))) - -(defmethod object-width ((drawing-object display:eol-cursor-object)) - 0) - -(defmethod object-width ((drawing-object display:extend-to-eol-object)) - 0) - -(defmethod object-width ((drawing-object display:line-end-object)) - (lem-core:string-width (lem-core/display:text-object-string drawing-object))) - -(defmethod object-width ((drawing-object display:image-object)) - 0) - -(defgeneric draw-object (jsonrpc object x y view)) - -(defmethod draw-object (jsonrpc (object display:void-object) x y view) +(defmethod draw-object (jsonrpc (object display:void-object) x y view row) (values)) (defvar *put-target* :edit-area) @@ -634,25 +672,50 @@ same hash." (setf attribute (lem:make-attribute :background lem-if:*background-color-of-drawing-window*))) (attribute-to-hash attribute))) -(defun put (jsonrpc view x y string attribute &key font text-width) +(defun taller-than-text-p (jsonrpc row) + "whether ROW is taller than a line of text, which an image on it can make it." + (and row (> (display:row-height row) (jsonrpc-cell-height jsonrpc)))) + +(defun put (jsonrpc view x y string attribute &key font text-width row) + "draw STRING at pixel position X, Y in VIEW, over a background TEXT-WIDTH wide and as tall as ROW." + (with-error-handler () + (let ((tall (taller-than-text-p jsonrpc row))) + (notify* jsonrpc + (ecase *put-target* + (:edit-area "put") + (:modeline "modeline-put")) + (hash "viewInfo" (view-id-hash view) + "x" x + "y" y + "text" string + "textWidth" (or text-width + (* (lem:string-width string) (jsonrpc-cell-width jsonrpc))) + "backgroundY" (and tall (display:row-top row)) + "backgroundHeight" (and tall (display:row-height row)) + "attribute" (ensure-attribute attribute) + "font" font))))) + +(defun draw-block (jsonrpc view x y width height color) + "fill the WIDTH by HEIGHT rectangle at pixel position X, Y in VIEW with COLOR. +unlike `put', which is one line of text tall, this covers a row an image made taller. a NIL COLOR +leaves the client to use its default background." (with-error-handler () (notify* jsonrpc (ecase *put-target* - (:edit-area "put") - (:modeline "modeline-put")) + (:edit-area "draw-block") + (:modeline "modeline-draw-block")) (hash "viewInfo" (view-id-hash view) "x" x "y" y - "text" string - "textWidth" (or text-width (lem:string-width string)) - "attribute" (ensure-attribute attribute) - "font" font)))) + "width" width + "height" height + "color" (and color (lem:color-to-hex-string color)))))) -(defmethod draw-object (jsonrpc (object display:text-object) x y view) +(defmethod draw-object (jsonrpc (object display:text-object) x y view row) (let* ((string (display:text-object-string object)) (attribute (display:text-object-attribute object)) (type (display:text-object-type object)) - (width (object-width object))) + (width (lem-if:object-width jsonrpc object))) (when (and attribute (lem-core:cursor-attribute-p attribute)) (lem-core:set-last-print-cursor (view-window view) x y)) (put jsonrpc @@ -661,13 +724,14 @@ same hash." y string attribute - :text-width width))) + :text-width width + :row row))) -(defmethod draw-object (jsonrpc (object display:icon-object) x y view) +(defmethod draw-object (jsonrpc (object display:icon-object) x y view row) (let* ((string (display:text-object-string object)) (attribute (display:text-object-attribute object)) (type (display:text-object-type object)) - (width (object-width object))) + (width (lem-if:object-width jsonrpc object))) (when (and attribute (lem-core:cursor-attribute-p attribute)) (lem-core:set-last-print-cursor (view-window view) x y)) (put jsonrpc @@ -677,83 +741,140 @@ same hash." string attribute :text-width width + :row row :font (lem:icon-value (char-code (char string 0)) :font)))) -(defmethod draw-object (jsonrpc (object display:eol-cursor-object) x y view) +(defmethod draw-object (jsonrpc (object display:eol-cursor-object) x y view row) (lem-core:set-last-print-cursor (view-window view) x y) (let ((attr (lem:make-attribute :background (lem:color-to-hex-string (display:eol-cursor-object-color object))))) (lem-core:set-cursor-attribute attr) - (put jsonrpc view x y " " attr :text-width 1))) - -(defmethod draw-object (jsonrpc (object display:extend-to-eol-object) x y view) - (let ((width (lem-if:view-width (lem-core:implementation) view))) - (when (< x width) - (let ((fill-width (- width x))) - (put jsonrpc view x y - (make-string fill-width :initial-element #\space) - (lem:make-attribute - :background - (lem:color-to-hex-string (display:extend-to-eol-object-color object))) - :text-width fill-width))))) - -(defmethod draw-object (jsonrpc (object display:line-end-object) x y view) + (put jsonrpc view x y " " attr :text-width (jsonrpc-cell-width jsonrpc)))) + +(defmethod draw-object (jsonrpc (object display:line-end-object) x y view row) (let ((string (display:text-object-string object)) (attribute (display:text-object-attribute object)) - (width (object-width object))) + (width (lem-if:object-width jsonrpc object))) (put jsonrpc view - (+ x (display:line-end-object-offset object)) + ;; the offset is a column count, unlike the x it is added to. + (+ x (* (display:line-end-object-offset object) (jsonrpc-cell-width jsonrpc))) y string attribute - :text-width width))) - -(defmethod draw-object (jsonrpc (object display:image-object) x y view) - (values)) - -(defun render-line (jsonrpc view x y objects) - (loop :for object :in objects - :do (draw-object jsonrpc object x y view) - (incf x (object-width object)))) - -(defun render-line-from-behind (jsonrpc view y objects) - (loop :with current-x := (view-width view) - :for object :in objects - :do (decf current-x (object-width object)) - (draw-object jsonrpc object current-x y view))) - -(defmethod lem-if:render-line ((jsonrpc jsonrpc) view x y objects height) + :text-width width + :row row))) + +(defun image-object-url (object) + "return a URL the JS client can load for OBJECT's image, or NIL. +a pathname or plain-string path is served through the existing /local static route. +a string already carrying a data:/https: URL is passed through unchanged." + (let ((image (display:image-object-image object))) + (typecase image + (pathname (format nil "/local~A" (namestring image))) + (string (if (or (alexandria:starts-with-subseq "data:" image) + (alexandria:starts-with-subseq "http:" image) + (alexandria:starts-with-subseq "https:" image)) + image + (format nil "/local~A" image))) + (t nil)))) + +(defun attribute-own-background (attribute) + "the background ATTRIBUTE asks for as a color, or NIL when it asks for none. +not `lem:attribute-background-with-reverse', which answers with the default background rather than NIL." + (alexandria:when-let ((background (if (lem:attribute-reverse attribute) + (lem:attribute-foreground attribute) + (lem:attribute-background attribute)))) + (typecase background + (lem:color background) + (string (lem:parse-color background))))) + +(defun row-text-top (jsonrpc row) + "the top of a line of text on ROW: its baseline less the font's ascent." + (- (display:row-baseline row) + (or (jsonrpc-cell-ascent jsonrpc) (jsonrpc-cell-height jsonrpc)))) + +(defmethod draw-object (jsonrpc (object display:image-object) x y view row) + (alexandria:when-let ((attribute (lem:ensure-attribute (display:image-object-attribute object) + nil))) + ;; the image carries the attribute of the text it replaced, so selecting the line reaches it too + (alexandria:when-let ((color (attribute-own-background attribute))) + (draw-block jsonrpc view x (display:row-top row) (lem-if:object-width jsonrpc object) + (display:row-height row) color)) + ;; the cursor can sit on an image. Y is the image's top, which can be far above the row's text, + ;; so report the text's top instead and the caret aligns with the text. + (when (lem-core:cursor-attribute-p attribute) + (lem-core:set-last-print-cursor (view-window view) x (row-text-top jsonrpc row)))) + (let ((url (image-object-url object))) + (when url + (with-error-handler () + (let* ((pw (display:image-draw-width jsonrpc object)) + (ph (display:image-draw-height jsonrpc object)) + ;; how much may appear: the crop the layout applied, and the room left in the view. + ;; an image is a DOM element over the view, not pixels in it, so nothing clips it + ;; for us. + (clip-width (min pw + (max 0 (- (view-px-width view) x)) + (or (display:image-object-visible-width object) pw))) + (clip-height (min ph (max 0 (- (view-px-height view) y))))) + (notify* jsonrpc + "put-image" + (hash "viewInfo" (view-id-hash view) + "x" x + "y" y + "pixelWidth" pw + "pixelHeight" ph + ;; the visible part, from the image's top-left + "clipWidth" clip-width + "clipHeight" clip-height + "url" url))))))) + +(defun draw-row (jsonrpc view row) + "draw ROW's background fill, then everything placed on it. +the fill covers the row's full height, which a tall object (e.g. an image) can push past a single +text line's, so it goes as a `draw-block' rather than a put's background." + (let ((width (view-px-width view))) + (when (and (display:row-fill-color row) + (< (display:row-fill-x row) width)) + (draw-block jsonrpc + view + (display:row-fill-x row) + (display:row-top row) + (- width (display:row-fill-x row)) + (display:row-height row) + (display:row-fill-color row)))) + (loop :for placement :in (display:row-placements row) + :do (draw-object jsonrpc + (display:placement-object placement) + (display:placement-x placement) + (display:placement-top placement) + view + row))) + +(defmethod lem-if:render-row ((jsonrpc jsonrpc) view row) (with-error-handler () (notify* jsonrpc "clear-eol" (hash "viewInfo" (view-id-hash view) - "x" x - "y" y)) - (render-line jsonrpc view x y objects))) + "x" 0 + "y" (display:row-top row) + "height" (display:row-height row))) + (draw-row jsonrpc view row))) -(defmethod lem-if:render-line-on-modeline ((jsonrpc jsonrpc) view left-objects right-objects - default-attribute height) +(defmethod lem-if:render-modeline-row ((jsonrpc jsonrpc) view row default-attribute) + ;; the modeline has a surface of its own here, so the row is drawn where it was laid out. (let ((*put-target* :modeline)) - (with-error-handler () - (notify* jsonrpc - "modeline-put" - (hash "viewInfo" (view-id-hash view) - "x" 0 - "y" 0 - "text" (make-string (view-width view) :initial-element #\space) - "textWidth" (view-width view) - "attribute" (attribute-to-hash default-attribute))) - (render-line jsonrpc view 0 0 left-objects) - (render-line-from-behind jsonrpc view 0 right-objects)))) - -(defmethod lem-if:object-width ((jsonrpc jsonrpc) drawing-object) - (object-width drawing-object)) - -(defmethod lem-if:object-height ((jsonrpc jsonrpc) drawing-object) - 1) + ;; the modeline's own background, under everything the row places on it + (draw-block jsonrpc + view + 0 + (display:row-top row) + (view-px-width view) + (display:row-height row) + (lem:attribute-background-with-reverse default-attribute)) + (draw-row jsonrpc view row))) (defmethod lem-if:clear-to-end-of-window ((jsonrpc jsonrpc) view y) (notify* jsonrpc diff --git a/frontends/server/view.lisp b/frontends/server/view.lisp index fe607b8e8..b5c26de81 100644 --- a/frontends/server/view.lisp +++ b/frontends/server/view.lisp @@ -13,6 +13,10 @@ :view-pixel-y :view-pixel-width :view-pixel-height + :view-px-x + :view-px-y + :view-px-width + :view-px-height :view-use-modeline :view-kind :move-view @@ -51,35 +55,63 @@ use-modeline kind border border-shape)) (apply #'%make-view args)) +(defun cell-pixel-size () + "The pixel size of one character cell, as (values WIDTH HEIGHT). +Never NIL here: this frontend starts from a guess and the client corrects it at login." + (lem-if:cell-pixel-size (lem:implementation))) + +(defun view-px-x (view) + "VIEW's left edge in pixels." + (or (view-pixel-x view) + (* (view-x view) (nth-value 0 (cell-pixel-size))))) + +(defun view-px-y (view) + "VIEW's top edge in pixels." + (or (view-pixel-y view) + (* (view-y view) (nth-value 1 (cell-pixel-size))))) + +(defun view-px-width (view) + "VIEW's width in pixels." + (or (view-pixel-width view) + (* (view-width view) (nth-value 0 (cell-pixel-size))))) + +(defun view-px-height (view) + "VIEW's height in pixels, the edit area only, since the modeline is a surface of its own." + (or (view-pixel-height view) + (* (view-height view) (nth-value 1 (cell-pixel-size))))) + (defun move-view (view x y &optional pixel-x pixel-y) - "Move view to new position. Pixel coordinates are optional." + "Move VIEW to cell position X, Y, or to PIXEL-X / PIXEL-Y for an axis given in pixels. +Passing no pixel position clears any earlier one, so the view follows the cell grid again." (setf (view-x view) x - (view-y view) y) - (when pixel-x (setf (view-pixel-x view) pixel-x)) - (when pixel-y (setf (view-pixel-y view) pixel-y))) + (view-y view) y + (view-pixel-x view) pixel-x + (view-pixel-y view) pixel-y) + (values)) (defun resize-view (view width height &optional pixel-width pixel-height) - "Resize view. Pixel dimensions are optional." + "Resize VIEW to WIDTH x HEIGHT cells, or to PIXEL-WIDTH / PIXEL-HEIGHT for a dimension given in +pixels. As in `move-view', passing no pixel size clears any earlier one." (setf (view-width view) width - (view-height view) height) - (when pixel-width (setf (view-pixel-width view) pixel-width)) - (when pixel-height (setf (view-pixel-height view) pixel-height)) + (view-height view) height + (view-pixel-width view) pixel-width + (view-pixel-height view) pixel-height) (values)) (defmethod yason:encode ((view view) &optional (stream *standard-output*)) (yason:with-output (stream) (yason:with-object () (yason:encode-object-element "id" (view-id view)) - ;; Character-unit coordinates (for backward compatibility) + ;; the cell geometry the core laid this view out on (yason:encode-object-element "x" (view-x view)) (yason:encode-object-element "y" (view-y view)) (yason:encode-object-element "width" (view-width view)) (yason:encode-object-element "height" (view-height view)) - ;; Pixel coordinates (new) - (yason:encode-object-element "pixelX" (view-pixel-x view)) - (yason:encode-object-element "pixelY" (view-pixel-y view)) - (yason:encode-object-element "pixelWidth" (view-pixel-width view)) - (yason:encode-object-element "pixelHeight" (view-pixel-height view)) + ;; and in pixels, always present, so the client never needs the cell size + (yason:encode-object-element "pixelX" (view-px-x view)) + (yason:encode-object-element "pixelY" (view-px-y view)) + (yason:encode-object-element "pixelWidth" (view-px-width view)) + (yason:encode-object-element "pixelHeight" (view-px-height view)) ;; Other existing fields (yason:encode-object-element "use_modeline" (view-use-modeline view)) (yason:encode-object-element "kind" (view-kind view)) diff --git a/src/color-theme.lisp b/src/color-theme.lisp index 760301ecc..b97c69fd4 100644 --- a/src/color-theme.lisp +++ b/src/color-theme.lisp @@ -101,7 +101,7 @@ for example, to maintain an attribute like CURSOR.") ;; The per-window drawing-cache compares attributes via ensure-attribute, ;; which resolves to the *current* theme on both sides — so after a color ;; change the cache silently treats every line as unchanged and skips - ;; render-line. With :no-force-needed implementations (e.g. webview), + ;; render-row. With :no-force-needed implementations (e.g. webview), ;; (redraw-display :force t) above also strips force, so non-current ;; windows never invalidate their cache. Mark every window dirty so ;; clear-cache-if-screen-modified drops the stale cache. diff --git a/src/display/logical-line.lisp b/src/display/logical-line.lisp index b2a043e0c..8b2d12ae3 100644 --- a/src/display/logical-line.lisp +++ b/src/display/logical-line.lisp @@ -6,8 +6,17 @@ "a display-only string fragment injected at a character position within a logical line." ;; 0-based position in the line's string where this fragment is inserted charpos - string - attribute) + ;; list of (string attribute) runs, drawn in order. see `virtual-text-runs'. + runs) + +(defun virtual-text-runs (spec) + "an overlay's :before-string / :after-string as a list of (string attribute) runs. +SPEC is a bare string, a single (string attribute) pair, or a list of such pairs" + (cond ((stringp spec) (list (list spec nil))) + ((not (consp spec)) nil) + ((stringp (first spec)) (list (list (first spec) (second spec)))) + (t (loop :for (run-string run-attribute) :in spec + :collect (list run-string run-attribute))))) (defstruct logical-line string @@ -378,18 +387,14 @@ several folds that each hide arbitrary character ranges across multiple buffer l :for before-str := (overlay-get overlay :before-string) :for after-str := (overlay-get overlay :after-string) :do (when (and before-str (start-in-line-p overlay)) - (let ((bs (alexandria:ensure-list before-str))) - (push (make-virtual-item :charpos (overlay-start-charpos overlay) - :string (first bs) - :attribute (second bs)) - virtual-items))) + (push (make-virtual-item :charpos (overlay-start-charpos overlay) + :runs (virtual-text-runs before-str)) + virtual-items)) (when (and after-str (end-in-line-p overlay)) - (let ((as (alexandria:ensure-list after-str))) - (push (make-virtual-item :charpos (or (overlay-end-charpos overlay) - (length string)) - :string (first as) - :attribute (second as)) - virtual-items)))) + (push (make-virtual-item :charpos (or (overlay-end-charpos overlay) + (length string)) + :runs (virtual-text-runs after-str)) + virtual-items))) ;; markers were positioned in raw coordinates; remap them into the ;; spliced string so several folds on one visual line stay anchored. (dolist (vi virtual-items) @@ -409,8 +414,7 @@ several folds that each hide arbitrary character ranges across multiple buffer l :when (>= (virtual-item-charpos vi) charpos) :collect (make-virtual-item :charpos (- (virtual-item-charpos vi) charpos) - :string (virtual-item-string vi) - :attribute (virtual-item-attribute vi)))))) + :runs (virtual-item-runs vi)))))) (make-logical-line :string string :attributes attributes @@ -440,6 +444,10 @@ several folds that each hide arbitrary character ranges across multiple buffer l attribute offset) +;; a newline inside virtual text (an overlay's :before-string / :after-string): ends the screen row +;; without touching the buffer line. +(defstruct line-break-item) + (defmethod item-string ((item string-with-attribute-item)) (string-with-attribute-item-string item)) @@ -518,12 +526,21 @@ VIRTUAL-ITEMS arrive in draw order (from `create-logical-line')." (items)) (flet ((add-virtuals-at (pos) (loop :while (and pending (= (virtual-item-charpos (first pending)) pos)) - :do (let ((vi (pop pending))) - (setf items (add-or-merge-item - (make-string-with-attribute-item - :string (virtual-item-string vi) - :attribute (virtual-item-attribute vi)) - items)))))) + ;; runs follow one another on the same row, each keeping its own attribute. + :do (loop :for (run-string run-attribute) :in (virtual-item-runs (pop pending)) + ;; a newline ends the screen row rather than being drawn, so the + ;; segments around it become items with a break between them. + :do (loop :for segment :in (uiop:split-string + run-string + :separator '(#\newline)) + :for firstp := t :then nil + :do (unless firstp + (setf items (cons (make-line-break-item) items))) + (setf items (add-or-merge-item + (make-string-with-attribute-item + :string segment + :attribute run-attribute) + items))))))) ;; walk segments between break positions, injecting virtual items at each boundary (loop :for (pos . rest) :on positions :while rest @@ -618,12 +635,19 @@ VIRTUAL-ITEMS arrive in draw order (from `create-logical-line')." (*active-modes* active-modes)) (loop :for logical-line := (create-logical-line point overlays active-modes) :do (when logical-line - (funcall function logical-line)) + (funcall function logical-line point)) (loop (unless (line-offset point 1) (return-from call-do-logical-line)) (unless (line-continuation-p point) (return))))))) -(defmacro do-logical-line ((logical-line window) &body body) - `(call-do-logical-line ,window (lambda (,logical-line) ,@body))) +(defmacro do-logical-line ((logical-line window &optional point) &body body) + "Run BODY for each logical line of WINDOW, in draw order. +POINT, when named, is bound to the start of the line. It is one point reused for every line and +moved on to the next once BODY returns, so BODY must `copy-point' it to hold on to it." + (let ((point-var (or point (gensym "POINT")))) + `(call-do-logical-line ,window + (lambda (,logical-line ,point-var) + (declare (ignorable ,point-var)) + ,@body)))) diff --git a/src/display/physical-line.lisp b/src/display/physical-line.lisp index 9fb31f600..72483667c 100644 --- a/src/display/physical-line.lisp +++ b/src/display/physical-line.lisp @@ -15,10 +15,15 @@ (setf (window-parameter window 'redrawing-cache) value)) (defclass drawing-object () - ((width :initform nil :accessor drawing-object-width))) + ;; where `object-width' caches its result. `width' is left free for subclasses like `image-object'. + ((occupied-width :initform nil :accessor drawing-object-width))) (defclass void-object (drawing-object) ()) +;; from a `line-break-item', consumed while splitting a line into rows, so it never reaches a +;; frontend. +(defclass line-break-object (void-object) ()) + (defclass text-object (drawing-object) ((surface :initarg :surface :initform nil :accessor text-object-surface) (string :initarg :string :reader text-object-string) @@ -58,9 +63,94 @@ (defclass image-object (drawing-object) ((image :initarg :image :reader image-object-image) + ;; the size the image is drawn at, in pixels, or NIL for its natural one. (width :initarg :width :reader image-object-width) (height :initarg :height :reader image-object-height) - (attribute :initarg :attribute :reader image-object-attribute))) + (attribute :initarg :attribute :reader image-object-attribute) + ;; columns of the line the image accounts for, so a click can be turned back into a position. + (columns :initarg :columns + :initform 1 + :reader image-object-columns) + ;; how much of the width may be shown, or NIL for all of it. see `crop-image-object'. + (visible-width :initarg :visible-width + :initform nil + :reader image-object-visible-width))) + +(defun image-object-ascent (object height) + "How much of OBJECT's image, drawn HEIGHT tall, sits above the text baseline. +Taken from the object's `:ascent' attribute: a percentage of HEIGHT, 50 by default. `:center' +instead puts the middle of the image on the middle of a line of text." + ;; attribute-value* rather than attribute-value: an object's attribute may be a name, as + ;; `attribute-image' above it allows. + (let ((ascent (or (attribute-value* (image-object-attribute object) :ascent) + 50))) + (if (eq ascent :center) + (multiple-value-bind (text-ascent text-height) (text-row-metrics) + (round (+ (/ height 2) (- text-ascent (/ text-height 2))))) + (round (* height (/ (max 0 (min 100 ascent)) 100)))))) + +(defun image-draw-width (implementation object) + "Pixel width OBJECT's image is drawn at. +:width on the object is a pixel count. An image carrying none is drawn at its natural size if the +frontend can report one (`lem-if:image-natural-size'), otherwise one cell wide." + (or (image-object-width object) + (nth-value 0 (lem-if:image-natural-size implementation (image-object-image object))) + (lem-if:cell-width implementation))) + +(defun image-draw-height (implementation object) + "Pixel height OBJECT's image is drawn at, as `image-draw-width' on the other axis." + (or (image-object-height object) + (nth-value 1 (lem-if:image-natural-size implementation (image-object-image object))) + (lem-if:cell-height implementation))) + +(defmethod lem-if:object-width (implementation (drawing-object void-object)) + 0) + +(defmethod lem-if:object-width (implementation (drawing-object text-object)) + (* (string-width (text-object-string drawing-object)) + (lem-if:cell-width implementation))) + +(defmethod lem-if:object-width (implementation (drawing-object eol-cursor-object)) + 0) + +(defmethod lem-if:object-width (implementation (drawing-object extend-to-eol-object)) + 0) + +(defmethod lem-if:object-width (implementation (drawing-object image-object)) + ;; a cropped image occupies only what it was cropped to, see `crop-image-object'. + (let ((width (image-draw-width implementation drawing-object))) + (alexandria:if-let ((visible (image-object-visible-width drawing-object))) + (min width visible) + width))) + +(defmethod lem-if:object-height (implementation (drawing-object drawing-object)) + (lem-if:cell-height implementation)) + +(defmethod lem-if:object-height (implementation (drawing-object image-object)) + (image-draw-height implementation drawing-object)) + +(defmethod lem-if:object-ascent (implementation (drawing-object drawing-object)) + ;; anything drawn in the editor's font shares that font's baseline, the cell ascent, when the + ;; frontend reports one. + (multiple-value-bind (cell-width cell-height cell-ascent) + (lem-if:cell-pixel-size implementation) + (declare (ignore cell-width cell-height)) + (or cell-ascent (lem-if:object-height implementation drawing-object)))) + +(defmethod lem-if:object-ascent (implementation (drawing-object image-object)) + (image-object-ascent drawing-object (lem-if:object-height implementation drawing-object))) + +(defun crop-image-object (object width) + "A copy of OBJECT allowed to occupy only WIDTH, in the units `object-width' counts in." + (make-instance 'image-object + :image (image-object-image object) + :width (image-object-width object) + :height (image-object-height object) + :attribute (image-object-attribute object) + :columns (image-object-columns object) + :visible-width (alexandria:if-let ((visible (image-object-visible-width object))) + (min width visible) + width))) (defmethod cursor-object-p (drawing-object) nil) @@ -103,9 +193,15 @@ (line-end-object-offset drawing-object-2)))) (defmethod drawing-object-equal ((drawing-object-1 image-object) (drawing-object-2 image-object)) - (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-1)) - (equal (image-object-width drawing-object-1) (image-object-width drawing-object-1)) - (equal (image-object-height drawing-object-1) (image-object-height drawing-object-1)))) + (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-2)) + (equal (image-object-width drawing-object-1) (image-object-width drawing-object-2)) + (equal (image-object-height drawing-object-1) (image-object-height drawing-object-2)) + ;; the cursor landing on the image changes its attribute and nothing else + (attribute-equal (image-object-attribute drawing-object-1) + (image-object-attribute drawing-object-2)) + ;; a differently cropped image draws differently, so the cached row must not be reused + (equal (image-object-visible-width drawing-object-1) + (image-object-visible-width drawing-object-2)))) (defgeneric drawing-object-mergable-p (drawing-object-1 drawing-object-2)) @@ -137,12 +233,6 @@ (equal (line-end-object-offset drawing-object-1) (line-end-object-offset drawing-object-2)))) -(defmethod drawing-object-mergable-p ((drawing-object-1 image-object) (drawing-object-2 image-object)) - (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-1)) - (equal (image-object-width drawing-object-1) (image-object-width drawing-object-1)) - (equal (image-object-height drawing-object-1) (image-object-height drawing-object-1)))) - - (defgeneric drawing-object-merge (drawing-object-1 drawing-object-2)) (defmethod drawing-object-merge ((drawing-object-1 void-object) (drawing-object-2 void-object)) @@ -190,6 +280,21 @@ (defun object-height (drawing-object) (lem-if:object-height (implementation) drawing-object)) +(defun object-ascent (drawing-object) + (lem-if:object-ascent (implementation) drawing-object)) + +(defgeneric object-columns (drawing-object) + (:documentation "How many columns of the line DRAWING-OBJECT accounts for. +Not its width in pixels (`object-width'): an image can account for one column and be hundreds of +pixels wide.") + (:method (drawing-object) 0) + (:method ((drawing-object text-object)) + (string-width (text-object-string drawing-object))) + ;; drawn past the end of the line, so it accounts for nothing on it. + (:method ((drawing-object line-end-object)) 0) + (:method ((drawing-object image-object)) + (image-object-columns drawing-object))) + (defun split-string-by-character-type (string) (loop :with pos := 0 :and items := '() :while (< pos (length string)) @@ -260,6 +365,8 @@ :true-cursor-p (eol-cursor-item-true-cursor-p item)))) ((typep item 'extend-to-eol-item) (list (make-instance 'extend-to-eol-object :color (extend-to-eol-item-color item)))) + ((typep item 'line-break-item) + (list (make-instance 'line-break-object))) ((typep item 'line-end-item) (let ((string (line-end-item-text item)) (attribute (line-end-item-attribute item))) @@ -279,7 +386,8 @@ :image (attribute-image attribute) :width (attribute-width attribute) :height (attribute-height attribute) - :attribute attribute))) + :attribute attribute + :columns (string-width string)))) (t (loop :for (type . string) :in (split-string-by-character-type string) :unless (alexandria:emptyp string) @@ -299,6 +407,11 @@ (char-type character))) (defun separate-objects-by-width (objects view-width buffer) + "Take one screen row's worth of OBJECTS, at most VIEW-WIDTH wide. +Returns (values ROW REST WHY): the row's objects, those left for the rows after it, and why the row +ended. :WRAPPED for running out of width, :LINE-BREAK for a newline inside virtual text, :END for +the end of the line. Only after :WRAPPED does the next row show more of the buffer's text, which is +what turning a row back into a buffer position needs to know." (flet ((explode-object (text-object) (check-type text-object text-object) (let* ((string (text-object-string text-object)) @@ -316,7 +429,20 @@ :and physical-line-objects := '() :for object := (pop objects) :while object - :do (cond ((and (typep object 'text-object) + :do (cond ((typep object 'line-break-object) + ;; a newline in virtual text, not a row that ran out of width, so no wrap + ;; marker and not :wrapped. + (return (values (nreverse physical-line-objects) objects :line-break))) + ((and (typep object 'image-object) + (< (- view-width total-width) (object-width object))) + ;; an image cannot be broken in half the way a text run is, so it moves whole + ;; to the next row. one that does not fit even a row of its own is cropped. + (if (null physical-line-objects) + (push (crop-image-object object (- view-width total-width)) + physical-line-objects) + (push object objects)) + (return (values (nreverse physical-line-objects) objects :wrapped))) + ((and (typep object 'text-object) (<= view-width (+ total-width (object-width object)))) (cond ((< 1 (length (text-object-string object))) (setf objects (nconc (explode-object object) objects))) @@ -325,14 +451,29 @@ (push (make-letter-object wrap-line-character wrap-line-attribute) physical-line-objects) - (return (values (nreverse physical-line-objects) objects))))) + (return (values (nreverse physical-line-objects) + objects + :wrapped))))) (t (incf total-width (object-width object)) (push object physical-line-objects))) - :finally (return (nreverse physical-line-objects)))))) - -(defun render-line (view x y objects height) - (lem-if:render-line (implementation) view x y objects height)) + :finally (return (values (nreverse physical-line-objects) nil :end)))))) + +(defun split-objects-at-line-breaks (objects) + "Split OBJECTS into one list per screen row, consuming each `line-break-object'. +Returns a list of lists, never empty: a line with no breaks in it gives one row." + (if (notany (lambda (object) (typep object 'line-break-object)) objects) + (list objects) + (let (rows row) + (dolist (object objects) + (if (typep object 'line-break-object) + (progn (push (nreverse row) rows) + (setf row nil)) + (push object row))) + (nreverse (cons (nreverse row) rows))))) + +(defun render-row (view row) + (lem-if:render-row (implementation) view row)) (defun reduce-list (list &key (test (alexandria:required-argument :test)) @@ -377,14 +518,18 @@ Assumes inputs are already reduced (no adjacent mergeable objects)." (drawing-objects-equal objects cache-objects)) :return t)) +(defun remove-drawing-cache-entries-overlapping (entries y height) + "Return ENTRIES with every entry whose rows overlap [Y, Y+HEIGHT) removed." + (remove-if (lambda (elt) + (destructuring-bind (cache-y cache-height drawing-objects) elt + (declare (ignore drawing-objects)) + (and (< cache-y (+ y height)) + (< y (+ cache-y cache-height))))) + entries)) + (defun invalidate-cache (window y height) (setf (drawing-cache window) - (remove-if (lambda (elt) - (destructuring-bind (cache-y cache-height drawing-objects) elt - (declare (ignore drawing-objects)) - (and (<= cache-y y) - (<= (+ y height) (+ cache-y cache-height))))) - (drawing-cache window)))) + (remove-drawing-cache-entries-overlapping (drawing-cache window) y height))) (defun remove-drawing-cache-entries-from (entries y) "Return ENTRIES with drawing-cache rows at or below Y removed. @@ -406,23 +551,129 @@ leaving the row blank on persistent-texture frontends such as SDL2." (remove-drawing-cache-entries-from (drawing-cache window) y))) (defun update-and-validate-cache-p (window y height objects) - "Check cache validity, reducing objects once before storing. + "Check cache validity for the already-reduced OBJECTS, storing them when they differ. Returns T if the cached entry matches (render can be skipped)." - (let ((reduced (reduce-objects objects))) - (cond ((validate-cache-p window y height reduced) t) - (t - (invalidate-cache window y height) - (push (list y height reduced) - (drawing-cache window)) - nil)))) - -(defun render-line-with-caching (window x y objects height) - (unless (update-and-validate-cache-p window y height objects) - (render-line (window-view window) x y objects height))) - -(defun max-height-of-objects (objects) - (loop :for object :in objects - :maximize (object-height object))) + (cond ((validate-cache-p window y height objects) t) + (t + (invalidate-cache window y height) + (push (list y height objects) + (drawing-cache window)) + nil))) + +(defun render-row-with-caching (window y objects) + "Lay OBJECTS out as one screen row of WINDOW at Y and draw it, unless it is already on screen. +Returns the laid-out row." + (let* ((reduced (reduce-objects objects)) + (row (layout-row y reduced))) + (unless (update-and-validate-cache-p window y (row-height row) reduced) + (render-row (window-view window) row)) + row)) + +(defun text-row-metrics () + "The ascent and height of a row holding nothing but text, as (values ASCENT HEIGHT). +A frontend that does not report a baseline is taken to put it at the bottom of the row." + (multiple-value-bind (cell-width cell-height cell-ascent) + (lem-if:cell-pixel-size (implementation)) + (declare (ignore cell-width)) + (let ((height (or cell-height (lem-if:cell-height (implementation))))) + (values (or cell-ascent height) height)))) + +(defun row-metrics-of-objects (&rest object-lists) + "The ascent and height a row of all the objects in OBJECT-LISTS needs, as (values ASCENT HEIGHT). +Everything shares one baseline, so the height is max ascent plus max descent, which can exceed any +single object's own height: an object with a tall ascent and short descent and one with a short +ascent and tall descent can each set one half of the row independently, so the row ends up taller +than either. An empty row is still one row of text tall. +The returned ASCENT is also the baseline's offset from the row's top, since the baseline sits +exactly ASCENT below it. `layout-row' uses it that way to hang everything on the row from it." + (multiple-value-bind (ascent height) (text-row-metrics) + (let ((descent (- height ascent))) + (dolist (objects object-lists) + (dolist (object objects) + (let ((object-ascent (object-ascent object))) + (setf ascent (max ascent object-ascent)) + (setf descent (max descent (- (object-height object) object-ascent)))))) + (values ascent (+ ascent descent))))) + +(defstruct (placement (:constructor make-placement (object x top))) + "Where one drawing object goes, top-left corner at (X, TOP), in the frontend's units. +TOP is the row's baseline minus this object's ascent, so objects of different heights hang from one +baseline instead of sharing a top edge." + object + x + top) + +(defstruct row + "One screen row, laid out by `layout-row' and ready for a frontend to draw. +TOP/HEIGHT already account for every object on the row, including one taller than a line of text. +A frontend should size the row from these fields rather than re-deriving its extent from any +single object's own height." + top + height + ;; needed by a frontend that draws a text-object letter by letter, to put each letter on it. + baseline + ;; where each object goes, its own x and top. + placements + ;; from an `extend-to-eol-object', if the row holds one. A frontend paints FILL-COLOR first, + ;; before any of the row's objects, over the rectangle from FILL-X to the right edge and down + ;; the row's full height. NIL FILL-COLOR means nothing to paint. + fill-x + fill-color) + +(defun layout-row (top objects + &key + right-objects + (right-edge (and right-objects + (alexandria:required-argument :right-edge)))) + "Lay OBJECTS out as one screen row with its top edge at TOP, as a `row'. +RIGHT-OBJECTS are laid out leftwards from RIGHT-EDGE instead, for a row drawn from both ends (the +modeline), so the left- and right-aligned objects share one baseline. Everything, including an +image, is positioned by its own ascent measured from that one shared baseline (see +`image-object-ascent'), so it stays correctly placed relative to the text beside it however tall +the row is. +An `extend-to-eol-object' is not placed. It draws nothing of its own and colors the row's full +height, so it becomes ROW-FILL-X and ROW-FILL-COLOR." + (multiple-value-bind (ascent height) (row-metrics-of-objects objects right-objects) + (let ((baseline (+ top ascent)) + (placements) + (fill-x) + (fill-color)) + (flet ((place (object x) + (if (typep object 'extend-to-eol-object) + ;; only the first can show, it colors everything from its x rightwards. + (unless fill-color + (setf fill-x x + fill-color (extend-to-eol-object-color object))) + (push (make-placement object x (- baseline (object-ascent object))) + placements)))) + (loop :with x := 0 + :for object :in objects + :do (place object x) + (incf x (object-width object))) + (loop :with x := right-edge + :for object :in right-objects + :do (decf x (object-width object)) + (place object x))) + (make-row :top top + :height height + :baseline baseline + :placements (nreverse placements) + :fill-x fill-x + :fill-color fill-color)))) + +(defun translate-row (row dy) + "A copy of ROW moved DY down the view. +For a frontend that draws a row elsewhere than where it was laid out, a modeline drawn into the +bottom of the window's view rather than onto a surface of its own." + (let ((moved (copy-row row))) + (setf (row-top moved) (+ (row-top row) dy) + (row-baseline moved) (+ (row-baseline row) dy) + (row-placements moved) + (loop :for placement :in (row-placements row) + :collect (make-placement (placement-object placement) + (placement-x placement) + (+ (placement-top placement) dy)))) + moved)) ;;; Line fingerprint cache — avoids creating drawing objects for unchanged lines @@ -540,27 +791,107 @@ over the top-level spine and tolerant of improper (dotted) lists." scroll-start left-side-width)) +(defstruct screen-row + "One drawn row of a window, recorded as it was drawn." + ;; which buffer line this row's logical line starts on. + line-number + ;; this row's index within its line. a break in virtual text starts a row without advancing it. + wrap-index + ;; how much of the row's left edge the left area took + left-width + ;; as `layout-row' laid it out and the frontend drew it, so a pixel position can be read back + ;; against what is on screen rather than derived a second time. + row) + +(defun screen-row-height (screen-row) + (row-height (screen-row-row screen-row))) + +(defun window-screen-rows (window) + "Every screen row of WINDOW, top to bottom, as recorded while it was drawn." + (window-parameter window 'screen-rows)) + +(defun (setf window-screen-rows) (rows window) + (setf (window-parameter window 'screen-rows) rows)) + +(defun window-screen-row-index-at-y (window y) + "Index of the screen row Y falls in, counted from the top of WINDOW's view, or NIL when Y is past +the last row drawn or the window has not been drawn yet. Y is in the frontend's units. +Walks the rows because they are not all one height, so there is nothing to divide by." + (loop :with top := 0 + :for row :in (window-screen-rows window) + :for index :from 0 + :do (when (< y (+ top (screen-row-height row))) + (return index)) + (incf top (screen-row-height row)))) + +(defun window-screen-row-at-index (window index) + "WINDOW's screen row at INDEX, counted from the top of its view, or NIL if no row was drawn +there." + (nth index (window-screen-rows window))) + +(defun screen-row-column-at-x (screen-row x) + "The column of SCREEN-ROW's line that pixel X, measured from the window's left edge, is over. +Walks the objects drawn rather than dividing by a cell width, which would miscount every row holding +something not one cell wide." + (let ((column 0) + (right (screen-row-left-width screen-row))) + (dolist (placement (row-placements (screen-row-row screen-row))) + (let ((object (placement-object placement)) + (left (placement-x placement))) + ;; skip the left area: line numbers and the like, which account for no column + (when (<= (screen-row-left-width screen-row) left) + (let ((width (object-width object))) + (when (and (plusp width) (< x (+ left width))) + (return-from screen-row-column-at-x + (+ column (floor (* (object-columns object) (- x left)) width)))) + (incf column (object-columns object)) + (setf right (max right (+ left width))))))) + ;; no object under x, so it is out past the line where the window is plain cells. callers clamp + ;; this to the line's end. + (+ column (floor (max 0 (- x right)) (lem-if:cell-width (implementation)))))) + (defun check-line-fingerprint (window y fingerprint) - "Check if the fingerprint for line at Y matches. Returns cached height or NIL." + "Check if the fingerprint for line at Y matches. Returns the cached list of rows, or NIL. +One entry per row, so a line taken from the cache still contributes its rows to +`window-screen-rows'." (let ((cache (line-fingerprint-cache window))) (multiple-value-bind (entry found) (gethash y cache) (when (and found (eql (car entry) fingerprint)) (cdr entry))))) -(defun update-line-fingerprint (window y fingerprint height) - "Store the fingerprint and height for line at Y." - (setf (gethash y (line-fingerprint-cache window)) - (cons fingerprint height))) +(defun evict-line-fingerprint-shadow (cache y height) + "Remove entries in CACHE for the rows a HEIGHT-tall line at Y covers. +Loops over the cache's keys, not over every Y in the range: on a pixel frontend that range is one +iteration per pixel, against a cache holding one entry per line drawn." + (let ((end (+ y height)) + (stale)) + (loop :for row :being :the :hash-keys :of cache + :when (and (< y row) (< row end)) + :do (push row stale)) + (dolist (row stale) + (remhash row cache)))) + +(defun update-line-fingerprint (window y fingerprint rows) + "Store the fingerprint and ROWS for line at Y, and drop the rows it covers. +ROWS is one `screen-row' per screen row the line drew, as the redraw functions collect them." + (let ((cache (line-fingerprint-cache window))) + (setf (gethash y cache) (cons fingerprint rows)) + (evict-line-fingerprint-shadow cache + y + (reduce #'+ rows :key #'screen-row-height :initial-value 0)))) + +(defun left-side-character-count (left-side-objects) + (loop :for obj :in left-side-objects + :when (typep obj 'text-object) + :sum (length (text-object-string obj)))) (defun redraw-logical-line-when-line-wrapping (window y logical-line left-side-objects left-side-width) - (let* ((left-side-characters (loop :for obj :in left-side-objects - :when (typep obj 'text-object) - :sum (length (text-object-string obj))))) - (multiple-value-bind (first-line-objects rest-line-objects) + (let* ((left-side-characters (left-side-character-count left-side-objects))) + (multiple-value-bind (first-line-objects rest-line-objects why) (separate-objects-by-width (create-drawing-objects logical-line) (- (window-view-width window) left-side-width) (window-buffer window)) @@ -570,23 +901,31 @@ over the top-level spine and tolerant of improper (dotted) lists." *active-modes* left-side-width left-side-characters))))) - (let ((total-height 0) + (let ((rows) + (wrap-index 0) (objects first-line-objects)) (loop - (unless objects (return)) - (let* ((all-objects (append left-side-objects objects)) - (height (max-height-of-objects all-objects))) - (render-line-with-caching window 0 y all-objects height) - (incf y height) + ;; an empty row is still a row when more of the line follows, which is what a break at + ;; the very start of the virtual text asks for. + (unless (or objects rest-line-objects) (return)) + (let ((row (render-row-with-caching window y (append left-side-objects objects)))) + (incf y (row-height row)) (setq left-side-objects wrapped-left-side-objects) - (incf total-height height) - (unless (< y (window-height window)) + (push (make-screen-row :row row + :wrap-index wrap-index + :left-width left-side-width) + rows) + ;; only running out of width advances the position, a virtual-text break does not. + (when (eq why :wrapped) + (incf wrap-index)) + ;; y is in the frontend's units, so the bound must be too, not the row count. + (unless (< y (window-view-height window)) (return))) - (setf (values objects rest-line-objects) + (setf (values objects rest-line-objects why) (separate-objects-by-width rest-line-objects (- (window-view-width window) left-side-width) (window-buffer window)))) - total-height))))) + (nreverse rows)))))) (defun find-cursor-object (objects) (loop :for object :in objects @@ -652,6 +991,10 @@ creating zero temporary letter-objects." (text-object-attribute object) (text-object-type object)) result)))) + ;; an image crossing the right edge is cut down to what fits. the left edge is not, since + ;; that needs an offset into the image and an image-object carries only a visible width. + ((and (typep object 'image-object) (< x end-x) (< end-x obj-end)) + (push (crop-image-object object (- end-x x)) result)) ;; Non-text objects straddling boundary - include (t (push object result))) (incf x w))) @@ -667,32 +1010,49 @@ creating zero temporary letter-objects." scroll-before left-side-width))) ;; Early exit if line content unchanged - (alexandria:when-let ((cached-height (check-line-fingerprint window y fingerprint))) - (return-from redraw-logical-line-when-horizontal-scroll cached-height)) - (let* ((objects (create-drawing-objects logical-line)) - (height - (max (max-height-of-objects left-side-objects) - (max-height-of-objects objects)))) - (multiple-value-bind (cursor-object cursor-x) - (find-cursor-object objects) - (when cursor-object - (let ((width (- (window-view-width window) left-side-width))) - (cond ((< cursor-x (horizontal-scroll-start window)) - (setf (horizontal-scroll-start window) cursor-x)) - ((< (+ (horizontal-scroll-start window) - width) - (+ cursor-x (object-width cursor-object))) - (setf (horizontal-scroll-start window) - (+ (- cursor-x width) - (object-width cursor-object))))))) - (setf objects - (reduce-objects - (clip-objects-to-display-range - objects - (horizontal-scroll-start window) - (+ (horizontal-scroll-start window) - (window-view-width window))))) - (render-line-with-caching window 0 y (append left-side-objects objects) height)) + (alexandria:when-let ((cached-rows (check-line-fingerprint window y fingerprint))) + (return-from redraw-logical-line-when-horizontal-scroll cached-rows)) + (let* ((rows (split-objects-at-line-breaks (create-drawing-objects logical-line))) + (left-side-characters (left-side-character-count left-side-objects)) + (screen-rows) + (total-height 0)) + ;; the cursor is on one of the rows, scrolling follows it there. + (dolist (row-objects rows) + (multiple-value-bind (cursor-object cursor-x) + (find-cursor-object row-objects) + (when cursor-object + (let ((width (- (window-view-width window) left-side-width))) + (cond ((< cursor-x (horizontal-scroll-start window)) + (setf (horizontal-scroll-start window) cursor-x)) + ((< (+ (horizontal-scroll-start window) + width) + (+ cursor-x (object-width cursor-object))) + (setf (horizontal-scroll-start window) + (+ (- cursor-x width) + (object-width cursor-object))))))))) + (let ((wrapped-left-side-objects + (when (rest rows) + (copy-list (compute-wrap-left-area-content *active-modes* + left-side-width + left-side-characters))))) + (loop :for row-objects :in rows + ;; only the first row carries the real left area, the rest get the wrap padding. + :for side := left-side-objects :then wrapped-left-side-objects + :do (let* ((clipped (clip-objects-to-display-range + row-objects + (horizontal-scroll-start window) + (+ (horizontal-scroll-start window) + (window-view-width window)))) + (row (render-row-with-caching window (+ y total-height) + (append side clipped)))) + (incf total-height (row-height row)) + ;; wrapping is off here, so every row begins where the line does, index 0 + (push (make-screen-row :row row :wrap-index 0 :left-width left-side-width) + screen-rows)) + ;; y is in the frontend's units, as is the bound + (when (<= (window-view-height window) (+ y total-height)) + (return)))) + (setf screen-rows (nreverse screen-rows)) ;; Reuse fingerprint if scroll position didn't change; avoids redundant sxhash (update-line-fingerprint window y @@ -701,8 +1061,8 @@ creating zero temporary letter-objects." (compute-line-fingerprint logical-line (horizontal-scroll-start window) left-side-width)) - height) - height))) + screen-rows) + screen-rows))) (defun redraw-lines (window) (let* ((*line-wrap* (variable-value 'line-wrap @@ -712,9 +1072,11 @@ creating zero temporary letter-objects." #'redraw-logical-line-when-horizontal-scroll))) (let ((y 0) (height (window-view-height window)) + ;; every row drawn, in reverse. see `window-screen-rows' + (rows) left-side-width) (block outer - (do-logical-line (logical-line window) + (do-logical-line (logical-line window line-point) (let* ((left-side-objects (alexandria:when-let (content (logical-line-left-content logical-line)) (mapcan #'create-drawing-object @@ -724,15 +1086,23 @@ creating zero temporary letter-objects." (setf left-side-width (loop :for object :in left-side-objects :sum (object-width object))) - (incf y (funcall redraw-fn window y logical-line left-side-objects left-side-width)) + (let ((line-rows + (funcall redraw-fn window y logical-line left-side-objects left-side-width)) + ;; read once, shared by the line's rows + (line-number (line-number-at-point line-point))) + (loop :for row :in line-rows + :do (setf (screen-row-line-number row) line-number) + (push row rows) + (incf y (screen-row-height row)))) (unless (< y height) (return-from outer))))) + (setf (window-screen-rows window) (nreverse rows)) (when (< y height) (clear-line-fingerprint-cache-from window y) (invalidate-drawing-cache-from window y) (lem-if:clear-to-end-of-window (implementation) (window-view window) y)) (setf (window-left-width window) - (floor left-side-width (lem-if:get-char-width (implementation))))))) + (floor left-side-width (lem-if:cell-width (implementation))))))) (defun call-with-display-error (function) (handler-bind ((error (lambda (e) @@ -779,13 +1149,16 @@ creating zero temporary letter-objects." 'modeline-inactive)))) (multiple-value-bind (left-objects right-objects) (make-modeline-objects window default-attribute) - (lem-if:render-line-on-modeline (implementation) - view - left-objects - right-objects - default-attribute - (max (max-height-of-objects left-objects) - (max-height-of-objects right-objects))))))) + ;; top 0: only the frontend knows where the modeline actually goes on screen. see + ;; `lem-if:render-modeline-row'. + (lem-if:render-modeline-row (implementation) + view + (layout-row 0 + left-objects + :right-objects right-objects + :right-edge (lem-if:view-width (implementation) + view)) + default-attribute))))) (defun get-background-color-of-window (window) (cond ((typep window 'floating-window) diff --git a/src/interface.lisp b/src/interface.lisp index ef762a86a..8a7f5d7bf 100644 --- a/src/interface.lisp +++ b/src/interface.lisp @@ -44,7 +44,12 @@ When rendering the DOM and a window in a one-to-one manner, no redraw is require :initform nil :initarg :support-pixel-positioning :reader support-pixel-positioning-p - :documentation "When true, the frontend supports pixel-based floating window positioning."))) + :documentation "When true, the frontend supports pixel-based floating window positioning.") + (image-support + :initform nil + :initarg :image-support + :reader image-support-p + :documentation "When true, the frontend can draw an image object."))) (defun get-default-implementation (&key implementation) (let ((classes (c2mop:class-direct-subclasses (find-class 'implementation))) @@ -175,14 +180,68 @@ PIXEL-X, PIXEL-Y, PIXEL-WIDTH, PIXEL-HEIGHT are in pixels (may be nil for auto-c (:method (implementation) (values -1 -1))) -(defgeneric lem-if:get-char-width (implementation)) -(defgeneric lem-if:get-char-height (implementation)) +(defgeneric lem-if:cell-width (implementation) + (:documentation "Width of one character cell in the frontend's native layout units. +1 on a cell-based frontend (a terminal counts in cells), pixels on a pixel-based one. These are +the units `object-width' / `object-height' are counted in.")) + +(defgeneric lem-if:cell-height (implementation) + (:documentation "Height of one character cell in the frontend's native layout units. +Unit-relative like `cell-width'.")) -(defgeneric lem-if:render-line (implementation view x y objects height)) -(defgeneric lem-if:render-line-on-modeline (implementation view left-objects right-objects - default-attribute height)) -(defgeneric lem-if:object-width (implementation drawing-object)) -(defgeneric lem-if:object-height (implementation drawing-object)) +(defgeneric lem-if:cell-pixel-size (implementation) + (:documentation "One character cell in real pixels, as (values WIDTH HEIGHT ASCENT). +ASCENT is how far below the cell's top the text baseline sits, and may be NIL on its own. +All three are NIL on a frontend that does not draw in pixels. +Always pixels, unlike `cell-width' / `cell-height', which are 1 on a cell-based frontend.") + (:method (implementation) + (values nil nil nil))) + +(defgeneric lem-if:font-em-pixels (implementation) + (:documentation "Pixels one em of the editor font takes, or NIL when the frontend cannot say. +In the same units as `cell-pixel-size'. +The em is smaller than `cell-height', which is measured from the glyph bounding box.") + (:method (implementation) nil)) + +(defgeneric lem-if:render-row (implementation view row) + (:documentation "Draw ROW, one screen row of VIEW, replacing whatever it held before. +ROW is a `lem-core/display:row'. Its height, background and the position of every object on it were +decided by `lem-core/display:layout-row', so a frontend only paints. +Blank the row's full width, `row-top' down by ROW-HEIGHT, first.")) + +(defgeneric lem-if:render-modeline-row (implementation view row default-attribute) + (:documentation "Draw ROW as VIEW's modeline, filled with DEFAULT-ATTRIBUTE's background. +Like `render-row', except ROW was laid out with its top at Y 0, since only the frontend knows +where on screen its modeline goes. One that draws it into the view moves it with +`lem-core/display:translate-row'.")) + +(defgeneric lem-if:object-width (implementation drawing-object) + (:documentation "Width DRAWING-OBJECT occupies, in the same units as `cell-width'. +Defaults in src/display/physical-line.lisp: a text-object is `string-width' cells, counting a wide +glyph as two, an image its `lem-core/display:image-draw-width'. +Specialize this only for an object the frontend draws at some other size, as sdl2 does for its +folder and emoji glyphs.")) + +(defgeneric lem-if:object-height (implementation drawing-object) + (:documentation "Height DRAWING-OBJECT occupies, in the same units as `cell-height'. +Defaults to one cell for every object but an image, which takes the pixel height it is drawn at +(`lem-core/display:image-draw-height'). Specialize it as in `object-width'.")) + +(defgeneric lem-if:object-ascent (implementation drawing-object) + (:documentation "How much of DRAWING-OBJECT sits above the text baseline, in the same units as +`cell-height'. +Everything on a row shares one baseline, so a row is as tall as the furthest anything reaches above +it plus the furthest anything reaches below, which can exceed the tallest single object. +Defaults to the cell ascent a frontend reports through `cell-pixel-size', or the object's bottom +when it reports none, so one that does not know its baseline keeps its old layout exactly.")) + +(defgeneric lem-if:image-natural-size (implementation image) + (:documentation "Fallback size for an image whose object requests no particular size. +Returns (values WIDTH HEIGHT) in pixels, or NIL NIL if the frontend can't tell. IMAGE is the +frontend's own loaded-image handle (an SDL surface, a path handed to a browser, ...), not the +`lem-core/display:image-object' holding it.") + (:method (implementation image) + (values nil nil))) (defgeneric lem-if:clear-to-end-of-window (implementation view y)) (defgeneric lem-if:js-eval (implementation view code &key wait) diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index 4a98e4298..32cbad0db 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -14,9 +14,31 @@ :folder-object :icon-object :image-object + :image-object-attribute :image-object-height :image-object-image :image-object-width + :image-object-visible-width + :image-object-ascent + :image-draw-width + :image-draw-height + :object-ascent + :object-height + :object-width + :row-metrics-of-objects + :layout-row + :translate-row + :row + :row-top + :row-height + :row-baseline + :row-placements + :row-fill-x + :row-fill-color + :placement + :placement-object + :placement-x + :placement-top :line-end-object :line-end-object-offset :text-object @@ -680,6 +702,7 @@ :support-pixel-positioning-p :html-support-p :underline-color-support-p + :image-support-p :no-force-needed-p :set-foreground :set-background @@ -819,12 +842,16 @@ :get-font-by-name-and-style :get-font :get-mouse-position - :get-char-width - :get-char-height + :cell-width + :cell-height + :cell-pixel-size + :font-em-pixels :clear-to-end-of-window :js-eval - :render-line - :render-line-on-modeline + :render-row + :render-modeline-row :object-width :object-height + :object-ascent + :image-natural-size :set-frame-color)) diff --git a/src/mouse.lisp b/src/mouse.lisp index c08f9e105..84acbfb54 100644 --- a/src/mouse.lisp +++ b/src/mouse.lisp @@ -64,23 +64,62 @@ (y (mouse-event-pixel-y mouse-event))) (values (- x (* (window-x window) - (lem-if:get-char-width (implementation)))) + (lem-if:cell-width (implementation)))) (- y (* (window-y window) - (lem-if:get-char-height (implementation))))))) + (lem-if:cell-height (implementation))))))) + +(defun mouse-event-screen-row (mouse-event window fallback-row) + "The screen row of WINDOW that MOUSE-EVENT points at, counted from the top of its view. +Walks the heights recorded when the window was drawn, since rows are not all one height. +FALLBACK-ROW, what dividing by a row height gives, is used when there is no pixel position to walk +with or the window is undrawn." + (if (and (mouse-event-pixel-x mouse-event) + (mouse-event-pixel-y mouse-event)) + (multiple-value-bind (relative-x relative-y) + (get-relative-mouse-coordinates-pixels mouse-event window) + (declare (ignore relative-x)) + (or (window-screen-row-index-at-y window relative-y) + fallback-row)) + fallback-row)) + +(defun mouse-event-screen-column (mouse-event window row-index fallback-column) + "The column that MOUSE-EVENT points at on ROW-INDEX of WINDOW, counted from the top of its view. +FALLBACK-COLUMN is used when there is no pixel position to walk with or no such row was drawn." + (alexandria:if-let ((screen-row (and (mouse-event-pixel-x mouse-event) + (mouse-event-pixel-y mouse-event) + (window-screen-row-at-index window row-index)))) + (multiple-value-bind (relative-x relative-y) + (get-relative-mouse-coordinates-pixels mouse-event window) + (declare (ignore relative-y)) + ;; measured from the window's left edge, where the row was laid out from + (screen-row-column-at-x screen-row relative-x)) + fallback-column)) + +(defun move-point-to-screen-row (point window row) + "Move POINT to the start of screen ROW of WINDOW, counting rows from the top of its view. +Uses the line recorded when the row was drawn, since counting virtual lines down from the view top +would miscount every row a newline inside virtual text added. Falls back to that count for a row +that was not drawn, or whose line the buffer no longer has." + (flet ((count-from-view-top () + (move-point point (window-view-point window)) + (move-to-next-virtual-line point row window))) + (alexandria:if-let ((screen-row (window-screen-row-at-index window row))) + (if (move-to-line point (screen-row-line-number screen-row)) + (move-to-next-virtual-line point (screen-row-wrap-index screen-row) window) + (count-from-view-top)) + (count-from-view-top)))) (defun get-point-from-window-with-coordinates (window x y &optional (allow-overflow-column t)) (with-point ((point (buffer-point (window-buffer window)))) - (move-point point (window-view-point window)) - (move-to-next-virtual-line point y window) + (move-point-to-screen-row point window y) (let ((moved (move-to-virtual-line-column point x window))) (when (or moved allow-overflow-column) point)))) (defun move-current-point-to-x-y-position (window x y) (switch-to-window window) - (move-point (current-point) (window-view-point window)) - (move-to-next-virtual-line (current-point) y) + (move-point-to-screen-row (current-point) window y) (move-to-virtual-line-column (current-point) x)) (defvar *last-mouse-event*) @@ -190,11 +229,12 @@ (mouse-event-y mouse-event)) (when (and window (window-clickable window)) - (handle-mouse-button-down (window-buffer window) - mouse-event - :window window - :x x - :y y))))))) + (let ((row-index (mouse-event-screen-row mouse-event window y))) + (handle-mouse-button-down (window-buffer window) + mouse-event + :window window + :x (mouse-event-screen-column mouse-event window row-index x) + :y row-index)))))))) (defmethod handle-mouse-event ((mouse-event mouse-button-up)) (setf *last-dragged-separator* nil) @@ -256,11 +296,12 @@ (mouse-event-x mouse-event) (mouse-event-y mouse-event)) (when window - (handle-mouse-hover (window-buffer window) - mouse-event - :window window - :x x - :y y)))) + (let ((row-index (mouse-event-screen-row mouse-event window y))) + (handle-mouse-hover (window-buffer window) + mouse-event + :window window + :x (mouse-event-screen-column mouse-event window row-index x) + :y row-index))))) ((typep *last-dragged-separator* 'window-vertical-separator) (let ((x (mouse-event-x mouse-event)) (button (mouse-event-button mouse-event))) diff --git a/src/window/floating-window.lisp b/src/window/floating-window.lisp index d37516a1d..ca93109d5 100644 --- a/src/window/floating-window.lisp +++ b/src/window/floating-window.lisp @@ -142,8 +142,8 @@ This updates the window's pixel dimensions and notifies the frontend." Returns (values pixel-x pixel-y pixel-width pixel-height). If pixel coordinates are not set, calculates from character coordinates." (check-type window floating-window) - (let ((char-width (lem-if:get-char-width (implementation))) - (char-height (lem-if:get-char-height (implementation)))) + (let ((char-width (lem-if:cell-width (implementation))) + (char-height (lem-if:cell-height (implementation)))) (values (or (floating-window-pixel-x window) (* (window-x window) char-width)) (or (floating-window-pixel-y window)