Desktop: UI scale preference (#3475)

* ui scale preference

* cleanup

* add update ui scale message to SIDE_EFFECT_FREE_MESSAGES

* fix mac title bar height

* hide UI preference section on web

* set % as unit of ui scale
This commit is contained in:
Timon
2025-12-15 14:11:43 +00:00
committed by GitHub
parent e44f993095
commit 820865389c
12 changed files with 296 additions and 219 deletions
+7 -1
View File
@@ -27,6 +27,7 @@ pub(crate) struct App {
window_size: PhysicalSize<u32>, window_size: PhysicalSize<u32>,
window_maximized: bool, window_maximized: bool,
window_fullscreen: bool, window_fullscreen: bool,
ui_scale: f64,
app_event_receiver: Receiver<AppEvent>, app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler, app_event_scheduler: AppEventScheduler,
desktop_wrapper: DesktopWrapper, desktop_wrapper: DesktopWrapper,
@@ -83,6 +84,7 @@ impl App {
window_size: PhysicalSize { width: 0, height: 0 }, window_size: PhysicalSize { width: 0, height: 0 },
window_maximized: false, window_maximized: false,
window_fullscreen: false, window_fullscreen: false,
ui_scale: 1.,
app_event_receiver, app_event_receiver,
app_event_scheduler, app_event_scheduler,
desktop_wrapper: DesktopWrapper::new(), desktop_wrapper: DesktopWrapper::new(),
@@ -119,7 +121,7 @@ impl App {
} }
let size = window.surface_size(); let size = window.surface_size();
let scale = window.scale_factor(); let scale = window.scale_factor() * self.ui_scale;
let is_new_size = size != self.window_size; let is_new_size = size != self.window_size;
let is_new_scale = scale != self.window_scale; let is_new_scale = scale != self.window_scale;
@@ -228,6 +230,10 @@ impl App {
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]); render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
} }
} }
DesktopFrontendMessage::UpdateUIScale { scale } => {
self.ui_scale = scale;
self.resize();
}
DesktopFrontendMessage::UpdateOverlays(scene) => { DesktopFrontendMessage::UpdateOverlays(scene) => {
if let Some(render_state) = &mut self.render_state { if let Some(render_state) = &mut self.render_state {
render_state.set_overlays_scene(scene); render_state.set_overlays_scene(scene);
@@ -67,6 +67,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => { FrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
dispatcher.respond(DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height }); dispatcher.respond(DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height });
} }
FrontendMessage::UpdateUIScale { scale } => {
dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale });
return Some(FrontendMessage::UpdateUIScale { scale });
}
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => { FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument { dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
id: document_id, id: document_id,
+3
View File
@@ -35,6 +35,9 @@ pub enum DesktopFrontendMessage {
width: f64, width: f64,
height: f64, height: f64,
}, },
UpdateUIScale {
scale: f64,
},
UpdateOverlays(vello::Scene), UpdateOverlays(vello::Scene),
PersistenceWriteDocument { PersistenceWriteDocument {
id: DocumentId, id: DocumentId,
+5
View File
@@ -158,3 +158,8 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
// INPUT // INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
// UI
pub const UI_SCALE_DEFAULT: f64 = 1.;
pub const UI_SCALE_MIN: f64 = 0.5;
pub const UI_SCALE_MAX: f64 = 3.;
+1
View File
@@ -52,6 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
))), ))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
]; ];
/// Since we don't need to update the frontend multiple times per frame, /// Since we don't need to update the frontend multiple times per frame,
/// we have a set of messages which we will buffer until the next frame is requested. /// we have a set of messages which we will buffer until the next frame is requested.
@@ -1,4 +1,4 @@
use crate::consts::{VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE}; use crate::consts::{UI_SCALE_DEFAULT, UI_SCALE_MAX, UI_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE};
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
@@ -36,250 +36,273 @@ impl PreferencesDialogMessageHandler {
const TITLE: &'static str = "Editor Preferences"; const TITLE: &'static str = "Editor Preferences";
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
let mut rows = Vec::new();
// ========== // ==========
// NAVIGATION // NAVIGATION
// ========== // ==========
{
let header = vec![TextLabel::new("Navigation").italic(true).widget_instance()];
let navigation_header = vec![TextLabel::new("Navigation").italic(true).widget_instance()]; let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50).";
let zoom_rate_label = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
];
let zoom_rate = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
.tooltip_label("Zoom Rate")
.tooltip_description(zoom_rate_description)
.mode_range()
.int()
.min(1.)
.max(100.)
.on_update(|number_input: &NumberInput| {
if let Some(display_value) = number_input.value {
let actual_rate = map_display_to_zoom_rate(display_value);
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into()
} else {
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into()
}
})
.widget_instance(),
];
let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50)."; let checkbox_id = CheckboxId::new();
let zoom_rate_label = vec![ let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).";
Separator::new(SeparatorType::Unrelated).widget_instance(), let zoom_with_scroll = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
]; CheckboxInput::new(preferences.zoom_with_scroll)
let zoom_rate = vec![ .tooltip_label("Zoom with Scroll")
Separator::new(SeparatorType::Unrelated).widget_instance(), .tooltip_description(zoom_with_scroll_description)
Separator::new(SeparatorType::Unrelated).widget_instance(), .on_update(|checkbox_input: &CheckboxInput| {
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate))) PreferencesMessage::ModifyLayout {
.tooltip_label("Zoom Rate") zoom_with_scroll: checkbox_input.checked,
.tooltip_description(zoom_rate_description) }
.mode_range() .into()
.int() })
.min(1.) .for_label(checkbox_id)
.max(100.) .widget_instance(),
.on_update(|number_input: &NumberInput| { TextLabel::new("Zoom with Scroll")
if let Some(display_value) = number_input.value { .tooltip_label("Zoom with Scroll")
let actual_rate = map_display_to_zoom_rate(display_value); .tooltip_description(zoom_with_scroll_description)
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into() .for_checkbox(checkbox_id)
} else { .widget_instance(),
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into() ];
}
})
.widget_instance(),
];
let checkbox_id = CheckboxId::new(); rows.extend_from_slice(&[header, zoom_rate_label, zoom_rate, zoom_with_scroll]);
let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)."; }
let zoom_with_scroll = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
CheckboxInput::new(preferences.zoom_with_scroll)
.tooltip_label("Zoom with Scroll")
.tooltip_description(zoom_with_scroll_description)
.on_update(|checkbox_input: &CheckboxInput| {
PreferencesMessage::ModifyLayout {
zoom_with_scroll: checkbox_input.checked,
}
.into()
})
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Zoom with Scroll")
.tooltip_label("Zoom with Scroll")
.tooltip_description(zoom_with_scroll_description)
.for_checkbox(checkbox_id)
.widget_instance(),
];
// ======= // =======
// EDITING // EDITING
// ======= // =======
{
let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
let editing_header = vec![TextLabel::new("Editing").italic(true).widget_instance()]; let selection_label = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
TextLabel::new("Selection")
.tooltip_label("Selection")
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
.widget_instance(),
];
let selection_label = vec![ let selection_mode = RadioInput::new(vec![
Separator::new(SeparatorType::Unrelated).widget_instance(), RadioEntryData::new(SelectionMode::Touched.to_string())
Separator::new(SeparatorType::Unrelated).widget_instance(), .label(SelectionMode::Touched.to_string())
TextLabel::new("Selection") .tooltip_label(SelectionMode::Touched.to_string())
.tooltip_label("Selection") .tooltip_description(SelectionMode::Touched.tooltip_description())
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.") .on_update(move |_| {
.widget_instance(), PreferencesMessage::SelectionMode {
]; selection_mode: SelectionMode::Touched,
}
.into()
}),
RadioEntryData::new(SelectionMode::Enclosed.to_string())
.label(SelectionMode::Enclosed.to_string())
.tooltip_label(SelectionMode::Enclosed.to_string())
.tooltip_description(SelectionMode::Enclosed.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Enclosed,
}
.into()
}),
RadioEntryData::new(SelectionMode::Directional.to_string())
.label(SelectionMode::Directional.to_string())
.tooltip_label(SelectionMode::Directional.to_string())
.tooltip_description(SelectionMode::Directional.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Directional,
}
.into()
}),
])
.selected_index(Some(preferences.selection_mode as u32))
.widget_instance();
let selection_mode = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
selection_mode,
];
let selection_mode = RadioInput::new(vec![ rows.extend_from_slice(&[header, selection_label, selection_mode]);
RadioEntryData::new(SelectionMode::Touched.to_string()) }
.label(SelectionMode::Touched.to_string())
.tooltip_label(SelectionMode::Touched.to_string()) // ==========
.tooltip_description(SelectionMode::Touched.tooltip_description()) // UI
.on_update(move |_| { // ==========
PreferencesMessage::SelectionMode { #[cfg(not(target_family = "wasm"))]
selection_mode: SelectionMode::Touched, {
} let header = vec![TextLabel::new("UI").italic(true).widget_instance()];
.into()
}), let scale_description = "Adjust the scale of the user interface (100 is default).";
RadioEntryData::new(SelectionMode::Enclosed.to_string()) let scale_label = vec![
.label(SelectionMode::Enclosed.to_string()) Separator::new(SeparatorType::Unrelated).widget_instance(),
.tooltip_label(SelectionMode::Enclosed.to_string()) Separator::new(SeparatorType::Unrelated).widget_instance(),
.tooltip_description(SelectionMode::Enclosed.tooltip_description()) TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
.on_update(move |_| { ];
PreferencesMessage::SelectionMode { let scale = vec![
selection_mode: SelectionMode::Enclosed, Separator::new(SeparatorType::Unrelated).widget_instance(),
} Separator::new(SeparatorType::Unrelated).widget_instance(),
.into() NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
}), .tooltip_label("Scale")
RadioEntryData::new(SelectionMode::Directional.to_string()) .tooltip_description(scale_description)
.label(SelectionMode::Directional.to_string()) .mode_range()
.tooltip_label(SelectionMode::Directional.to_string()) .int()
.tooltip_description(SelectionMode::Directional.tooltip_description()) .min(ui_scale_to_display(UI_SCALE_MIN))
.on_update(move |_| { .max(ui_scale_to_display(UI_SCALE_MAX))
PreferencesMessage::SelectionMode { .unit("%")
selection_mode: SelectionMode::Directional, .on_update(|number_input: &NumberInput| {
} if let Some(display_value) = number_input.value {
.into() let scale = map_display_to_ui_scale(display_value);
}), PreferencesMessage::UIScale { scale }.into()
]) } else {
.selected_index(Some(preferences.selection_mode as u32)) PreferencesMessage::UIScale { scale: UI_SCALE_DEFAULT }.into()
.widget_instance(); }
let selection_mode = vec![ })
Separator::new(SeparatorType::Unrelated).widget_instance(), .widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(), ];
selection_mode,
]; rows.extend_from_slice(&[header, scale_label, scale]);
}
// ============ // ============
// EXPERIMENTAL // EXPERIMENTAL
// ============ // ============
{
let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
let experimental_header = vec![TextLabel::new("Experimental").italic(true).widget_instance()]; let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph.";
let node_graph_wires_label = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
TextLabel::new("Node Graph Wires")
.tooltip_label("Node Graph Wires")
.tooltip_description(node_graph_section_description)
.widget_instance(),
];
let graph_wire_style = RadioInput::new(vec![
RadioEntryData::new(GraphWireStyle::Direct.to_string())
.label(GraphWireStyle::Direct.to_string())
.tooltip_label(GraphWireStyle::Direct.to_string())
.tooltip_description(GraphWireStyle::Direct.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
.label(GraphWireStyle::GridAligned.to_string())
.tooltip_label(GraphWireStyle::GridAligned.to_string())
.tooltip_description(GraphWireStyle::GridAligned.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
])
.selected_index(Some(preferences.graph_wire_style as u32))
.widget_instance();
let graph_wire_style = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
graph_wire_style,
];
let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph."; let checkbox_id = CheckboxId::new();
let node_graph_wires_label = vec![ let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string();
Separator::new(SeparatorType::Unrelated).widget_instance(), #[cfg(target_family = "wasm")]
Separator::new(SeparatorType::Unrelated).widget_instance(), let mut vello_description = vello_description;
TextLabel::new("Node Graph Wires") #[cfg(target_family = "wasm")]
.tooltip_label("Node Graph Wires") vello_description.push_str("\n\n(Your browser must support WebGPU.)");
.tooltip_description(node_graph_section_description)
.widget_instance(),
];
let graph_wire_style = RadioInput::new(vec![
RadioEntryData::new(GraphWireStyle::Direct.to_string())
.label(GraphWireStyle::Direct.to_string())
.tooltip_label(GraphWireStyle::Direct.to_string())
.tooltip_description(GraphWireStyle::Direct.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
.label(GraphWireStyle::GridAligned.to_string())
.tooltip_label(GraphWireStyle::GridAligned.to_string())
.tooltip_description(GraphWireStyle::GridAligned.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
])
.selected_index(Some(preferences.graph_wire_style as u32))
.widget_instance();
let graph_wire_style = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
graph_wire_style,
];
let checkbox_id = CheckboxId::new(); let use_vello = vec![
let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string(); Separator::new(SeparatorType::Unrelated).widget_instance(),
#[cfg(target_family = "wasm")] Separator::new(SeparatorType::Unrelated).widget_instance(),
let mut vello_description = vello_description; CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
#[cfg(target_family = "wasm")] .tooltip_label("Vello Renderer")
vello_description.push_str("\n\n(Your browser must support WebGPU.)"); .tooltip_description(vello_description.clone())
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Vello Renderer")
.tooltip_label("Vello Renderer")
.tooltip_description(vello_description)
.disabled(!preferences.supports_wgpu())
.for_checkbox(checkbox_id)
.widget_instance(),
];
let use_vello = vec![ let checkbox_id = CheckboxId::new();
Separator::new(SeparatorType::Unrelated).widget_instance(), let vector_mesh_description = "
Separator::new(SeparatorType::Unrelated).widget_instance(),
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
.tooltip_label("Vello Renderer")
.tooltip_description(vello_description.clone())
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Vello Renderer")
.tooltip_label("Vello Renderer")
.tooltip_description(vello_description)
.disabled(!preferences.supports_wgpu())
.for_checkbox(checkbox_id)
.widget_instance(),
];
let checkbox_id = CheckboxId::new();
let vector_mesh_description = "
Allow the Pen tool to produce branching geometry, where more than two segments may be connected to one anchor point.\n\ Allow the Pen tool to produce branching geometry, where more than two segments may be connected to one anchor point.\n\
\n\ \n\
Currently, vector meshes do not properly render strokes (branching joins) and fills (multiple regions). Currently, vector meshes do not properly render strokes (branching joins) and fills (multiple regions).
" "
.trim(); .trim();
let vector_meshes = vec![ let vector_meshes = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
CheckboxInput::new(preferences.vector_meshes) CheckboxInput::new(preferences.vector_meshes)
.tooltip_label("Vector Meshes") .tooltip_label("Vector Meshes")
.tooltip_description(vector_mesh_description) .tooltip_description(vector_mesh_description)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into()) .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id) .for_label(checkbox_id)
.widget_instance(), .widget_instance(),
TextLabel::new("Vector Meshes") TextLabel::new("Vector Meshes")
.tooltip_label("Vector Meshes") .tooltip_label("Vector Meshes")
.tooltip_description(vector_mesh_description) .tooltip_description(vector_mesh_description)
.for_checkbox(checkbox_id) .for_checkbox(checkbox_id)
.widget_instance(), .widget_instance(),
]; ];
let checkbox_id = CheckboxId::new(); let checkbox_id = CheckboxId::new();
let brush_tool_description = " let brush_tool_description = "
Enable the Brush tool to support basic raster-based layer painting.\n\ Enable the Brush tool to support basic raster-based layer painting.\n\
\n\ \n\
This legacy tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing. This legacy tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing.
" "
.trim(); .trim();
let brush_tool = vec![ let brush_tool = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
CheckboxInput::new(preferences.brush_tool) CheckboxInput::new(preferences.brush_tool)
.tooltip_label("Brush Tool") .tooltip_label("Brush Tool")
.tooltip_description(brush_tool_description) .tooltip_description(brush_tool_description)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::BrushTool { enabled: checkbox_input.checked }.into()) .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::BrushTool { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id) .for_label(checkbox_id)
.widget_instance(), .widget_instance(),
TextLabel::new("Brush Tool") TextLabel::new("Brush Tool")
.tooltip_label("Brush Tool") .tooltip_label("Brush Tool")
.tooltip_description(brush_tool_description) .tooltip_description(brush_tool_description)
.for_checkbox(checkbox_id) .for_checkbox(checkbox_id)
.widget_instance(), .widget_instance(),
]; ];
Layout(vec![ rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, vector_meshes, brush_tool]);
// NAVIGATION }
LayoutGroup::Row { widgets: navigation_header },
// Navigation: Zoom Rate Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect())
LayoutGroup::Row { widgets: zoom_rate_label },
LayoutGroup::Row { widgets: zoom_rate },
// Navigation: Zoom with Scroll
LayoutGroup::Row { widgets: zoom_with_scroll },
//
// EDITING
LayoutGroup::Row { widgets: editing_header },
// Editing: Selection
LayoutGroup::Row { widgets: selection_label },
LayoutGroup::Row { widgets: selection_mode },
//
// EXPERIMENTAL
LayoutGroup::Row { widgets: experimental_header },
// Experimental: Node Graph Wires
LayoutGroup::Row { widgets: node_graph_wires_label },
LayoutGroup::Row { widgets: graph_wire_style },
// Experimental: Vello Renderer
LayoutGroup::Row { widgets: use_vello },
// Experimental: Vector Meshes
LayoutGroup::Row { widgets: vector_meshes },
// Experimental: Brush Tool
LayoutGroup::Row { widgets: brush_tool },
])
} }
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
@@ -351,3 +374,13 @@ fn map_zoom_rate_to_display(rate: f64) -> f64 {
let display = 50. + distance_from_reference; let display = 50. + distance_from_reference;
display.clamp(1., 100.).round() display.clamp(1., 100.).round()
} }
/// Maps display values in percent to actual ui scale.
fn map_display_to_ui_scale(display: f64) -> f64 {
display / 100.
}
/// Maps actual ui scale back to display values in percent.
fn ui_scale_to_display(scale: f64) -> f64 {
scale * 100.
}
@@ -338,6 +338,9 @@ pub enum FrontendMessage {
width: f64, width: f64,
height: f64, height: f64,
}, },
UpdateUIScale {
scale: f64,
},
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
RenderOverlays { RenderOverlays {
@@ -17,4 +17,5 @@ pub enum PreferencesMessage {
ModifyLayout { zoom_with_scroll: bool }, ModifyLayout { zoom_with_scroll: bool },
GraphWireStyle { style: GraphWireStyle }, GraphWireStyle { style: GraphWireStyle },
ViewportZoomWheelRate { rate: f64 }, ViewportZoomWheelRate { rate: f64 },
UIScale { scale: f64 },
} }
@@ -1,4 +1,4 @@
use crate::consts::VIEWPORT_ZOOM_WHEEL_RATE; use crate::consts::{UI_SCALE_DEFAULT, VIEWPORT_ZOOM_WHEEL_RATE};
use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
@@ -14,6 +14,7 @@ pub struct PreferencesMessageHandler {
pub brush_tool: bool, pub brush_tool: bool,
pub graph_wire_style: GraphWireStyle, pub graph_wire_style: GraphWireStyle,
pub viewport_zoom_wheel_rate: f64, pub viewport_zoom_wheel_rate: f64,
pub ui_scale: f64,
} }
impl PreferencesMessageHandler { impl PreferencesMessageHandler {
@@ -42,6 +43,7 @@ impl Default for PreferencesMessageHandler {
brush_tool: false, brush_tool: false,
graph_wire_style: GraphWireStyle::default(), graph_wire_style: GraphWireStyle::default(),
viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE, viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE,
ui_scale: UI_SCALE_DEFAULT,
} }
} }
} }
@@ -61,6 +63,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
responses.add(PreferencesMessage::ModifyLayout { responses.add(PreferencesMessage::ModifyLayout {
zoom_with_scroll: self.zoom_with_scroll, zoom_with_scroll: self.zoom_with_scroll,
}); });
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
} }
PreferencesMessage::ResetToDefaults => { PreferencesMessage::ResetToDefaults => {
refresh_dialog(responses); refresh_dialog(responses);
@@ -99,6 +102,10 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
PreferencesMessage::ViewportZoomWheelRate { rate } => { PreferencesMessage::ViewportZoomWheelRate { rate } => {
self.viewport_zoom_wheel_rate = rate; self.viewport_zoom_wheel_rate = rate;
} }
PreferencesMessage::UIScale { scale } => {
self.ui_scale = scale;
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
}
} }
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
@@ -17,6 +17,9 @@
let menuBarLayout: Layout = []; let menuBarLayout: Layout = [];
// On mac menu bar needs to be scaled with inverse of UI scale to match native menu buttons.
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => { editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
patchLayout(menuBarLayout, updateMenuBarLayout); patchLayout(menuBarLayout, updateMenuBarLayout);
@@ -25,7 +28,7 @@
}); });
</script> </script>
<LayoutRow class="title-bar"> <LayoutRow class="title-bar" styles={{ height: height + "px" }}>
<!-- Menu bar --> <!-- Menu bar -->
<LayoutRow> <LayoutRow>
{#if $appWindow.platform !== "Mac"} {#if $appWindow.platform !== "Mac"}
@@ -48,7 +51,6 @@
<style lang="scss" global> <style lang="scss" global>
.title-bar { .title-bar {
height: 28px;
flex: 0 0 auto; flex: 0 0 auto;
> .layout-row { > .layout-row {
+5
View File
@@ -319,6 +319,10 @@ export class UpdateViewportPhysicalBounds extends JsMessage {
readonly height!: number; readonly height!: number;
} }
export class UpdateUIScale extends JsMessage {
readonly scale!: number;
}
// Rust enum `Key` // Rust enum `Key`
export type KeyRaw = string; export type KeyRaw = string;
// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key // Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key
@@ -1766,6 +1770,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateToolShelfLayout, UpdateToolShelfLayout,
UpdateViewportHolePunch, UpdateViewportHolePunch,
UpdateViewportPhysicalBounds, UpdateViewportPhysicalBounds,
UpdateUIScale,
UpdateVisibleNodes, UpdateVisibleNodes,
UpdateWelcomeScreenButtonsLayout, UpdateWelcomeScreenButtonsLayout,
UpdateWirePathInProgress, UpdateWirePathInProgress,
+8 -1
View File
@@ -1,7 +1,7 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import { type Editor } from "@graphite/editor";
import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen } from "@graphite/messages"; import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen, UpdateUIScale } from "@graphite/messages";
export function createAppWindowState(editor: Editor) { export function createAppWindowState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -9,6 +9,7 @@ export function createAppWindowState(editor: Editor) {
maximized: false, maximized: false,
fullscreen: false, fullscreen: false,
viewportHolePunch: false, viewportHolePunch: false,
uiScale: 1.0,
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation
@@ -36,6 +37,12 @@ export function createAppWindowState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateUIScale, (uiScale) => {
update((state) => {
state.uiScale = uiScale.scale;
return state;
});
});
return { return {
subscribe, subscribe,