echoes 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a8be3caa5aa66d44068fc6f0f3d9b9e0e8ada790feb546a8b93e1fa11285c8e
4
- data.tar.gz: 88c4d5b99a0c214a3825f5f7cdaeca2a9c94be873e306ebea307d7dca0e959ce
3
+ metadata.gz: 6ef610bfece0d2ba0f400475abf738bc9fa373ae3f64574652c6a090a9c63425
4
+ data.tar.gz: 24c08669c3f540953c06f60d2e39f91351e359866ebf4bded0012faddd637095
5
5
  SHA512:
6
- metadata.gz: a3556fc2682146b14af577037e25a922013d953acfcd07ccafd9f4fb4731ad1d0b1c822e4eb1cb6bccb0181565f087c21368f96ee97c86bb1bb4845b3b59c1cb
7
- data.tar.gz: a39762c03920861a60deef354b7322a58016f37b28b5e97643a206494c53fc29cf91d8e8b4a5ec68b5651b241a4e0cf6500048c633bd33d54430e9426a99be0c
6
+ metadata.gz: 5ed9ff19f3c30e359a5595544dc849a3247d4c947a2b3422e7139bc28ce7eb05cc8ff21e45b9d10aa87dc3ba94472a295804e7fa31297a141a906c6b47a4ab54
7
+ data.tar.gz: a8a967dce6e4b25108a58660f213d5697a7a440d6a06c32ff542444e9d8b8547783919cdb98b656d2fa2a32380ab61946b6bc991ead382adea022644ad7835cc
data/lib/echoes/gui.rb CHANGED
@@ -785,10 +785,45 @@ module Echoes
785
785
  [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP]
786
786
  ) { |_self, _cmd, _loc, _len, _actual| Fiddle::Pointer.new(0) }
787
787
 
788
- @first_rect_closure = Fiddle::Closure::BlockCaller.new(
789
- Fiddle::TYPE_DOUBLE,
790
- [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP]
791
- ) { |_self, _cmd, _loc, _len, _actual| 0.0 }
788
+ # firstRectForCharacterRange:actualRange: returns NSRect (4
789
+ # doubles). Fiddle::Closure has no way to return a struct, so
790
+ # we can't implement it directly — a TYPE_DOUBLE closure only
791
+ # writes one of the four NSRect fields to v0 and leaves the
792
+ # rest as register garbage, which makes the IME candidate
793
+ # window appear off-screen or get suppressed entirely.
794
+ #
795
+ # Forward it through methodSignatureForSelector: + forward-
796
+ # Invocation: instead — NSInvocation.setReturnValue: handles
797
+ # the struct ABI for us. We just write 4 doubles into a buffer
798
+ # and hand the pointer to AppKit. The wiring lives in
799
+ # @method_sig_closure / @forward_inv_closure below.
800
+ @method_sig_closure = Fiddle::Closure::BlockCaller.new(
801
+ Fiddle::TYPE_VOIDP,
802
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
803
+ ) do |_self, _cmd, sel|
804
+ gui.method_signature_for_selector(_self, sel)
805
+ rescue => e
806
+ gui.log_crash(e, context: 'methodSignatureForSelector')
807
+ Fiddle::Pointer.new(0)
808
+ end
809
+
810
+ @forward_inv_closure = Fiddle::Closure::BlockCaller.new(
811
+ Fiddle::TYPE_VOID,
812
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
813
+ ) do |_self, _cmd, inv|
814
+ gui.forward_invocation(_self, inv)
815
+ rescue => e
816
+ gui.log_crash(e, context: 'forwardInvocation')
817
+ end
818
+
819
+ # respondsToSelector: needs to claim YES for the forwarded
820
+ # selector — AppKit's IME code checks before calling, and
821
+ # the runtime's default class_respondsToSelector returns NO
822
+ # for selectors we don't implement directly (only forward).
823
+ @responds_to_sel_closure = Fiddle::Closure::BlockCaller.new(
824
+ Fiddle::TYPE_CHAR,
825
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
826
+ ) { |_self, _cmd, sel| gui.responds_to_selector?(_self, sel) }
792
827
 
