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,11 +36,13 @@ 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 navigation_header = vec![TextLabel::new("Navigation").italic(true).widget_instance()]; let 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_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![ let zoom_rate_label = vec![
@@ -92,11 +94,14 @@ impl PreferencesDialogMessageHandler {
.widget_instance(), .widget_instance(),
]; ];
rows.extend_from_slice(&[header, zoom_rate_label, zoom_rate, zoom_with_scroll]);
}
// ======= // =======
// EDITING // EDITING
// ======= // =======
{
let editing_header = vec![TextLabel::new("Editing").italic(true).widget_instance()]; let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
let selection_label = vec![ let selection_label = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(),
@@ -147,11 +152,52 @@ impl PreferencesDialogMessageHandler {
selection_mode, selection_mode,
]; ];
rows.extend_from_slice(&[header, selection_label, selection_mode]);
}
// ==========
// UI
// ==========
#[cfg(not(target_family = "wasm"))]
{
let header = vec![TextLabel::new("UI").italic(true).widget_instance()];
let scale_description = "Adjust the scale of the user interface (100 is default).";
let scale_label = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
];
let scale = vec![
Separator::new(SeparatorType::Unrelated).widget_instance(),
Separator::new(SeparatorType::Unrelated).widget_instance(),
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
.tooltip_label("Scale")
.tooltip_description(scale_description)
.mode_range()
.int()
.min(ui_scale_to_display(UI_SCALE_MIN))
.max(ui_scale_to_display(UI_SCALE_MAX))
.unit("%")
.on_update(|number_input: &NumberInput| {
if let Some(display_value) = number_input.value {
let scale = map_display_to_ui_scale(display_value);
PreferencesMessage::UIScale { scale }.into()
} else {
PreferencesMessage::UIScale { scale: UI_SCALE_DEFAULT }.into()
}
})
.widget_instance(),
];
rows.extend_from_slice(&[header, scale_label, scale]);
}
// ============ // ============
// EXPERIMENTAL // EXPERIMENTAL
// ============ // ============
{
let experimental_header = vec![TextLabel::new("Experimental").italic(true).widget_instance()]; let 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_section_description = "Configure the appearance of the wires running between node connections in the graph.";
let node_graph_wires_label = vec![ let node_graph_wires_label = vec![
@@ -253,33 +299,10 @@ impl PreferencesDialogMessageHandler {
.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,