Compare commits
13 Commits
main
...
force-dire
Author | SHA1 | Date |
---|---|---|
mars | 14f432543a | |
mars | 33bf08d4c1 | |
mars | 351a85f484 | |
mars | 69355204d7 | |
mars | ca3877f96c | |
mars | be816127fb | |
mars | 3a04fe95b6 | |
mars | a530600e54 | |
mars | eac404e976 | |
mars | dfb160ffef | |
mars | 6f0be98a11 | |
mars | 647b6ec59a | |
mars | 420fc2089a |
|
@ -4,6 +4,7 @@ members = [
|
||||||
"apps/music-player",
|
"apps/music-player",
|
||||||
"apps/sandbox",
|
"apps/sandbox",
|
||||||
"crates/script",
|
"crates/script",
|
||||||
|
"scripts/force-directed-graph",
|
||||||
"scripts/music-player",
|
"scripts/music-player",
|
||||||
"scripts/sao-ui",
|
"scripts/sao-ui",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "canary-force-directed-graph-script"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
canary-script = { path = "../../crates/script" }
|
|
@ -0,0 +1,566 @@
|
||||||
|
use canary_script::api::{BindPanel, DrawContext, Font, Message, Panel, PanelImpl, TextLayout};
|
||||||
|
use canary_script::{export_abi, Color, CursorEventKind, MeshVertex, Vec2};
|
||||||
|
|
||||||
|
static LABEL_FONT: &str = "Liberation Sans";
|
||||||
|
|
||||||
|
pub struct Body {
|
||||||
|
pub position: Vec2,
|
||||||
|
pub velocity: Vec2,
|
||||||
|
pub acceleration: Vec2,
|
||||||
|
pub mass: f32,
|
||||||
|
pub friction: f32,
|
||||||
|
pub fixed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Body {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
acceleration: Vec2::ZERO,
|
||||||
|
friction: 0.001,
|
||||||
|
mass: 1.0,
|
||||||
|
fixed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
pub fn apply_force(&mut self, force: Vec2) {
|
||||||
|
self.acceleration += force / self.mass; // F = ma
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_velocity(&mut self, vel: Vec2) {
|
||||||
|
self.velocity += vel;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, dt: f32) {
|
||||||
|
if self.fixed {
|
||||||
|
self.velocity = Vec2::ZERO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = self.position;
|
||||||
|
let vel = self.velocity;
|
||||||
|
let acc = self.acceleration;
|
||||||
|
|
||||||
|
let new_pos = pos + vel * dt + acc * (dt * dt * 0.5);
|
||||||
|
let new_vel = vel + acc * (dt * 0.5);
|
||||||
|
|
||||||
|
self.position = new_pos;
|
||||||
|
self.velocity = new_vel * self.friction.powf(dt);
|
||||||
|
self.acceleration = Vec2::ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NodeInfo {
|
||||||
|
pub label: TextLayout,
|
||||||
|
pub label_offset: Vec2,
|
||||||
|
pub text_size: f32,
|
||||||
|
pub hidden: bool,
|
||||||
|
pub parent: Option<usize>,
|
||||||
|
pub unhide: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeInfo {
|
||||||
|
pub fn new(label: &str) -> Self {
|
||||||
|
let text_size = 5.0;
|
||||||
|
let label = TextLayout::new(&Font::new(LABEL_FONT), &label);
|
||||||
|
let bounds = label.get_bounds();
|
||||||
|
let label_offset = Vec2::new(-bounds.width() / 2.0, text_size / 2.0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
label_offset,
|
||||||
|
text_size,
|
||||||
|
hidden: false,
|
||||||
|
parent: None,
|
||||||
|
unhide: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Node {
|
||||||
|
pub body: Body,
|
||||||
|
pub info: NodeInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MenuNode {
|
||||||
|
pub label: String,
|
||||||
|
pub children: Vec<MenuNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuNode {
|
||||||
|
pub fn make_test() -> Self {
|
||||||
|
Self {
|
||||||
|
label: "Menu".to_string(),
|
||||||
|
children: vec![
|
||||||
|
Self {
|
||||||
|
label: "File".to_string(),
|
||||||
|
children: vec![
|
||||||
|
Self {
|
||||||
|
label: "Open...".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Save as...".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Save".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Quit".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Edit".to_string(),
|
||||||
|
children: vec![
|
||||||
|
Self {
|
||||||
|
label: "Undo".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Redo".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "View".to_string(),
|
||||||
|
children: vec![
|
||||||
|
Self {
|
||||||
|
label: "Zoom in".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Zoom out".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Reset zoom".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Fullscreen".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Self {
|
||||||
|
label: "Help".to_string(),
|
||||||
|
children: vec![Self {
|
||||||
|
label: "About".to_string(),
|
||||||
|
children: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_force_graph(&self) -> ForceGraph {
|
||||||
|
let mut graph = ForceGraph::default();
|
||||||
|
|
||||||
|
let root = Node {
|
||||||
|
body: Body {
|
||||||
|
fixed: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
info: NodeInfo::new(&self.label),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = graph.nodes.len();
|
||||||
|
graph.nodes.push(root);
|
||||||
|
self.build_force_graph(&mut graph, parent);
|
||||||
|
graph.unhide(parent);
|
||||||
|
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_force_graph(&self, graph: &mut ForceGraph, parent: usize) {
|
||||||
|
let mut siblings = Vec::new();
|
||||||
|
let mut first_sibling = None;
|
||||||
|
let mut last_sibling = None;
|
||||||
|
|
||||||
|
const SIBLING_LENGTH: f32 = 60.0;
|
||||||
|
const SIBLING_STRENGTH: f32 = 0.1;
|
||||||
|
|
||||||
|
for child in self.children.iter() {
|
||||||
|
let id = graph.nodes.len();
|
||||||
|
let _ = first_sibling.get_or_insert(id);
|
||||||
|
|
||||||
|
graph.nodes.push(Node {
|
||||||
|
body: Body {
|
||||||
|
position: Vec2::from_angle(id as f32),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
info: NodeInfo {
|
||||||
|
hidden: true,
|
||||||
|
parent: Some(parent),
|
||||||
|
..NodeInfo::new(&child.label)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.springs.push(Constraint {
|
||||||
|
from: parent,
|
||||||
|
to: id,
|
||||||
|
length: 30.0,
|
||||||
|
strength: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(last) = last_sibling {
|
||||||
|
graph.springs.push(Constraint {
|
||||||
|
from: last,
|
||||||
|
to: id,
|
||||||
|
length: SIBLING_LENGTH,
|
||||||
|
strength: SIBLING_STRENGTH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
siblings.push(id);
|
||||||
|
last_sibling = Some(id);
|
||||||
|
|
||||||
|
graph.connections.push(Connection {
|
||||||
|
from: parent,
|
||||||
|
to: id,
|
||||||
|
width: 1.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.build_force_graph(graph, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(first), Some(last)) = (first_sibling, last_sibling) {
|
||||||
|
graph.springs.push(Constraint {
|
||||||
|
from: first,
|
||||||
|
to: last,
|
||||||
|
length: SIBLING_LENGTH,
|
||||||
|
strength: SIBLING_STRENGTH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.nodes[parent].info.unhide.extend_from_slice(&siblings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Constraint {
|
||||||
|
pub from: usize,
|
||||||
|
pub to: usize,
|
||||||
|
pub length: f32,
|
||||||
|
pub strength: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Constraint {
|
||||||
|
pub fn apply(&self, mut delta: Vec2) -> Vec2 {
|
||||||
|
const ALPHA: f32 = 0.99;
|
||||||
|
let distance = delta.length();
|
||||||
|
let displacement = distance - self.length;
|
||||||
|
delta *= displacement / distance * self.strength * ALPHA;
|
||||||
|
delta / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Connection {
|
||||||
|
pub from: usize,
|
||||||
|
pub to: usize,
|
||||||
|
pub width: f32,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ForceGraph {
|
||||||
|
pub nodes: Vec<Node>,
|
||||||
|
pub springs: Vec<Constraint>,
|
||||||
|
pub connections: Vec<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForceGraph {
|
||||||
|
pub fn update(&mut self, dt: f32) {
|
||||||
|
self.apply_springs(dt);
|
||||||
|
self.apply_repulsion();
|
||||||
|
|
||||||
|
for node in self.nodes.iter_mut() {
|
||||||
|
if !node.info.hidden {
|
||||||
|
node.body.update(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reaction(&self, from: usize, to: usize, delta: Vec2) -> (Vec2, Vec2) {
|
||||||
|
let from_fixed = self.nodes[from].body.fixed;
|
||||||
|
let to_fixed = self.nodes[to].body.fixed;
|
||||||
|
|
||||||
|
if from_fixed {
|
||||||
|
let from = Vec2::ZERO;
|
||||||
|
let to = if to_fixed { Vec2::ZERO } else { -delta };
|
||||||
|
(from, to)
|
||||||
|
} else if to_fixed {
|
||||||
|
let to = Vec2::ZERO;
|
||||||
|
let from = if from_fixed { Vec2::ZERO } else { delta };
|
||||||
|
(from, to)
|
||||||
|
} else {
|
||||||
|
let half_delta = delta / 2.0;
|
||||||
|
(half_delta, -half_delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_unhidden<R>(
|
||||||
|
&self,
|
||||||
|
from: usize,
|
||||||
|
to: usize,
|
||||||
|
f: impl FnOnce(&Node, &Node) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
let from = &self.nodes[from];
|
||||||
|
let to = &self.nodes[to];
|
||||||
|
|
||||||
|
if from.info.hidden || to.info.hidden {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(f(from, to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_springs(&mut self, dt: f32) {
|
||||||
|
for spring in self.springs.iter() {
|
||||||
|
let from = spring.from;
|
||||||
|
let to = spring.to;
|
||||||
|
|
||||||
|
let vel = self.with_unhidden(from, to, |from, to| {
|
||||||
|
let from = from.body.position + from.body.velocity * dt;
|
||||||
|
let to = to.body.position + to.body.velocity * dt;
|
||||||
|
let delta = to - from;
|
||||||
|
spring.apply(delta)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(vel) = vel {
|
||||||
|
let (from_vel, to_vel) = self.reaction(from, to, vel);
|
||||||
|
self.nodes[from].body.apply_velocity(from_vel);
|
||||||
|
self.nodes[to].body.apply_velocity(to_vel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_repulsion(&mut self) {
|
||||||
|
for i in 1..self.nodes.len() {
|
||||||
|
for j in i..self.nodes.len() {
|
||||||
|
let from = i - 1;
|
||||||
|
let to = j;
|
||||||
|
|
||||||
|
let force = self.with_unhidden(from, to, |from, to| {
|
||||||
|
const REPULSION_CONSTANT: f32 = 1000.0;
|
||||||
|
let delta = to.body.position - from.body.position;
|
||||||
|
let proximity = delta.length().max(0.1);
|
||||||
|
let force = -(REPULSION_CONSTANT / (proximity * proximity));
|
||||||
|
delta * force
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(force) = force {
|
||||||
|
let (from_force, to_force) = self.reaction(from, to, force);
|
||||||
|
self.nodes[from].body.apply_force(from_force);
|
||||||
|
self.nodes[to].body.apply_force(to_force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unhide(&mut self, node: usize) {
|
||||||
|
let node = &mut self.nodes[node];
|
||||||
|
node.info.hidden = false;
|
||||||
|
|
||||||
|
let position = node.body.position;
|
||||||
|
let children = node.info.unhide.to_owned();
|
||||||
|
|
||||||
|
// Avoid divide-by-zero
|
||||||
|
if children.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut neighbor_num = children.len();
|
||||||
|
let mut rotate_by = Vec2::new(1.0, 0.0);
|
||||||
|
|
||||||
|
if let Some(parent) = node.info.parent {
|
||||||
|
neighbor_num += 1;
|
||||||
|
rotate_by = self.nodes[parent].body.position - position;
|
||||||
|
rotate_by = rotate_by.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let theta = std::f32::consts::TAU / neighbor_num as f32;
|
||||||
|
|
||||||
|
for (index, child) in children.into_iter().enumerate() {
|
||||||
|
let angle = Vec2::from_angle((index + 1) as f32 * theta).rotate(rotate_by);
|
||||||
|
let child = &mut self.nodes[child];
|
||||||
|
child.info.hidden = false;
|
||||||
|
child.body.fixed = false;
|
||||||
|
child.body.position = position + angle;
|
||||||
|
child.body.velocity = Vec2::ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&mut self, node: usize) {
|
||||||
|
let node = &mut self.nodes[node];
|
||||||
|
node.info.hidden = true;
|
||||||
|
|
||||||
|
let children = node.info.unhide.to_owned();
|
||||||
|
for child in children {
|
||||||
|
self.hide(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_select(&mut self, id: usize) {
|
||||||
|
let node = &mut self.nodes[id];
|
||||||
|
|
||||||
|
if !node.body.fixed {
|
||||||
|
node.body.fixed = true;
|
||||||
|
self.unhide(id);
|
||||||
|
} else {
|
||||||
|
node.body.fixed = false;
|
||||||
|
|
||||||
|
let children = node.info.unhide.to_owned();
|
||||||
|
for child in children {
|
||||||
|
self.hide(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self, ctx: &DrawContext) {
|
||||||
|
let radius = 5.0;
|
||||||
|
let thickness = 1.0;
|
||||||
|
let color = Color::WHITE;
|
||||||
|
let label_color = Color::WHITE;
|
||||||
|
|
||||||
|
for node in self.nodes.iter() {
|
||||||
|
if node.info.hidden {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.draw_ring(node.body.position, radius, thickness, color);
|
||||||
|
|
||||||
|
let offset = node.body.position + node.info.label_offset;
|
||||||
|
ctx.draw_text_layout(&node.info.label, offset, node.info.text_size, label_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
for connection in self.connections.iter() {
|
||||||
|
self.with_unhidden(connection.from, connection.to, |from, to| {
|
||||||
|
let from = from.body.position;
|
||||||
|
let to = to.body.position;
|
||||||
|
let delta = to - from;
|
||||||
|
|
||||||
|
if delta.length() < radius * 2.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta_norm = delta.normalize();
|
||||||
|
let from = from + delta_norm * radius;
|
||||||
|
let to = to - delta_norm * radius;
|
||||||
|
let delta_cross = Vec2::new(delta_norm.y, -delta_norm.x);
|
||||||
|
let delta_side = delta_cross * connection.width / 2.0;
|
||||||
|
|
||||||
|
let vertices = [
|
||||||
|
MeshVertex {
|
||||||
|
position: from + delta_side,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
MeshVertex {
|
||||||
|
position: from - delta_side,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
MeshVertex {
|
||||||
|
position: to + delta_side,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
MeshVertex {
|
||||||
|
position: to - delta_side,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let indices = [0, 1, 2, 1, 2, 3];
|
||||||
|
|
||||||
|
ctx.draw_indexed(&vertices, &indices);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export_abi!(ForceDirectedGraphPanel);
|
||||||
|
|
||||||
|
pub struct ForceDirectedGraphPanel {
|
||||||
|
panel: Panel,
|
||||||
|
graph: ForceGraph,
|
||||||
|
center: Vec2,
|
||||||
|
last_focus: Vec2,
|
||||||
|
focus_anim: f32,
|
||||||
|
focus_target: usize,
|
||||||
|
size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindPanel for ForceDirectedGraphPanel {
|
||||||
|
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let menu = MenuNode::make_test();
|
||||||
|
let graph = menu.make_force_graph();
|
||||||
|
let panel = Self {
|
||||||
|
panel,
|
||||||
|
graph,
|
||||||
|
last_focus: Vec2::ZERO,
|
||||||
|
center: Vec2::ZERO,
|
||||||
|
focus_anim: 1.0,
|
||||||
|
focus_target: 0,
|
||||||
|
size: Vec2::splat(100.0),
|
||||||
|
};
|
||||||
|
Box::new(panel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelImpl for ForceDirectedGraphPanel {
|
||||||
|
fn update(&mut self, dt: f32) {
|
||||||
|
self.graph.update(dt);
|
||||||
|
self.focus_anim += dt;
|
||||||
|
|
||||||
|
let target_pos = self.graph.nodes[self.focus_target].body.position;
|
||||||
|
let anim = self.focus_anim / 0.5;
|
||||||
|
if anim < 0.0 {
|
||||||
|
self.center = self.last_focus;
|
||||||
|
} else if anim < 1.0 {
|
||||||
|
let anim = -(anim * (anim - 2.0));
|
||||||
|
self.center = target_pos * anim + self.last_focus * (1.0 - anim);
|
||||||
|
} else {
|
||||||
|
self.center = target_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self) {
|
||||||
|
let offset = self.size / 2.0 - self.center;
|
||||||
|
let ctx = DrawContext::new(self.panel).with_offset(offset);
|
||||||
|
self.graph.draw(&ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&mut self, new_size: Vec2) {
|
||||||
|
self.size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||||
|
let offset = self.size / 2.0 - self.center;
|
||||||
|
let at = at - offset;
|
||||||
|
if let CursorEventKind::Deselect = kind {
|
||||||
|
let node = self
|
||||||
|
.graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.position(|node| !node.info.hidden && node.body.position.distance(at) < 6.0);
|
||||||
|
if let Some(node) = node {
|
||||||
|
self.graph.on_select(node);
|
||||||
|
self.last_focus = self.center;
|
||||||
|
self.focus_target = node;
|
||||||
|
self.focus_anim = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_message(&mut self, msg: Message) {}
|
||||||
|
}
|
Loading…
Reference in New Issue