
Rust+Slint 实现蜂巢菜单源码分享
- 一、效果展示
- 二、源码分享
- 三、实现原理
-
- 1、蜂巢网格布局
- 2、圆形裁剪与可见性控制
- [3、鱼眼凸起效果(Fisheye Bulge)](#3、鱼眼凸起效果(Fisheye Bulge))
- 4、鼠标交互与点击检测
-
- 凸起跟随
- [点击检测(Rust 回调)](#点击检测(Rust 回调))
- 5、数据流与响应式更新
- 6、小结
一、效果展示




二、源码分享
1、工程结构

2、main.rs
rust
slint::include_modules!();
use slint::{Model, ModelRc};
fn main() {
let ui = MainWindow::new().expect("无法创建主窗口");
// ─── 读取 Slint 中的 icon_data(含位置信息)───
let icon_data_model = ui.get_icon_data();
let icon_count = icon_data_model.row_count();
// 缓存图标位置数据,用于点击检测
let positions: Vec<(f32, f32, f32, i32)> = (0..icon_count)
.map(|i| {
let d = icon_data_model.row_data(i).unwrap();
(d.base_x, d.base_y, d.sz, d.icon_index)
})
.collect();
println!("已加载 {} 个图标位置", positions.len());
// ─── 点击检测回调 ───
// 遍历 icon_data,找距离点击位置最近的图标(在 sz*0.7 范围内)
let positions_clone = positions.clone();
ui.on_find_icon_at(move |click_x, click_y| -> i32 {
let mut closest_idx: i32 = -1;
let mut closest_dist = f32::MAX;
for (base_x, base_y, sz, icon_index) in &positions_clone {
let dx = *base_x - click_x as f32;
let dy = *base_y - click_y as f32;
let dist = (dx * dx + dy * dy).sqrt();
let hit_radius = sz * 0.7;
if dist <= hit_radius && dist < closest_dist {
closest_dist = dist;
closest_idx = *icon_index;
}
}
closest_idx
});
ui.run().expect("运行 UI 失败");
}
3、build.rs
rust
fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}
4、main.slint
rust
export struct IconData {
base_x: float,
base_y: float,
sz: float,
icon_index: int,
name: string,
}
export component MainWindow inherits Window {
title: "蜂巢菜单";
preferred-width: 700px;
preferred-height: 440px;
background: #0a0a0a;
// ─── 凸起效果中心(跟随鼠标)───
property <float> bulge_cx: 350;
property <float> bulge_cy: 200;
// ─── 选中图标(-1 = 未选中)───
property <int> selected_index: -1;
// ─── 点击检测回调(Rust 实现:遍历 icon_data 找最近图标)───
callback find_icon_at(float, float) -> int;
// ─── 布局常量 ───
property <float> clock_cx: 350;
property <float> clock_cy: 200;
property <float> clip_r: 155;
// ─── 图片资源(编译时嵌入)───
property <[image]> image_urls: [
@image-url("../images/apple.svg"),
@image-url("../images/avocado.svg"),
@image-url("../images/banana.svg"),
@image-url("../images/bayberry.svg"),
@image-url("../images/blueberry.svg"),
@image-url("../images/cherry.svg"),
@image-url("../images/dragonfruit.svg"),
@image-url("../images/grape.svg"),
@image-url("../images/mango.svg"),
@image-url("../images/mangosteen.svg"),
@image-url("../images/orange.svg"),
@image-url("../images/peach.svg"),
@image-url("../images/persimmon.svg"),
@image-url("../images/pineapple.svg"),
@image-url("../images/strawberry.svg"),
@image-url("../images/watermelon.svg"),
@image-url("../images/waxapple.svg"),
];
// ─── 图标数据(21个六角网格位置,Rust 需要读取以做点击检测)───
in property <[IconData]> icon_data: [
// 第1行 (y=100, 3个)
{ base_x: 290.0, base_y: 100.0, sz: 54.0, icon_index: 0, name: "apple" },
{ base_x: 348.0, base_y: 100.0, sz: 54.0, icon_index: 1, name: "avocado" },
{ base_x: 406.0, base_y: 100.0, sz: 54.0, icon_index: 2, name: "banana" },
// 第2行 (y=150, 5个, 偏移+29)
{ base_x: 205.0, base_y: 150.0, sz: 54.0, icon_index: 3, name: "bayberry" },
{ base_x: 263.0, base_y: 150.0, sz: 54.0, icon_index: 4, name: "blueberry" },
{ base_x: 321.0, base_y: 150.0, sz: 54.0, icon_index: 5, name: "cherry" },
{ base_x: 379.0, base_y: 150.0, sz: 54.0, icon_index: 6, name: "dragonfruit" },
{ base_x: 437.0, base_y: 150.0, sz: 54.0, icon_index: 7, name: "grape" },
// 第3行 (y=200, 5个, 中间行)
{ base_x: 234.0, base_y: 200.0, sz: 54.0, icon_index: 8, name: "mango" },
{ base_x: 292.0, base_y: 200.0, sz: 54.0, icon_index: 9, name: "mangosteen" },
{ base_x: 350.0, base_y: 200.0, sz: 54.0, icon_index: 10, name: "orange" },
{ base_x: 408.0, base_y: 200.0, sz: 54.0, icon_index: 11, name: "peach" },
{ base_x: 466.0, base_y: 200.0, sz: 54.0, icon_index: 12, name: "persimmon" },
// 第4行 (y=250, 5个, 偏移+29)
{ base_x: 205.0, base_y: 250.0, sz: 54.0, icon_index: 13, name: "pineapple" },
{ base_x: 263.0, base_y: 250.0, sz: 54.0, icon_index: 14, name: "strawberry" },
{ base_x: 321.0, base_y: 250.0, sz: 54.0, icon_index: 15, name: "watermelon" },
{ base_x: 379.0, base_y: 250.0, sz: 54.0, icon_index: 16, name: "waxapple" },
{ base_x: 437.0, base_y: 250.0, sz: 54.0, icon_index: 17, name: "apple" },
// 第5行 (y=300, 3个)
{ base_x: 290.0, base_y: 300.0, sz: 54.0, icon_index: 18, name: "avocado" },
{ base_x: 348.0, base_y: 300.0, sz: 54.0, icon_index: 19, name: "banana" },
{ base_x: 406.0, base_y: 300.0, sz: 54.0, icon_index: 20, name: "bayberry" },
];
// ═══════════════════════════════════════════════
// 表盘外框
// ═══════════════════════════════════════════════
Rectangle {
x: root.clock_cx * 1px - root.clip_r * 1px - 8px;
y: root.clock_cy * 1px - root.clip_r * 1px - 8px;
width: root.clip_r * 2px + 16px;
height: root.clip_r * 2px + 16px;
border-radius: root.clip_r * 1px + 8px;
background: @linear-gradient(145deg, #2e2e2e 0%, #0f0f0f 50%, #1e1e1e 100%);
border-width: 1.5px;
border-color: #555;
}
Rectangle {
x: root.clock_cx * 1px - root.clip_r * 1px - 3px;
y: root.clock_cy * 1px - root.clip_r * 1px - 3px;
width: root.clip_r * 2px + 6px;
height: root.clip_r * 2px + 6px;
border-radius: root.clip_r * 1px + 3px;
background: @linear-gradient(200deg, #222 0%, #0a0a0a 50%, #181818 100%);
border-width: 1px;
border-color: #333;
}
Rectangle {
x: root.clock_cx * 1px - root.clip_r * 1px;
y: root.clock_cy * 1px - root.clip_r * 1px;
width: root.clip_r * 2px;
height: root.clip_r * 2px;
border-radius: root.clip_r * 1px;
background: #050505;
border-width: 0.5px;
border-color: #1a1a1a;
}
// ═══════════════════════════════════════════════
// 蜂巢图标网格 + 鱼眼凸起效果
// ═══════════════════════════════════════════════
for icon[idx] in root.icon_data : Rectangle {
property <float> cdx: icon.base_x - root.clock_cx;
property <float> cdy: icon.base_y - root.clock_cy;
property <float> cdist: Math.sqrt(self.cdx * self.cdx + self.cdy * self.cdy);
property <bool> in_circle: self.cdist <= root.clip_r - 14;
property <float> mdx: icon.base_x - root.bulge_cx;
property <float> mdy: icon.base_y - root.bulge_cy;
property <float> mdist: Math.sqrt(self.mdx * self.mdx + self.mdy * self.mdy);
property <float> bulge: 0.38 + 0.82 * Math.exp(-(self.mdist * self.mdist) / 5500.0);
property <float> scaled_sz: icon.sz * self.bulge;
property <bool> is_selected: idx == root.selected_index;
visible: self.in_circle;
x: (icon.base_x - self.scaled_sz / 2.0) * 1px;
y: (icon.base_y - self.scaled_sz / 2.0) * 1px;
width: self.scaled_sz * 1px;
height: self.scaled_sz * 1px;
border-radius: self.scaled_sz * 0.24 * 1px;
background: self.is_selected
? @linear-gradient(155deg, #2a3a50 0%, #152030 100%)
: @linear-gradient(155deg, #1e1e20 0%, #0c0c0e 100%);
border-width: self.is_selected ? 1.5px : 0.5px;
border-color: self.is_selected ? #4a8eff80 : #3a3a3a50;
Image {
source: root.image_urls[Math.mod(icon.icon_index, root.image_urls.length)];
width: parent.width * 0.60;
height: parent.height * 0.60;
x: parent.width * 0.20;
y: parent.height * 0.20;
}
}
// ═══════════════════════════════════════════════
// 鼠标交互(凸起跟随 + 点击检测)
// ═══════════════════════════════════════════════
TouchArea {
x: 0;
y: 0;
width: root.width;
height: root.height;
private property <bool> is_pressed:false;
moved => {
root.bulge_cx = self.mouse-x / 1px;
root.bulge_cy = self.mouse-y / 1px;
}
clicked => {
// Rust 回调:遍历 icon_data,找距离点击位置最近的图标
/*root.selected_index = root.find_icon_at(
self.mouse-x / 1px,
self.mouse-y / 1px
);*/
}
pointer-event(event) => {
if event.kind == PointerEventKind.down {
self.is_pressed = true;
} else if event.kind == PointerEventKind.move{
self.is_pressed = false;
}
else if event.kind == PointerEventKind.up {
if(self.is_pressed){
// Rust 回调:遍历 icon_data,找距离点击位置最近的图标
root.selected_index = root.find_icon_at(
self.mouse-x / 1px,
self.mouse-y / 1px
);
}
self.is_pressed = false;
}
}
}
// ═══════════════════════════════════════════════
// 底部信息面板
// ═══════════════════════════════════════════════
Rectangle {
x: root.clock_cx * 1px - 150px;
y: root.clock_cy * 1px + root.clip_r * 1px + 20px;
width: 300px;
height: 46px;
border-radius: 23px;
background: root.selected_index >= 0 ? #1a1a2e : #111118;
border-width: 1px;
border-color: root.selected_index >= 0 ? #4a8eff40 : #222;
Text {
x: 0;
y: 0;
width: parent.width;
height: parent.height;
horizontal-alignment: center;
vertical-alignment: center;
text: root.selected_index >= 0
? "📌 #" + root.selected_index + " " + root.icon_data[root.selected_index].name
: "点击图标查看详情";
font-size: 14px;
font-weight: 300;
color: root.selected_index >= 0 ? #8ab4f8 : #555;
}
}
}
5、Cargo.toml
rust
[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"
[dependencies]
slint = "1.17.1"
[build-dependencies]
slint-build = "1.17.1"
三、实现原理
本项目整体采用 Rust + Slint 前后端分离架构:Slint 负责 UI 渲染与交互,Rust 负责业务逻辑与点击检测。下面从布局、渲染、交互三个层面拆解核心实现。
1、蜂巢网格布局
蜂巢(六边形)网格的本质是错位排列的圆形网格:奇数行与偶数行在水平方向错开半个间距,从而形成六边形的视觉交错感。
在 main.slint 中,21 个图标的位置并非运行时计算,而是预先在 icon_data 数组中硬编码。以第 1 行(y=100)和第 2 行(y=150)为例:
slint
// 第1行 (y=100, 3个)
{ base_x: 290.0, base_y: 100.0, sz: 54.0, icon_index: 0, name: "apple" },
{ base_x: 348.0, base_y: 100.0, sz: 54.0, icon_index: 1, name: "avocado" },
{ base_x: 406.0, base_y: 100.0, sz: 54.0, icon_index: 2, name: "banana" },
// 第2行 (y=150, 5个, 偏移+29)
{ base_x: 205.0, base_y: 150.0, sz: 54.0, icon_index: 3, name: "bayberry" },
可以看到,第 2 行比第 1 行在水平方向偏移了 +29px,这正是蜂巢错位的关键。每行图标数量按 3-5-5-5-3 分布,整体呈菱形,正好贴合圆形表盘的裁剪区域。
2、圆形裁剪与可见性控制
表盘并非把所有图标平铺显示,而是通过圆形裁剪 只保留中心区域内的图标。核心逻辑在 for 循环中:
slint
property <float> cdx: icon.base_x - root.clock_cx;
property <float> cdy: icon.base_y - root.clock_cy;
property <float> cdist: Math.sqrt(self.cdx * self.cdx + self.cdy * self.cdy);
property <bool> in_circle: self.cdist <= root.clip_r - 14;
cdx/cdy:图标中心到表盘中心(clock_cx, clock_cy)的向量;cdist:图标中心到表盘中心的欧几里得距离;in_circle:当距离小于裁剪半径clip_r - 14时,图标才可见。
slint
visible: self.in_circle;
这样,位于圆形表盘边缘之外的图标会被自动隐藏,形成干净的圆形表盘效果。
3、鱼眼凸起效果(Fisheye Bulge)
这是整个项目最出彩的视觉效果。当鼠标移动时,靠近鼠标的图标会放大凸起,远离的则缩小,形成类似鱼眼镜头的扭曲感。
核心公式在 for 循环中:
slint
property <float> mdx: icon.base_x - root.bulge_cx;
property <float> mdy: icon.base_y - root.bulge_cy;
property <float> mdist: Math.sqrt(self.mdx * self.mdx + self.mdy * self.mdy);
property <float> bulge: 0.38 + 0.82 * Math.exp(-(self.mdist * self.mdist) / 5500.0);
这里使用了高斯函数(正态分布)作为缩放因子:
- 当鼠标恰好位于图标中心(
mdist = 0)时,bulge = 0.38 + 0.82 * 1.0 = 1.20,图标放大到 120%; - 当鼠标远离时,
mdist增大,exp(-mdist²/5500)趋近于 0,bulge趋近于0.38,图标缩小到 38%。
5500 是高斯函数的方差参数,控制凸起的影响范围------数值越大,影响半径越广,过渡越平缓。
最终每个图标的实际尺寸为:
slint
property <float> scaled_sz: icon.sz * self.bulge;
4、鼠标交互与点击检测
凸起跟随
TouchArea 的 moved 事件实时更新凸起中心:
slint
moved => {
root.bulge_cx = self.mouse-x / 1px;
root.bulge_cy = self.mouse-y / 1px;
}
bulge_cx / bulge_cy 变化后,所有图标的 bulge 因子会自动重新计算,因为 Slint 的响应式绑定会自动追踪依赖并触发重绘。
点击检测(Rust 回调)
点击检测采用 Rust 回调 + 最近邻搜索 策略。Slint 侧通过 pointer-event 捕获按下与抬起:
slint
pointer-event(event) => {
if event.kind == PointerEventKind.down {
self.is_pressed = true;
} else if event.kind == PointerEventKind.move{
self.is_pressed = false;
}
else if event.kind == PointerEventKind.up {
if(self.is_pressed){
root.selected_index = root.find_icon_at(
self.mouse-x / 1px,
self.mouse-y / 1px
);
}
self.is_pressed = false;
}
}
这里用 is_pressed 标志区分「按下后抬起」与「拖动后抬起」,避免拖动过程中误触发点击。
Rust 侧实现 find_icon_at 回调,遍历所有图标位置,找到距离点击点最近且在命中半径内的图标:
rust
ui.on_find_icon_at(move |click_x, click_y| -> i32 {
let mut closest_idx: i32 = -1;
let mut closest_dist = f32::MAX;
for (base_x, base_y, sz, icon_index) in &positions_clone {
let dx = *base_x - click_x as f32;
let dy = *base_y - click_y as f32;
let dist = (dx * dx + dy * dy).sqrt();
let hit_radius = sz * 0.7;
if dist <= hit_radius && dist < closest_dist {
closest_dist = dist;
closest_idx = *icon_index;
}
}
closest_idx
});
- 命中半径取图标尺寸的
0.7倍,兼顾点击精度与容错; - 采用最近邻策略,即使两个图标相邻,也只会选中距离最近的那个。
5、数据流与响应式更新
整个项目的数据流可以概括为:
#mermaid-svg-KtdjF8m1Uqygaiy5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KtdjF8m1Uqygaiy5 .error-icon{fill:#552222;}#mermaid-svg-KtdjF8m1Uqygaiy5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KtdjF8m1Uqygaiy5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .marker.cross{stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KtdjF8m1Uqygaiy5 p{margin:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label text{fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label span{color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label span p{background-color:transparent;}#mermaid-svg-KtdjF8m1Uqygaiy5 .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 span{fill:#333;color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node rect,#mermaid-svg-KtdjF8m1Uqygaiy5 .node circle,#mermaid-svg-KtdjF8m1Uqygaiy5 .node ellipse,#mermaid-svg-KtdjF8m1Uqygaiy5 .node polygon,#mermaid-svg-KtdjF8m1Uqygaiy5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .rough-node .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .rough-node .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label{text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node.clickable{cursor:pointer;}#mermaid-svg-KtdjF8m1Uqygaiy5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .arrowheadPath{fill:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster text{fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster span{color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KtdjF8m1Uqygaiy5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape p,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label rect,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KtdjF8m1Uqygaiy5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TouchArea 鼠标移动
更新 bulge_cx / bulge_cy
高斯函数计算 bulge 因子
scaled_sz 缩放图标尺寸
pointer-event 抬起事件
Rust find_icon_at 回调
返回最近图标 index
selected_index 更新
底部面板显示图标名称
Slint 的响应式绑定 机制保证了:只要 bulge_cx、bulge_cy、selected_index 任一属性变化,所有依赖它们的 UI 元素会自动重绘,无需手动刷新。
6、小结
| 模块 | 技术要点 |
|---|---|
| 蜂巢布局 | 奇数/偶数行错位排列,预置坐标 |
| 圆形裁剪 | 距离判断 + visible 绑定 |
| 鱼眼凸起 | 高斯函数 exp(-d²/σ²) 缩放 |
| 点击检测 | Rust 最近邻搜索 + 命中半径 |
| 响应式更新 | Slint 属性绑定自动重绘 |
通过 Rust 与 Slint 的配合,既保留了 Slint 声明式 UI 的高效开发体验,又借助 Rust 实现了高性能的几何计算,整体架构清晰、职责分明。
