GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Refactor 3D Viewport into standalone Viewport3D widget
Cargo.toml | 1 +
src/backend/window_runner.rs | 41 +---
src/config.rs | 10 +-
src/layout.rs | 33 +++-
src/widget/container/mod.rs | 2 -
src/widget/container/plate.rs | 2 +-
src/widget/container/section_container.rs | 38 ++++
src/widget/container/viewport_bg.rs | 26 ---
src/widget/display/mod.rs | 3 +
src/widget/display/viewport_3d.rs | 306 ++++++++++++++++++++++++++++++
src/widget/input/dropdown.rs | 2 +-
src/widget/input/slider.rs | 4 +-
src/widget/input/spinbox.rs | 4 +-
src/widget/input/trackpad.rs | 2 +-
src/widget/mod.rs | 5 +-
15 files changed, 401 insertions(+), 78 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index fa88b2f..854f1c6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ path = "src/lib.rs"
[dependencies]
raw-window-handle = "0.6"
wgpu = "24"
+glam = "0.29"
bytemuck = { version = "1", features = ["derive"] }
pollster = "0.4"
glyphon = "0.8"
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 999912c..e665591 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -141,12 +141,6 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
_ => None,
});
- let resolved_mono = if !mono_fallback.is_empty() {
- find_cased_family(fs, &mono_fallback)
- } else {
- None
- };
-
let resolved_sans = if !sans_fallback.is_empty() {
find_cased_family(fs, &sans_fallback)
} else {
@@ -160,7 +154,7 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
- glyphon::Family::Name(&mono_fallback)
+ glyphon::Family::Name(crate::layout::get_system_monospace_font())
}
} else {
glyphon::Family::Name(crate::layout::get_system_monospace_font())
@@ -171,15 +165,7 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
- if !mono_fallback.is_empty() {
- if let Some(ref cased_mono) = resolved_mono {
- glyphon::Family::Name(cased_mono)
- } else {
- glyphon::Family::Name(&mono_fallback)
- }
- } else {
- glyphon::Family::Name(crate::layout::get_system_monospace_font())
- }
+ glyphon::Family::SansSerif
}
} else {
glyphon::Family::SansSerif
@@ -190,15 +176,7 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
- if !mono_fallback.is_empty() {
- if let Some(ref cased_mono) = resolved_mono {
- glyphon::Family::Name(cased_mono)
- } else {
- glyphon::Family::Name(&mono_fallback)
- }
- } else {
- glyphon::Family::Name(crate::layout::get_system_monospace_font())
- }
+ glyphon::Family::Serif
}
} else {
glyphon::Family::Serif
@@ -210,17 +188,11 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
if !sans_fallback.is_empty() {
if let Some(ref cased) = resolved_sans {
glyphon::Family::Name(cased)
- } else if !mono_fallback.is_empty() {
- if let Some(ref cased_mono) = resolved_mono {
- glyphon::Family::Name(cased_mono)
- } else {
- glyphon::Family::Name(&mono_fallback)
- }
} else {
- glyphon::Family::Name(crate::layout::get_system_monospace_font())
+ glyphon::Family::SansSerif
}
} else {
- glyphon::Family::Name(crate::layout::get_system_monospace_font())
+ glyphon::Family::SansSerif
}
};
attrs = attrs.family(family);
@@ -756,8 +728,7 @@ pub fn push_plate_bevel_vertices(
) {
let r = r.min(ww * 0.5).min(h * 0.5);
- let light_angle = crate::layout::light_source_position();
- let rad = light_angle.to_radians();
+ let rad = crate::layout::light_source_position();
let lx = rad.cos();
let ly = -rad.sin();
diff --git a/src/config.rs b/src/config.rs
index d65c7f3..d0e12cb 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -394,7 +394,7 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
};
if let Some(ref ext_ty) = existing_ty {
- if ext_ty.starts_with("menu:") || ext_ty == "button" || ext_ty.starts_with("button:") || ext_ty == "vec2i" {
+ if ext_ty.starts_with("menu:") || ext_ty == "button" || ext_ty.starts_with("button:") || ext_ty == "vec2i" || ext_ty == "radian" {
kdl_ty = Some(ext_ty.clone());
}
}
@@ -823,7 +823,9 @@ pub fn value_to_kdl_with_annotations(
let (val_str, val_ty) = match prop_val {
serde_json::Value::Bool(b) => (b.to_string(), Some("bool".to_string())),
serde_json::Value::Number(num) => {
- if num.is_f64() {
+ if prop_name == "light_source_position" {
+ (num.to_string(), Some("radian".to_string()))
+ } else if num.is_f64() {
(num.to_string(), Some("f64".to_string()))
} else {
(num.to_string(), Some("i64".to_string()))
@@ -881,7 +883,9 @@ pub fn value_to_kdl_with_annotations(
let (val_str, val_ty) = match val {
serde_json::Value::Bool(b) => (b.to_string(), Some("bool".to_string())),
serde_json::Value::Number(num) => {
- if num.is_f64() {
+ if key == "light_source_position" {
+ (num.to_string(), Some("radian".to_string()))
+ } else if num.is_f64() {
(num.to_string(), Some("f64".to_string()))
} else {
(num.to_string(), Some("i64".to_string()))
diff --git a/src/layout.rs b/src/layout.rs
index e3bdb7c..1b39ecc 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1446,7 +1446,12 @@ pub fn set_toggle_height(height: f32) {
}
pub fn light_source_position() -> f32 {
- get_style_registry().read().unwrap().get_float("light_source_position").unwrap_or(135.0)
+ let val = get_style_registry().read().unwrap().get_float("light_source_position").unwrap_or(2.3561945);
+ if val > 2.0 * std::f32::consts::PI {
+ val.to_radians()
+ } else {
+ val
+ }
}
pub fn bevel_depth() -> f32 {
@@ -5116,8 +5121,30 @@ pub struct VStack<'b, 'a, P> {
}
impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
- pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32, ctx: &mut UiContext) {
- self.context.widget(w, SectionContext::<P>::DEFAULT_MARGIN_X, ww, wh, ctx);
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, _ww: f32, wh: f32, ctx: &mut UiContext) {
+ let pad = self.context.padding();
+ let margin_x = 2.0 * pad + 12.0;
+ let x = self.context.left + margin_x;
+ let clamped_w = (self.context.cw - 2.0 * margin_x).max(0.0);
+
+ let mut max_h = self.context.grid.max_height().max(self.context.content_y);
+ if max_h > self.context.content_start_y {
+ max_h += self.context.row_gap;
+ }
+ let y = max_h;
+
+ let pref_h = w.preferred_height().unwrap_or(wh);
+ let top_room = crate::widget::label_offset(w);
+ let total_h = pref_h + top_room;
+
+ w.set_row_rect(self.context.left + pad, self.context.cw - 2.0 * pad);
+ render_widget(self.context.pc, w, x, y, clamped_w, total_h, ctx);
+
+ let new_bottom = y + total_h;
+ self.context.content_y = new_bottom;
+ for h in &mut self.context.grid.col_heights {
+ *h = new_bottom;
+ }
self.context.spacing(self.spacing);
}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 09089e8..5fdf72e 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -3,7 +3,6 @@ pub mod container_layout;
pub mod section_container;
pub mod header;
pub mod content_bg;
-pub mod viewport_bg;
pub mod parameters_bg;
pub mod menu;
pub mod breadcrumb;
@@ -28,7 +27,6 @@ pub use container_layout::{ContainerLayout, OverlayLayout, ManualLayout, Vertica
pub use section_container::SectionContainer;
pub use header::Header;
pub use content_bg::ContentBg;
-pub use viewport_bg::ViewportBg;
pub use parameters_bg::ParametersBg;
pub use menu::{Menu, MenuBar};
pub use breadcrumb::Breadcrumb;
diff --git a/src/widget/container/plate.rs b/src/widget/container/plate.rs
index 7589a17..b6f8166 100644
--- a/src/widget/container/plate.rs
+++ b/src/widget/container/plate.rs
@@ -239,7 +239,7 @@ impl Element for Plate {
self.base.base.x = clamped_x;
self.base.base.y = clamped_y;
self.base.base.w = clamped_w;
- self.base.base.h = clamped_h + label_off;
+ self.base.base.h = clamped_h;
if !self.base.visible {
return;
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
index f669b35..f24abc5 100644
--- a/src/widget/container/section_container.rs
+++ b/src/widget/container/section_container.rs
@@ -10,6 +10,7 @@ pub struct SectionContainer {
pub container: Container,
pub base: Widget,
pub parent: Option<*mut (dyn Element + 'static)>,
+ pub draw_children: bool,
}
impl SectionContainer {
@@ -19,6 +20,7 @@ impl SectionContainer {
container: Container::new(),
base: Widget::new(),
parent: None,
+ draw_children: true,
}
}
@@ -27,6 +29,15 @@ impl SectionContainer {
self.container = container.with_layout(layout);
self
}
+
+ pub fn with_draw_children(mut self, draw: bool) -> Self {
+ self.draw_children = draw;
+ self
+ }
+
+ pub fn set_draw_children(&mut self, draw: bool) {
+ self.draw_children = draw;
+ }
}
impl Default for SectionContainer {
@@ -114,35 +125,62 @@ impl Element for SectionContainer {
}
fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.draw_children {
+ return Vec::new();
+ }
let mut quads = self.header.all_quads(ctx);
quads.extend(self.container.all_quads(ctx));
quads
}
fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.draw_children {
+ return Vec::new();
+ }
let mut labels = self.header.text_labels();
labels.extend(self.container.text_labels());
labels
}
fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ if !self.draw_children {
+ return Vec::new();
+ }
let mut labels = self.header.text_labels_with_bounds(ctx);
labels.extend(self.container.text_labels_with_bounds(ctx));
labels
}
fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ if !self.draw_children {
+ return Vec::new();
+ }
let mut labels = self.header.text_labels_with_font_and_bounds(ctx);
labels.extend(self.container.text_labels_with_font_and_bounds(ctx));
labels
}
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.draw_children {
+ return Vec::new();
+ }
let mut items = self.header.get_text_items();
items.extend(self.container.get_text_items());
items
}
+ fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ if !self.draw_children {
+ return self.header.all_rounded_quads(ctx);
+ }
+ let mut quads = self.header.all_rounded_quads(ctx);
+ quads.extend(self.container.all_rounded_quads(ctx));
+ quads
+ }
+
fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
self.header.hit_test(px, py, ctx) || self.container.hit_test(px, py, ctx)
}
diff --git a/src/widget/container/viewport_bg.rs b/src/widget/container/viewport_bg.rs
deleted file mode 100644
index 94bde12..0000000
--- a/src/widget/container/viewport_bg.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-use crate::colors;
-use crate::widget::*;
-
-pub struct ViewportBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl ViewportBg {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Element for ViewportBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::VIEWPORT_BG }
- fn as_ptr(&self) -> *mut (dyn Element + 'static) {
- self as *const Self as *mut Self as *mut (dyn Element + 'static)
- }
- fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
- self as *mut Self as *mut (dyn Element + 'static)
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
-}
diff --git a/src/widget/display/mod.rs b/src/widget/display/mod.rs
index c0d9df4..7ed2505 100644
--- a/src/widget/display/mod.rs
+++ b/src/widget/display/mod.rs
@@ -42,3 +42,6 @@ pub use self::info_box::InfoBox;
pub use self::status_dot::{DotStatus, StatusDot};
pub use self::preview::{PreviewState, ImagePreviewData};
pub use self::text_sizer::{measure_text_width, measure_text};
+pub mod viewport_3d;
+pub use self::viewport_3d::Viewport3D;
+
diff --git a/src/widget/display/viewport_3d.rs b/src/widget/display/viewport_3d.rs
new file mode 100644
index 0000000..a88cae8
--- /dev/null
+++ b/src/widget/display/viewport_3d.rs
@@ -0,0 +1,306 @@
+use crate::colors;
+use crate::widget::*;
+use glam::{Mat4, Vec3};
+
+#[derive(Debug, Clone)]
+pub struct Viewport3D {
+ base: Widget,
+ pub rotation_x: f32,
+ pub rotation_y: f32,
+ pub zoom: f32,
+ pub active_camera: String,
+ pub bg_color: [f32; 3],
+ pub grid_color: [f32; 3],
+ pub show_grid: bool,
+ pub show_cube: bool,
+ pub show_origin: bool,
+ pub show_camera_pivot: bool,
+
+ // Pending rotation to be consumed by the application when not using the default camera
+ pub pending_yaw: f32,
+ pub pending_pitch: f32,
+
+ // Modifiers state
+ ctrl_pressed: bool,
+ shift_pressed: bool,
+ alt_pressed: bool,
+
+ // Drag & scroll state tracking
+ is_rotating: bool,
+ is_zooming: bool,
+ scroll_lock: u8,
+ rotate_accum_yaw: f32,
+ rotate_accum_pitch: f32,
+ zoom_accum: f32,
+ rotate_velocity_yaw: f32,
+ rotate_velocity_pitch: f32,
+ zoom_velocity: f32,
+ last_rotate_time: std::time::Instant,
+ last_zoom_time: std::time::Instant,
+ pub scroll_speed: f32,
+ pub inertial_scroll: bool,
+ pub scroll_friction: f32,
+}
+
+impl Viewport3D {
+ pub fn new() -> Self {
+ let base = Widget::new();
+ Self {
+ base,
+ rotation_x: 0.0,
+ rotation_y: 0.0,
+ zoom: 1.0,
+ active_camera: "Default Camera".to_string(),
+ bg_color: [0.10, 0.10, 0.13],
+ grid_color: [0.18, 0.18, 0.22],
+ show_grid: true,
+ show_cube: true,
+ show_origin: true,
+ show_camera_pivot: true,
+ pending_yaw: 0.0,
+ pending_pitch: 0.0,
+ ctrl_pressed: false,
+ shift_pressed: false,
+ alt_pressed: false,
+ is_rotating: false,
+ is_zooming: false,
+ scroll_lock: 0,
+ rotate_accum_yaw: 0.0,
+ rotate_accum_pitch: 0.0,
+ zoom_accum: 0.0,
+ rotate_velocity_yaw: 0.0,
+ rotate_velocity_pitch: 0.0,
+ zoom_velocity: 0.0,
+ last_rotate_time: std::time::Instant::now(),
+ last_zoom_time: std::time::Instant::now(),
+ scroll_speed: 1.0,
+ inertial_scroll: true,
+ scroll_friction: 0.90,
+ }
+ }
+
+ pub fn with_scroll_speed(mut self, speed: f32) -> Self {
+ self.scroll_speed = speed;
+ self
+ }
+
+ pub fn with_inertial_scroll(mut self, enabled: bool) -> Self {
+ self.inertial_scroll = enabled;
+ self
+ }
+
+ pub fn with_scroll_friction(mut self, friction: f32) -> Self {
+ self.scroll_friction = friction;
+ self
+ }
+
+ pub fn reset_velocity(&mut self) {
+ self.rotate_velocity_yaw = 0.0;
+ self.rotate_velocity_pitch = 0.0;
+ self.zoom_velocity = 0.0;
+ self.is_rotating = false;
+ self.is_zooming = false;
+ self.scroll_lock = 0;
+ }
+
+ pub fn get_matrices(&self, aspect: f32, custom_camera_pos: Option<Vec3>, custom_camera_rot: Option<Vec3>, custom_pivot: Option<Vec3>) -> (Mat4, Mat4, Mat4) {
+ let camera_pos = custom_camera_pos.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
+ let pivot = custom_pivot.unwrap_or(Vec3::ZERO);
+ let rot = custom_camera_rot.unwrap_or(Vec3::ZERO);
+ let rx = rot.x;
+ let ry = rot.y;
+ let rz = rot.z;
+
+ let base_offset = camera_pos - pivot;
+ let distance = base_offset.length();
+ let yaw0 = base_offset.x.atan2(base_offset.z);
+ let pitch0 = (base_offset.y / distance.max(1e-5)).asin();
+
+ let total_ry = ry.to_radians() + yaw0;
+ let total_rx = rx.to_radians() + pitch0;
+
+ let view_rot_pos = Mat4::from_rotation_y(total_ry) * Mat4::from_rotation_x(-total_rx);
+ let camera_up = view_rot_pos.transform_vector3(Vec3::Y);
+ let camera_world_pos = pivot + view_rot_pos.transform_vector3(Vec3::new(0.0, 0.0, distance) * self.zoom);
+ let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
+
+ let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
+ let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
+
+ (proj, view_mat, model)
+ }
+}
+
+impl Element for Viewport3D {
+ crate::impl_widget_base!(Viewport3D);
+
+ fn color(&self) -> [f32; 4] {
+ colors::VIEWPORT_BG
+ }
+
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ self.ctrl_pressed = ctrl;
+ self.shift_pressed = shift;
+ self.alt_pressed = alt;
+ }
+
+ fn hit_test(&self, px: f32, py: f32, _ctx: &UiContext) -> bool {
+ let (x, y, w, h) = self.rect();
+ px >= x && px <= x + w && py >= y && py <= y + h
+ }
+
+ fn mouse_wheel(&mut self, delta: &MouseScrollDelta, _px: f32, _py: f32, _ctx: &mut UiContext) -> bool {
+ let scale = crate::scale::scale_factor();
+ if self.ctrl_pressed {
+ match delta {
+ MouseScrollDelta::LineDelta(_x, y) => {
+ let dy = *y * 0.15 * self.scroll_speed;
+ self.zoom *= (-dy).exp();
+ self.zoom = self.zoom.clamp(0.05, 20.0);
+ self.is_zooming = false;
+
+ let dt = 0.016;
+ self.zoom_velocity = self.zoom_velocity * 0.4 + (dy / dt) * 0.6;
+ true
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let dy = (pos.y as f32 / scale) * 0.005 * self.scroll_speed;
+ self.zoom *= (-dy).exp();
+ self.zoom = self.zoom.clamp(0.05, 20.0);
+ self.is_zooming = true;
+ self.last_zoom_time = std::time::Instant::now();
+ self.zoom_accum += dy;
+ true
+ }
+ }
+ } else {
+ match delta {
+ MouseScrollDelta::LineDelta(x, y) => {
+ self.scroll_lock = 0;
+ let dx = *x * 0.05 * self.scroll_speed;
+ let dy = *y * 0.05 * self.scroll_speed;
+
+ if self.active_camera != "Default Camera" {
+ self.pending_yaw += dx;
+ self.pending_pitch += -dy;
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
+
+ self.is_rotating = false;
+ let dt = 0.016;
+ self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + (dx / dt) * 0.6;
+ self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + (-dy / dt) * 0.6;
+ true
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let mut dx = (pos.x as f32 / scale) * 0.005 * self.scroll_speed;
+ let mut dy = (pos.y as f32 / scale) * 0.005 * self.scroll_speed;
+
+ self.rotate_accum_yaw += dx;
+ self.rotate_accum_pitch -= dy;
+ if self.scroll_lock == 0 {
+ if self.rotate_accum_yaw.abs() > 0.002 || self.rotate_accum_pitch.abs() > 0.002 {
+ if self.rotate_accum_pitch.abs() > 1.2 * self.rotate_accum_yaw.abs() {
+ self.scroll_lock = 2;
+ } else if self.rotate_accum_yaw.abs() > 1.2 * self.rotate_accum_pitch.abs() {
+ self.scroll_lock = 1;
+ }
+ }
+ } else {
+ if self.scroll_lock == 1 {
+ dy = 0.0;
+ } else {
+ dx = 0.0;
+ }
+ }
+
+ if self.active_camera != "Default Camera" {
+ self.pending_yaw += dx;
+ self.pending_pitch += -dy;
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
+
+ self.is_rotating = true;
+ self.last_rotate_time = std::time::Instant::now();
+ true
+ }
+ }
+ }
+ }
+
+ fn tick(&mut self, dt: f32, _ctx: &mut UiContext) -> bool {
+ let now = std::time::Instant::now();
+ let mut changed = false;
+
+ if self.is_rotating {
+ if now.duration_since(self.last_rotate_time).as_secs_f32() > 0.05 {
+ self.is_rotating = false;
+ self.scroll_lock = 0;
+ } else if dt > 1e-5 {
+ let vel_yaw = self.rotate_accum_yaw / dt;
+ let vel_pitch = self.rotate_accum_pitch / dt;
+ self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + vel_yaw * 0.6;
+ self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + vel_pitch * 0.6;
+ }
+ self.rotate_accum_yaw = 0.0;
+ self.rotate_accum_pitch = 0.0;
+ }
+
+ if self.is_zooming {
+ if now.duration_since(self.last_zoom_time).as_secs_f32() > 0.05 {
+ self.is_zooming = false;
+ } else if dt > 1e-5 {
+ let vel_zoom = self.zoom_accum / dt;
+ self.zoom_velocity = self.zoom_velocity * 0.4 + vel_zoom * 0.6;
+ }
+ self.zoom_accum = 0.0;
+ }
+
+ if !self.is_rotating && (self.rotate_velocity_yaw.abs() > 0.001 || self.rotate_velocity_pitch.abs() > 0.001) {
+ if !self.inertial_scroll {
+ self.rotate_velocity_yaw = 0.0;
+ self.rotate_velocity_pitch = 0.0;
+ } else {
+ let dx = self.rotate_velocity_yaw * dt;
+ let dy = self.rotate_velocity_pitch * dt;
+
+ if self.active_camera != "Default Camera" {
+ self.pending_yaw += dx;
+ self.pending_pitch += dy;
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x += dy;
+ }
+
+ let decay = self.scroll_friction.powf(dt * 60.0);
+ self.rotate_velocity_yaw *= decay;
+ self.rotate_velocity_pitch *= decay;
+
+ if self.rotate_velocity_yaw.abs() < 0.01 { self.rotate_velocity_yaw = 0.0; }
+ if self.rotate_velocity_pitch.abs() < 0.01 { self.rotate_velocity_pitch = 0.0; }
+ changed = true;
+ }
+ }
+
+ if !self.is_zooming && self.zoom_velocity.abs() > 0.001 {
+ if !self.inertial_scroll {
+ self.zoom_velocity = 0.0;
+ } else {
+ let d_zoom = self.zoom_velocity * dt;
+ self.zoom *= (-d_zoom).exp();
+ self.zoom = self.zoom.clamp(0.05, 20.0);
+
+ let decay = self.scroll_friction.powf(dt * 60.0);
+ self.zoom_velocity *= decay;
+ if self.zoom_velocity.abs() < 0.01 { self.zoom_velocity = 0.0; }
+ changed = true;
+ }
+ }
+
+ changed
+ }
+}
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index ef520fb..2599d41 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -239,7 +239,7 @@ impl Element for Dropdown {
self.base.x = x;
self.base.y = y;
self.base.w = w;
- self.base.h = h + self.base.label_offset();
+ self.base.h = h;
}
fn get_value_string(&self) -> Option<String> {
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index f6b31af..dde0911 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -117,7 +117,7 @@ impl Element for Slider {
self.base.x = x;
self.base.y = y;
self.base.w = w;
- self.base.h = h + self.base.label_offset();
+ self.base.h = h;
}
fn widget_font(&self) -> Option<String> {
@@ -617,7 +617,7 @@ impl Element for RangeSlider {
self.base.x = x;
self.base.y = y;
self.base.w = w;
- self.base.h = h + self.base.label_offset();
+ self.base.h = h;
}
fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index b319ef9..00c3829 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -82,8 +82,8 @@ impl Element for Spinbox {
self.base.x = x;
self.base.y = y;
self.base.w = w;
- self.base.h = h + self.base.label_offset();
- }
+ self.base.h = h;
+ }
fn get_value_string(&self) -> Option<String> {
if self.decimals > 0 {
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 33a5fcc..734fa39 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -50,7 +50,7 @@ impl Element for Trackpad {
self.base.x = x;
self.base.y = y;
self.base.w = w;
- self.base.h = h + self.label_offset();
+ self.base.h = h;
}
fn color(&self) -> [f32; 4] {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 2011bad..2e8e43e 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -729,7 +729,7 @@ pub use self::input::{
pub use self::container::{
Container, ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
- SectionContainer, Header, ContentBg, ViewportBg, ParametersBg, List,
+ SectionContainer, Header, ContentBg, ParametersBg, List,
ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
Switcher, Layer, Page, Backplate, Paginator, ScrollBar, TreeList, TreeElement, ControlPanel
};
@@ -737,7 +737,8 @@ pub use self::display::{
TextLabel, Label, SectionHeader, StyledLabel, TextItem, Svg, UsageBar,
LayoutPreview, FontPreview, InfoBox, StatusDot, InteractiveListItem,
GraphNode, Graph, Float3, ProgressBar, StatusBar, Splitter, Node, Separator,
- DotStatus, PreviewLayoutMode, Sidebar, Panel, PreviewState, ImagePreviewData, serialize_widgets
+ DotStatus, PreviewLayoutMode, Sidebar, Panel, PreviewState, ImagePreviewData, serialize_widgets,
+ Viewport3D
};
pub trait PageSelector {