Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ WindowManager z 序表、demo toasts(`push_bounded` 顺带解决无上限堆
| 原语 | 说明 |
|---|---|
| `textarea` + `value:`/`on_submit` | 多行输入:Signal 双向绑定 + ⌘⏎ 提交 |
| `drag_move` / `drag_resize` + `drag_dir`/`drag_min`/`drag_clamp`/`drag_pane` | 零重渲染拖拽;八向缩放;最小尺寸;视口 clamp;面板选择器 |
| `drag_move` / `drag_resize` + `drag_dir`/`drag_min`/`drag_clamp`/`drag_pane`/`drag_scale` | 零重渲染拖拽;八向缩放;最小尺寸;视口 clamp;面板选择器;世界缩放补偿(屏幕位移 ÷ drag_scale) |
| `on_front` | mousedown 置顶 |
| `on_menu` | contextmenu 菜单 |
| `on_hover` | mouseenter/leave → true/false |
Expand Down Expand Up @@ -246,3 +246,12 @@ beryl/
- **纯点击不算拖拽**:L1 setup_drag 带 moved 阈值(位移 >1px 才算拖),
mouseup 未超阈值不回调 handler——点击标题栏不再触发 place/重渲染
(顺带消除「点在屏幕顶缘的标题栏把窗口最大化」的误吸附路径)。
- **世界缩放下的拖拽漂移(drag_scale)**:offsetLeft/Top 是布局(世界)坐标,
不受容器 CSS transform 影响,而 clientX/Y 是屏幕像素——容器带 zoom 缩放时
不按比例补偿,zoom=2 的窗口拖起来跑双倍距离,且松手回写把误差烤进 payload。
L1 setup_drag 加可选 `drag_scale:` prop(Numeric 或 callable,mousedown 时
取值一次、手势期冻结),屏幕位移 ÷ scale 再进 compute_geom 与 live 跟随;
数学核心提出为纯 CRuby 的 `Beryl::DragGeometry.compute`(回归测试
`test/drag_geometry_test.rb`),WindowFrame/wm.frame 经 `drag_scale:` prop
透传标题栏与八向手柄。注:drag_clamp 的视口边界仍是屏幕像素口径,仅
scale=1(经典桌面)语义精确。
58 changes: 22 additions & 36 deletions lib/beryl/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def setup_text_area(node)
# 通用拖拽:drag_move / drag_resize——按住本节点拖动父面板(或 drag_pane
# 选择器命中的祖先);期间直接改 style(零重渲染),松手回调 {x, y, w, h}。
# drag_dir: 八向缩放方位(n/e/s/w/ne/nw/se/sw,默认 se);
# drag_min: [w, h] 最小尺寸;drag_clamp: 拖动限制在视口内(留 60×30 可见)。
# drag_min: [w, h] 最小尺寸;drag_clamp: 拖动限制在视口内(留 60×30 可见);
# drag_scale: 世界缩放倍率(如 ZUI 相机 zoom,Numeric 或 callable,
# mousedown 时取值一次)——屏幕指针位移 ÷ scale 才等于布局位移。
def setup_drag(node)
move_handler = node.props[:drag_move]
resize_handler = node.props[:drag_resize]
Expand All @@ -83,6 +85,7 @@ def setup_drag(node)
dir = node.props[:drag_dir] || 'se'
min_w, min_h = node.props[:drag_min] || [160, 70]
clamp_move = node.props[:drag_clamp] ? true : false
drag_scale = node.props[:drag_scale]
doc = Native(`document`)

el.addEventListener("mousedown", ->(raw) {
Expand All @@ -92,42 +95,25 @@ def setup_drag(node)
ev.preventDefault
sx = ev[:clientX]
sy = ev[:clientY]
ol = pane[:offsetLeft]
ot = pane[:offsetTop]
ow = pane[:offsetWidth]
oh = pane[:offsetHeight]
vw = Native(`window`)[:innerWidth]
vh = Native(`window`)[:innerHeight]
# mousedown 时取值:手势期间冻结(与 sx/sy 同语义),支持传 callable 取动态 zoom
scale = drag_scale.respond_to?(:call) ? drag_scale.call : drag_scale
base = {
left: pane[:offsetLeft], top: pane[:offsetTop],
w: pane[:offsetWidth], h: pane[:offsetHeight],
}
viewport = { w: Native(`window`)[:innerWidth], h: Native(`window`)[:innerHeight] }
moved = false # 纯点击(未超过阈值)不算拖拽:mouseup 不回调,几何不回写
ldx = 0
ldy = 0
sdx = 0 # 屏幕像素位移(相对手势起点;moved 阈值按屏幕口径判定)
sdy = 0
# 手势中途 view 可能重渲染并替换面板节点(如 mousedown 触发 on_front/focus),
# 本监听器仍持有旧 pane——最终几何一律按「mousedown 基准 + 指针位移」纯数学
# 计算,不读旧节点现状;mousemove 的样式跟随在旧节点已脱离文档时自动跳过
# 计算(核心在 Beryl::DragGeometry,CRuby 可测),不读旧节点现状;
# mousemove 的样式跟随在旧节点已脱离文档时自动跳过
compute_geom = lambda {
if is_move
left = ol + ldx
top = ot + ldy
if clamp_move
left = [[left, 60 - ow].max, vw - 60].min
top = [[top, 0].max, vh - 30].min
end
{ left: left, top: top, w: ow, h: oh }
else
w = ow
h = oh
left = ol
top = ot
w = ow + ldx if dir.include?("e")
h = oh + ldy if dir.include?("s")
w = ow - ldx if dir.include?("w")
h = oh - ldy if dir.include?("n")
w = min_w if w < min_w
h = min_h if h < min_h
left = ol + (ow - w) if dir.include?("w")
top = ot + (oh - h) if dir.include?("n")
{ left: left, top: top, w: w, h: h }
end
Beryl::DragGeometry.compute(base, sdx, sdy,
scale: scale, move: is_move, dir: dir,
min: [min_w, min_h], clamp: clamp_move,
viewport: viewport)
}
on_move = nil
on_up = ->(_raw2) {
Expand All @@ -140,9 +126,9 @@ def setup_drag(node)
}
on_move = ->(raw2) {
e2 = Native(raw2)
ldx = e2[:clientX] - sx
ldy = e2[:clientY] - sy
moved = true if ldx.abs > 1 || ldy.abs > 1
sdx = e2[:clientX] - sx
sdy = e2[:clientY] - sy
moved = true if sdx.abs > 1 || sdy.abs > 1
next unless pane[:isConnected] # 旧面板已被重渲染替换:样式跟随降级,松手仍回写正确几何

g = compute_geom.call
Expand Down
42 changes: 42 additions & 0 deletions lib/beryl/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,46 @@ def self.next_z
def self.pick(hash, key)
hash[key] || hash[key.to_s] || hash[key.to_sym]
end

# 拖拽几何核心(纯 CRuby,F5):「mousedown 基准几何 + 屏幕指针位移」→ 手势几何。
# 数学从 L1 setup_drag 提出,CRuby 可直接回归测试。
# drag_scale(世界缩放倍率,如 ZUI 相机 zoom):offsetLeft/Top 是布局(世界)
# 坐标,不受容器 CSS transform 影响,而 clientX/Y 是屏幕像素——zoom ≠ 1 时
# 屏幕位移必须 ÷ scale 才等于布局位移,否则拖拽漂移(zoom=2 跑双倍距离)。
module DragGeometry
# base {left:, top:, w:, h:} mousedown 时的面板几何(布局坐标,口径同 offset*)
# dx/dy 屏幕像素位移(clientX/Y − 手势起点)
# scale drag_scale(mousedown 时取值;nil → 1.0)
# move true=移动 / false=缩放;dir 八向;min [w, h] 最小尺寸
# clamp 限制在视口内(横留 60 / 纵留 30 可见);viewport {w:, h:}
def self.compute(base, dx, dy, scale: 1.0, move: true, dir: 'se', min: [160, 70], clamp: false, viewport: nil)
scale = 1.0 if scale.nil?
ldx = dx / scale
ldy = dy / scale
if move
left = base[:left] + ldx
top = base[:top] + ldy
if clamp && viewport
left = [[left, 60 - base[:w]].max, viewport[:w] - 60].min
top = [[top, 0].max, viewport[:h] - 30].min
end
{ left: left, top: top, w: base[:w], h: base[:h] }
else
min_w, min_h = min
w = base[:w]
h = base[:h]
left = base[:left]
top = base[:top]
w = base[:w] + ldx if dir.include?("e")
h = base[:h] + ldy if dir.include?("s")
w = base[:w] - ldx if dir.include?("w")
h = base[:h] - ldy if dir.include?("n")
w = min_w if w < min_w
h = min_h if h < min_h
left = base[:left] + (base[:w] - w) if dir.include?("w")
top = base[:top] + (base[:h] - h) if dir.include?("n")
{ left: left, top: top, w: w, h: h }
end
end
end
end
6 changes: 4 additions & 2 deletions lib/beryl/window.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class WindowFrame < Citrine::Component
prop :resizable, default: true
prop :min_w, type: Numeric, default: 160
prop :min_h, type: Numeric, default: 70
prop :drag_scale # 世界缩放倍率(ZUI 相机 zoom,Numeric 或 callable),
# 透传拖拽/缩放手柄:屏幕位移 ÷ drag_scale = 布局位移
prop :active, default: nil # true/false/nil(nil 保持旧外观)
prop :minimized, default: false # true 时整个窗口不渲染
prop :maximized, default: false # 最大化态:□ 控制钮变还原图标 ⧉(由 WindowManager 注入)
Expand All @@ -46,7 +48,7 @@ def view

head_opts = {
drag_move: on_move, on_menu: on_menu, on_click: on_head_click,
on_dblclick: on_head_dblclick,
on_dblclick: on_head_dblclick, drag_scale: drag_scale,
}.compact
body_opts = { on_click: on_body_click }.compact
frame_opts = { on_front: on_front }.compact
Expand Down Expand Up @@ -108,7 +110,7 @@ def render_handles
dirs.each do |d|
box(css_class: "rs-handle rs-#{d}",
drag_resize: on_resize, drag_dir: d,
drag_min: [min_w, min_h])
drag_min: [min_w, min_h], drag_scale: drag_scale)
end
end

Expand Down
87 changes: 87 additions & 0 deletions test/drag_geometry_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

# 拖拽几何回归(Beryl::DragGeometry,L1 setup_drag 的纯数学核心):
# scale=1 时与既有行为逐点一致;drag_scale(世界缩放补偿)把屏幕位移
# ÷ scale 再进布局坐标——ZUI zoom≠1 不补偿会漂移(zoom=2 跑双倍距离)。
require 'minitest/autorun'
require 'beryl'

class DragGeometryTest < Minitest::Test
BASE = { left: 100, top: 80, w: 300, h: 200 }.freeze

# ── scale=1:移动 ─────────────────────────────────────

def test_move_plain
assert_equal({ left: 130, top: 100, w: 300, h: 200 },
Beryl::DragGeometry.compute(BASE, 30, 20))
end

def test_move_clamp_keeps_60x30_visible
vp = { w: 1200, h: 800 }
g = Beryl::DragGeometry.compute(BASE, 10_000, 10_000, clamp: true, viewport: vp)
assert_equal 1140, g[:left] # vw - 60
assert_equal 770, g[:top] # vh - 30
g = Beryl::DragGeometry.compute(BASE, -10_000, -10_000, clamp: true, viewport: vp)
assert_equal(-240, g[:left]) # 只留 60px 露出
assert_equal 0, g[:top]
end

# ── scale=1:八向缩放 ─────────────────────────────────

def test_resize_se_grows
assert_equal({ left: 100, top: 80, w: 360, h: 240 },
Beryl::DragGeometry.compute(BASE, 60, 40, move: false, dir: 'se'))
end

def test_resize_ne_combines_n_and_e
assert_equal({ left: 100, top: 50, w: 340, h: 230 },
Beryl::DragGeometry.compute(BASE, 40, -30, move: false, dir: 'ne'))
end

def test_resize_w_shifts_left
assert_equal({ left: 50, top: 80, w: 350, h: 200 },
Beryl::DragGeometry.compute(BASE, -50, 0, move: false, dir: 'w'))
end

def test_resize_enforces_min_and_anchors_far_side
# 拖过最小宽:w 钳到 min,left 锚在 ol + (ow - min_w)
g = Beryl::DragGeometry.compute(BASE, 500, 0, move: false, dir: 'w', min: [160, 70])
assert_equal 160, g[:w]
assert_equal 100 + (300 - 160), g[:left]
g = Beryl::DragGeometry.compute(BASE, 0, 500, move: false, dir: 'n', min: [160, 70])
assert_equal 70, g[:h]
assert_equal 80 + (200 - 70), g[:top]
end

# ── drag_scale:世界缩放补偿(ZUI 关键路径)────────────

def test_scale_zoom_in_halves_world_travel
# zoom=2:指针拖 60/40 屏幕像素 = 30/20 世界像素
assert_equal({ left: 130, top: 100, w: 300, h: 200 },
Beryl::DragGeometry.compute(BASE, 60, 40, scale: 2.0))
end

def test_scale_zoom_out_doubles_world_travel
# zoom=0.5:指针拖 15/10 屏幕像素 = 30/20 世界像素
assert_equal({ left: 130, top: 100, w: 300, h: 200 },
Beryl::DragGeometry.compute(BASE, 15, 10, scale: 0.5))
end

def test_scale_applies_to_resize_handle
g = Beryl::DragGeometry.compute(BASE, 120, 80, scale: 2.0, move: false, dir: 'se')
assert_equal({ left: 100, top: 80, w: 360, h: 240 }, g)
end

def test_scale_all_zui_tiers_match_unscaled_math
# Z1 验收五档:各档下同等世界位移,手感与光标一致
[0.25, 0.5, 1.0, 2.0, 4.0].each do |z|
g = Beryl::DragGeometry.compute(BASE, 30 * z, 20 * z, scale: z)
assert_equal({ left: 130, top: 100, w: 300, h: 200 }, g, "zoom=#{z}")
end
end

def test_nil_scale_defaults_to_one
assert_equal({ left: 130, top: 100, w: 300, h: 200 },
Beryl::DragGeometry.compute(BASE, 30, 20, scale: nil))
end
end
15 changes: 15 additions & 0 deletions test/window_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ def test_frame_move_and_close_wiring
assert_equal [], wm.windows
end

# drag_scale:世界缩放补偿(ZUI 相机 zoom)——prop 契约 + wm.frame opts 透传。
# L1 原语在浏览器侧才接线,这里锁 prop 通路;数学在 DragGeometryTest 逐点锁。
def test_frame_drag_scale_prop_and_passthrough
html = render(Beryl::WindowFrame.new(
title: 'z', geometry: { 'px' => 1, 'py' => 1 }, drag_scale: 2.0,
on_move: ->(_ev) {}, on_resize: ->(_ev) {},
content: -> { nil },
))
assert_includes html, 'rs-handle' # drag_scale 不改结构,随标题栏/手柄透传
wm = Beryl::WindowManager.new
wm.open(:a, geometry: { x: 0, y: 0, w: 100, h: 80 })
frame = wm.frame(:a, drag_scale: -> { 2.0 }, content: -> { nil })
assert_equal 2.0, frame.drag_scale.call # callable(mousedown 时取值)经 opts 透传
end

# ── Taskbar ───────────────────────────────────────────

def test_taskbar_renders_and_toggles
Expand Down