793
828
  @char_index_closure = Fiddle::Closure::BlockCaller.new(
794
829
  Fiddle::TYPE_LONG,
@@ -1067,7 +1102,14 @@ module Echoes
1067
1102
  'selectedRange' => ['{_NSRange=QQ}@:', @selected_range_closure],
1068
1103
  'validAttributesForMarkedText' => ['@@:', @valid_attrs_closure],
1069
1104
  'attributedSubstringForProposedRange:actualRange:' => ['@@:{_NSRange=QQ}^{_NSRange=QQ}', @attr_substring_closure],
1070
- 'firstRectForCharacterRange:actualRange:' => ['{CGRect={CGPoint=dd}{CGSize=dd}}@:{_NSRange=QQ}^{_NSRange=QQ}', @first_rect_closure],
1105
+ # firstRectForCharacterRange:actualRange: is intentionally
1106
+ # not in the methods dict — it returns NSRect (4 doubles)
1107
+ # which Fiddle::Closure can't model. The runtime falls
1108
+ # through to the methodSignatureForSelector / forwardInvocation
1109
+ # pair below, which writes the rect via NSInvocation.
1110
+ 'methodSignatureForSelector:' => ['@@::', @method_sig_closure],
1111
+ 'forwardInvocation:' => ['v@:@', @forward_inv_closure],
1112
+ 'respondsToSelector:' => ['c@::', @responds_to_sel_closure],
1071
1113
  'characterIndexForPoint:' => ['Q@:{CGPoint=dd}', @char_index_closure],
1072
1114
  'draggingEntered:' => ['Q@:@', @dragging_entered_closure],
1073
1115
  'draggingUpdated:' => ['Q@:@', @dragging_updated_closure],
@@ -1806,6 +1848,141 @@ module Echoes
1806
1848
  @marked_text ? 0 : 0x7FFFFFFFFFFFFFFF # NSNotFound
1807
1849
  end
1808
1850
 
1851
+ # --- IME candidate-window positioning via message forwarding ---
1852
+ #
1853
+ # firstRectForCharacterRange:actualRange: returns NSRect (4 doubles).
1854
+ # Fiddle::Closure can't model a struct return — only single-register
1855
+ # scalar returns. Without a real implementation, AppKit reads the
1856
+ # rect from v0..v3 with only v0 set to our scalar return value, so
1857
+ # the IME sees a garbage rect and either positions the candidate
1858
+ # window off-screen or suppresses it entirely.
1859
+ #
1860
+ # The workaround: don't implement the selector directly; let it
1861
+ # fall through to forwardInvocation:. Inside forward_invocation we
1862
+ # pack four doubles into an NSRect-sized buffer and hand it to
1863
+ # NSInvocation.setReturnValue:, which handles the struct ABI for
1864
+ # us correctly. methodSignatureForSelector: is the gateway that
1865
+ # tells the runtime to use the forwarding path.
1866
+ IME_FORWARDED_SELECTORS = ['firstRectForCharacterRange:actualRange:'].freeze
1867
+ FIRST_RECT_SIG = '{CGRect={CGPoint=dd}{CGSize=dd}}@:{_NSRange=QQ}^{_NSRange=QQ}'.freeze
1868
+
1869
+ def method_signature_for_selector(self_ptr, sel)
1870
+ name = sel_name_string(sel)
1871
+ if name == 'firstRectForCharacterRange:actualRange:'
1872
+ return ObjC::MSG_PTR_1.call(
1873
+ ObjC.cls('NSMethodSignature'),
1874
+ ObjC.sel('signatureWithObjCTypes:'),
1875
+ Fiddle::Pointer[FIRST_RECT_SIG.b])
1876
+ end
1877
+ # Default: defer to whatever the runtime would have computed
1878
+ # from the class's method table. class_getInstanceMethod walks
1879
+ # the hierarchy so directly-implemented + inherited methods
1880
+ # all resolve.
1881
+ self_class = ObjC::MSG_PTR.call(self_ptr, ObjC.sel('class'))
1882
+ method = ObjC::ClassGetInstanceMethod.call(self_class, sel)
1883
+ return Fiddle::Pointer.new(0) if method.null?
1884
+ enc = ObjC::MethodGetTypeEncoding.call(method)
1885
+ ObjC::MSG_PTR_1.call(
1886
+ ObjC.cls('NSMethodSignature'),
1887
+ ObjC.sel('signatureWithObjCTypes:'),
1888
+ enc)
1889
+ end
1890
+
1891
+ def forward_invocation(_self_ptr, inv)
1892
+ sel = ObjC::MSG_PTR.call(inv, ObjC.sel('selector'))
1893
+ name = sel_name_string(sel)
1894
+ return unless name == 'firstRectForCharacterRange:actualRange:'
1895
+
1896
+ rect = first_rect_for_marked_text_screen_coords
1897
+ buf = Fiddle::Pointer.malloc(32, Fiddle::RUBY_FREE)
1898
+ buf[0, 32] = rect.pack('d4')
1899
+ ObjC::MSG_VOID_1.call(inv, ObjC.sel('setReturnValue:'), buf)
1900
+ end
1901
+
1902
+ def responds_to_selector?(self_ptr, sel)
1903
+ name = sel_name_string(sel)
1904
+ return 1 if IME_FORWARDED_SELECTORS.include?(name)
1905
+ self_class = ObjC::MSG_PTR.call(self_ptr, ObjC.sel('class'))
1906
+ method = ObjC::ClassGetInstanceMethod.call(self_class, sel)
1907
+ method.null? ? 0 : 1
1908
+ end
1909
+
1910
+ def sel_name_string(sel)
1911
+ ptr = ObjC::SelGetName.call(sel)
1912
+ ptr.null? ? '' : ptr.to_s
1913
+ end
1914
+
1915
+ # Compute the on-screen rect for the marked-text caret, used by
1916
+ # the IME to anchor its candidate window. Returns 4 doubles
1917
+ # [origin_x, origin_y, width, height] in screen coords. Origin
1918
+ # is bottom-left (NSScreen convention, y from bottom).
1919
+ #
1920
+ # We anchor on the active pane's cursor (the marked text is
1921
+ # drawn there, see draw_pane_content). When @marked_text is set,
1922
+ # use its measured width so the candidate window aligns with the
1923
+ # right edge of the composition; otherwise fall back to a single
1924
+ # cell so the IME still has something sensible to anchor on.
1925
+ def first_rect_for_marked_text_screen_coords
1926
+ return [0.0, 0.0, 0.0, 0.0] unless @view && @window && @tabs && @tabs.any?
1927
+ tab = current_tab
1928
+ pane = tab&.active_pane
1929
+ return [0.0, 0.0, 0.0, 0.0] unless pane
1930
+
1931
+ rect_info = tab.pane_tree.layout(0, 0, @cols, @rows).find { |r| r[:pane] == pane }
1932
+ return [0.0, 0.0, 0.0, 0.0] unless rect_info
1933
+
1934
+ cell_w = @cell_width.to_f
1935
+ cell_h = @cell_height.to_f
1936
+ gy_off = grid_y_offset
1937
+ pane_px = rect_info[:x] * cell_w
1938
+ pane_py = gy_off + rect_info[:y] * cell_h
1939
+ cursor = pane.screen.cursor
1940
+ mx_view = pane_px + cursor.col * cell_w
1941
+ my_view = pane_py + cursor.row * cell_h
1942
+
1943
+ width = if @marked_text && !@marked_text.empty?
1944
+ @marked_text.each_char.sum { |c| c.ord > 0x7F ? cell_w * 2 : cell_w }
1945
+ else
1946
+ cell_w
1947
+ end
1948
+
1949
+ # View y is flipped (top-origin); NSWindow / NSScreen y is
1950
+ # bottom-origin. The rect's BOTTOM in window coords sits at
1951
+ # (view_frame_height − rect_bottom_in_view_coords).
1952
+ vfh = view_frame_height
1953
+ win_x = mx_view
1954
+ win_y = vfh - (my_view + cell_h)
1955
+
1956
+ sx, sy = window_to_screen_point(win_x, win_y)
1957
+ [sx, sy, width, cell_h]
1958
+ end
1959
+
1960
+ # [NSWindow convertPointToScreen:] returns NSPoint (2 doubles).
1961
+ # Same trick as event_location / dragging_location uses for the
1962
+ # other NSPoint-returning calls — Fiddle can't model the return
1963
+ # directly, so go through NSInvocation.
1964
+ def window_to_screen_point(x, y)
1965
+ sig = ObjC::MSG_PTR_1.call(
1966
+ ObjC.cls('NSWindow'),
1967
+ ObjC.sel('instanceMethodSignatureForSelector:'),
1968
+ ObjC.sel('convertPointToScreen:'))
1969
+ inv = ObjC::MSG_PTR_1.call(
1970
+ ObjC.cls('NSInvocation'),
1971
+ ObjC.sel('invocationWithMethodSignature:'), sig)
1972
+ ObjC::MSG_VOID_1.call(inv, ObjC.sel('setSelector:'),
1973
+ ObjC.sel('convertPointToScreen:'))
1974
+ arg = Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
1975
+ arg[0, 16] = [x, y].pack('dd')
1976
+ # setArgument:atIndex: signature is (id, SEL, void*, NSInteger).
1977
+ # No existing reusable msgSend variant matches; compose inline.
1978
+ set_arg_sig = ObjC.new_msg([ObjC::P, ObjC::P, ObjC::P, ObjC::L], ObjC::V)
1979
+ set_arg_sig.call(inv, ObjC.sel('setArgument:atIndex:'), arg, 2)
1980
+ ObjC::MSG_VOID_1.call(inv, ObjC.sel('invokeWithTarget:'), @window)
1981
+ out = Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
1982
+ ObjC::MSG_VOID_1.call(inv, ObjC.sel('getReturnValue:'), out)
1983
+ out[0, 16].unpack('dd')
1984
+ end
1985
+
1809
1986
  def timer_fired
1810
1987
  save_window_state
1811
1988
 
data/lib/echoes/objc.rb CHANGED
@@ -24,6 +24,14 @@ module Echoes
24
24
  GetMethodImpl = Fiddle::Function.new(LIBOBJC['class_getMethodImplementation'], [P, P], P)
25
25
  AddProtocol = Fiddle::Function.new(LIBOBJC['class_addProtocol'], [P, P], I)
26
26
  GetProtocol = Fiddle::Function.new(LIBOBJC['objc_getProtocol'], [P], P)
27
+ # SEL → const char *name. Used by the message-forwarding hooks
28
+ # so a single closure can dispatch on selector name.
29
+ SelGetName = Fiddle::Function.new(LIBOBJC['sel_getName'], [P], P)
30
+ # Look up a Method by (Class, SEL) and read its type encoding —
31
+ # lets methodSignatureForSelector: fall back to the runtime's
32
+ # default behavior for any selector we don't explicitly forward.
33
+ ClassGetInstanceMethod = Fiddle::Function.new(LIBOBJC['class_getInstanceMethod'], [P, P], P)
34
+ MethodGetTypeEncoding = Fiddle::Function.new(LIBOBJC['method_getTypeEncoding'], [P], P)
27
35
 
28
36
  # objc_msgSend variants for different signatures
29
37
  def self.new_msg(args, ret)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Echoes
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: echoes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Matsuda