This commit is contained in:
Victor Tran 2025-08-26 01:41:38 +02:00 committed by GitHub
commit ef7ae7ca76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 599 additions and 148 deletions

View file

@ -1,5 +1,5 @@
use gpui::{
App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
App, Context, EventEmitter, IntoElement, LayoutDirection, PlatformDisplay, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
linear_color_stop, linear_gradient, point,
};
@ -61,6 +61,7 @@ impl AgentNotification {
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
window_min_size: None,
layout_direction: LayoutDirection::LeftToRight,
window_decorations: Some(WindowDecorations::Client),
}
}

View file

@ -9,7 +9,7 @@ use std::{rc::Rc, sync::Arc};
pub use collab_panel::CollabPanel;
use gpui::{
App, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
App, LayoutDirection, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
WindowDecorations, WindowKind, WindowOptions, point,
};
use panel_settings::MessageEditorSettings;
@ -64,6 +64,7 @@ fn notification_window_options(
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
layout_direction: LayoutDirection::LeftToRight,
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
}

View file

@ -310,3 +310,7 @@ path = "examples/window_shadow.rs"
[[example]]
name = "grid_layout"
path = "examples/grid_layout.rs"
[[example]]
name = "bidi"
path = "examples/bidi.rs"

View file

@ -0,0 +1,149 @@
use gpui::{
App, Application, Bounds, Context, KeyBinding, LayoutDirection, Menu, MenuItem, Window,
WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
#[derive(IntoElement)]
struct BidiExampleComponent {
header: &'static str,
sub_title: &'static str,
content: &'static str,
}
impl RenderOnce for BidiExampleComponent {
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
let main_color = rgb(0xF0F0F3);
div()
.flex()
.flex_col()
.w_full()
.gap_2()
.child(div().text_3xl().child(self.header))
.child(self.sub_title)
.child(
div()
.border_r_1()
.border_color(main_color)
.pr_1()
.flex_shrink()
.child(self.content),
)
.child(
div()
.w_full()
.flex()
.gap_1()
.child(
div()
.border_1()
.p_1()
.border_color(main_color)
.child("Child 1"),
)
.child(
div()
.border_1()
.p_1()
.border_color(main_color)
.child("Child 2"),
)
.child(
div()
.border_1()
.p_1()
.border_color(main_color)
.child("Child 3"),
),
)
.child(div().child(format!(
"window.current_layout_direction(): {:?}",
window.current_layout_direction()
)))
}
}
struct BidiView;
impl Render for BidiView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let main_color = rgb(0xF0F0F3);
div()
.id("bidi-root")
.overflow_y_scroll()
.bg(rgb(0x0c0c11))
.text_color(main_color)
.flex()
.w_full()
.h_full()
.flex_col()
.p(px(20.0))
.gap(px(10.0))
.child(BidiExampleComponent {
header: "This div uses the window's default window direction!",
sub_title: "Try changing layout_direction in the example code's WindowOptions!",
content: "This div has a border and padding on its right side, but it's \
rendered in RTL, so it shows up on the left instead. Margins are \
also automatically switched based on the layout direction.",
})
.child(div().w_full().dir_ltr().child(BidiExampleComponent {
header: "This div is manually set to left-to-right!",
sub_title: "Except for the strings, the code for these elements are the exact \
as the RTL example! Directionality propagates to child \
elements, but you can always set children to a different \
directionality with dir_rtl() or dir_ltr().",
content: "This div has the border and padding on the right side, and it's \
displayed on the right side, as the directionality for the \
parent is set to left-to-right.",
}))
.child(
div()
.w_full()
.dir_ltr()
.child("If you're on macOS, the menu items are also rendered in RTL."),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
// Set the layout direction for anything isolated from a window, for example,
// the menu bar on macOS
cx.set_default_layout_direction(LayoutDirection::RightToLeft);
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("secondary-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Bidirectionality Example".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Set the default layout direction for the window. Setting this to RTL will
// cause the window controls to be drawn RTL on supported platforms.
// NOTE: On macOS, the default window controls are always drawn RTL when the system
// language is RTL, and LTR otherwise. When using client-side decorations, as long
// as you set titlebar.traffic_light_position, GPUI will not respect this and will
// draw the window controls in the layout direction set here.
layout_direction: LayoutDirection::RightToLeft,
..Default::default()
},
|_, cx| cx.new(|_| BidiView),
)
.unwrap();
cx.activate(true);
});
}
// Associate actions using the `actions!` macro (or `Action` derive macro)
actions!(menu_actions, [Quit]);
// Define the quit function that is registered with the App
fn quit(_: &Quit, cx: &mut App) {
println!("Gracefully quitting the application . . .");
cx.quit();
}

View file

@ -1,7 +1,7 @@
use gpui::{
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
px, rgb,
App, Application, Bounds, Context, DisplayId, Hsla, LayoutDirection, Pixels, SharedString,
Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point,
prelude::*, px, rgb,
};
struct WindowContent {
@ -60,6 +60,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
layout_direction: LayoutDirection::LeftToRight,
window_min_size: None,
window_decorations: None,
}

View file

@ -36,11 +36,11 @@ use crate::{
Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset,
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
WindowHandle, WindowId, WindowInvalidator,
Keymap, Keystroke, LayoutDirection, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton,
PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation,
ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@ -1539,6 +1539,17 @@ impl App {
self.platform.get_menus()
}
/// Sets the default layout direction for elements that are not tied to a window,
/// for example, menus on macOS
pub fn set_default_layout_direction(&self, direction: LayoutDirection) {
self.platform.set_default_layout_direction(direction)
}
/// Gets the default layout direction for elements that are not tied to a window
pub fn get_default_layout_direction(&self) -> LayoutDirection {
self.platform.get_default_layout_direction()
}
/// Sets the right click menu for the app icon in the dock
pub fn set_dock_menu(&self, menus: Vec<MenuItem>) {
self.platform.set_dock_menu(menus, &self.keymap.borrow())

View file

@ -1294,13 +1294,15 @@ impl Element for Div {
window,
cx,
|style, window, cx| {
window.with_text_style(style.text_style().cloned(), |window| {
child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
window.request_layout(style, child_layout_ids.iter().copied(), cx)
window.with_bidi_style(style.bidi_style().cloned(), |window| {
window.with_text_style(style.text_style().cloned(), |window| {
child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
window.request_layout(style, child_layout_ids.iter().copied(), cx)
})
})
},
)
@ -1616,22 +1618,24 @@ impl Interactivity {
}
}
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
let hitbox = if self.should_insert_hitbox(&style, window, cx) {
Some(window.insert_hitbox(bounds, self.hitbox_behavior))
} else {
None
};
window.with_bidi_style(style.bidi_style().cloned(), |window| {
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
let hitbox = if self.should_insert_hitbox(&style, window, cx) {
Some(window.insert_hitbox(bounds, self.hitbox_behavior))
} else {
None
};
let scroll_offset =
self.clamp_scroll_position(bounds, &style, window, cx);
let result = f(&style, scroll_offset, hitbox, window, cx);
(result, element_state)
},
)
let scroll_offset =
self.clamp_scroll_position(bounds, &style, window, cx);
let result = f(&style, scroll_offset, hitbox, window, cx);
(result, element_state)
},
)
})
})
},
)
@ -1763,61 +1767,65 @@ impl Interactivity {
window.with_element_opacity(style.opacity, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(
global_id, hitbox, &style, window, cx,
);
window.with_bidi_style(style.bidi_style().cloned(), |window| {
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(
global_id, hitbox, &style, window, cx,
);
if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style {
window.set_window_cursor_style(mouse_cursor);
if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style {
window.set_window_cursor_style(mouse_cursor);
}
} else {
if let Some(mouse_cursor) = style.mouse_cursor {
window.set_cursor_style(mouse_cursor, hitbox);
}
}
} else {
if let Some(mouse_cursor) = style.mouse_cursor {
window.set_cursor_style(mouse_cursor, hitbox);
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
if let Some(area) = self.window_control {
window.insert_window_control_hitbox(
area,
hitbox.clone(),
);
}
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
window,
cx,
);
self.paint_scroll_listener(hitbox, &style, window, cx);
}
self.paint_keyboard_listeners(window, cx);
f(&style, window, cx);
if let Some(_hitbox) = hitbox {
#[cfg(any(feature = "inspector", debug_assertions))]
window.insert_inspector_hitbox(
_hitbox.id,
_inspector_id,
cx,
);
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
if let Some(area) = self.window_control {
window
.insert_window_control_hitbox(area, hitbox.clone());
}
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
window,
cx,
);
self.paint_scroll_listener(hitbox, &style, window, cx);
}
self.paint_keyboard_listeners(window, cx);
f(&style, window, cx);
if let Some(_hitbox) = hitbox {
#[cfg(any(feature = "inspector", debug_assertions))]
window.insert_inspector_hitbox(
_hitbox.id,
_inspector_id,
cx,
);
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
},
);
},
);
});
});
});
});
@ -1933,15 +1941,17 @@ impl Interactivity {
}
};
window.with_text_style(
Some(crate::TextStyleRefinement {
color: Some(crate::red()),
line_height: Some(FONT_SIZE.into()),
background_color: Some(crate::white()),
..Default::default()
}),
render_debug_text,
)
window.with_bidi_style(None, |window| {
window.with_text_style(
Some(crate::TextStyleRefinement {
color: Some(crate::red()),
line_height: Some(FONT_SIZE.into()),
background_color: Some(crate::white()),
..Default::default()
}),
render_debug_text,
)
})
}
}
@ -2497,7 +2507,14 @@ impl Interactivity {
}
}
style
window.with_bidi_style(self.base_style.bidi.clone(), |window| {
let layout_direction = window.current_layout_direction();
let flex_mapped_style = layout_direction.apply_flex_direction(style);
let spacing_mapped_style = layout_direction.apply_spacing_direction(flex_mapped_style);
let border_mapped_style = layout_direction.apply_border_direction(spacing_mapped_style);
border_mapped_style
})
}
}

View file

@ -442,11 +442,14 @@ impl TextLayout {
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let text_style = window.text_style();
let text_align = window
.current_layout_direction()
.apply_text_align_direction(text_style.text_align);
for line in &element_state.lines {
line.paint_background(
line_origin,
line_height,
text_style.text_align,
text_align,
Some(bounds),
window,
cx,
@ -455,7 +458,7 @@ impl TextLayout {
line.paint(
line_origin,
line_height,
text_style.text_align,
text_align,
Some(bounds),
window,
cx,

View file

@ -38,10 +38,10 @@ pub(crate) mod scap_screen_capture;
use crate::{
Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LayoutDirection, LineLayout,
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage, RenderImageParams,
RenderSvgParams, ScaledPixels, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
SvgSize, Task, TaskLabel, Window, WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
@ -238,6 +238,9 @@ pub(crate) trait Platform: 'static {
None
}
fn set_default_layout_direction(&self, direction: LayoutDirection);
fn get_default_layout_direction(&self) -> LayoutDirection;
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
fn perform_dock_menu_action(&self, _action: usize) {}
fn add_recent_document(&self, _path: &Path) {}
@ -1105,6 +1108,9 @@ pub struct WindowOptions {
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
/// The layout direction
pub layout_direction: LayoutDirection,
}
/// The variables that can be configured when creating a new window
@ -1144,6 +1150,9 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_min_size: Option<Size<Pixels>>,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub layout_direction: LayoutDirection,
}
/// Represents the status of how a window should be opened.
@ -1194,6 +1203,7 @@ impl Default for WindowOptions {
app_id: None,
window_min_size: None,
window_decorations: None,
layout_direction: LayoutDirection::LeftToRight,
}
}
}

View file

@ -24,9 +24,9 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
Point, Result, Task, WindowAppearance, WindowParams, px,
ForegroundExecutor, Keymap, LayoutDirection, LinuxDispatcher, Menu, MenuItem, OwnedMenu,
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@ -95,6 +95,7 @@ pub(crate) struct LinuxCommon {
pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>,
pub(crate) default_layout_direction: LayoutDirection,
}
impl LinuxCommon {
@ -120,6 +121,7 @@ impl LinuxCommon {
auto_hide_scrollbars: false,
callbacks,
signal,
default_layout_direction: LayoutDirection::RightToLeft,
menus: Vec::new(),
};
@ -563,6 +565,16 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn add_recent_document(&self, _path: &Path) {}
fn set_default_layout_direction(&self, direction: LayoutDirection) {
self.with_common(|common| {
common.default_layout_direction = direction;
})
}
fn get_default_layout_direction(&self) -> LayoutDirection {
self.with_common(|common| common.default_layout_direction)
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]

View file

@ -6,9 +6,9 @@ use super::{
};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, LayoutDirection,
MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions,
Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
@ -171,6 +171,7 @@ pub(crate) struct MacPlatformState {
finish_launching: Option<Box<dyn FnOnce()>>,
dock_menu: Option<id>,
menus: Option<Vec<OwnedMenu>>,
default_layout_direction: LayoutDirection,
}
impl Default for MacPlatform {
@ -209,6 +210,7 @@ impl MacPlatform {
dock_menu: None,
on_keyboard_layout_change: None,
menus: None,
default_layout_direction: LayoutDirection::LeftToRight,
}))
}
@ -232,16 +234,21 @@ impl MacPlatform {
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keymap: &Keymap,
layout_direction: LayoutDirection,
) -> id {
let layout_direction = Self::ns_user_interface_layout_direction(layout_direction);
unsafe {
let application_menu = NSMenu::new(nil).autorelease();
application_menu.setDelegate_(delegate);
let _: () =
msg_send![application_menu, setUserInterfaceLayoutDirection: layout_direction];
for menu_config in menus {
let menu = NSMenu::new(nil).autorelease();
let menu_title = ns_string(&menu_config.name);
menu.setTitle_(menu_title);
menu.setDelegate_(delegate);
let _: () = msg_send![menu, setUserInterfaceLayoutDirection: layout_direction];
for item_config in &menu_config.items {
menu.addItem_(Self::create_menu_item(
@ -446,6 +453,13 @@ impl MacPlatform {
version.patchVersion as usize,
)
}
fn ns_user_interface_layout_direction(layout_direction: LayoutDirection) -> usize {
match layout_direction {
LayoutDirection::LeftToRight => 0,
LayoutDirection::RightToLeft => 1,
}
}
}
impl Platform for MacPlatform {
@ -894,8 +908,15 @@ impl Platform for MacPlatform {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.lock();
let layout_direction = state.default_layout_direction;
let actions = &mut state.menu_actions;
let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
let menu = self.create_menu_bar(
&menus,
NSWindow::delegate(app),
actions,
keymap,
layout_direction,
);
drop(state);
app.setMainMenu_(menu);
}
@ -1219,6 +1240,16 @@ impl Platform for MacPlatform {
Ok(())
})
}
fn set_default_layout_direction(&self, direction: LayoutDirection) {
let mut state = self.0.lock();
state.default_layout_direction = direction;
}
fn get_default_layout_direction(&self) -> LayoutDirection {
let state = self.0.lock();
state.default_layout_direction
}
}
impl MacPlatform {

View file

@ -1,11 +1,12 @@
use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
use crate::{
AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent,
ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
ForegroundExecutor, KeyDownEvent, Keystroke, LayoutDirection, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel,
RequestFrameOptions, ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler,
point, px, size,
};
use block::ConcreteBlock;
use cocoa::{
@ -367,6 +368,7 @@ struct MacWindowState {
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
layout_direction: LayoutDirection,
transparent_titlebar: bool,
previous_modifiers_changed_event: Option<PlatformInput>,
keystroke_for_do_command: Option<Keystroke>,
@ -406,13 +408,24 @@ impl MacWindowState {
let mut min_button_frame: CGRect = msg_send![min_button, frame];
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
let mut origin = point(
traffic_light_position.x,
match self.layout_direction {
LayoutDirection::LeftToRight => traffic_light_position.x,
LayoutDirection::RightToLeft => {
self.window_bounds().get_bounds().size.width
- traffic_light_position.x
- px(close_button_frame.size.width as f32)
}
},
titlebar_height
- traffic_light_position.y
- px(close_button_frame.size.height as f32),
);
let button_spacing =
px((min_button_frame.origin.x - close_button_frame.origin.x) as f32);
px(((min_button_frame.origin.x - close_button_frame.origin.x) as f32).abs())
* match self.layout_direction {
LayoutDirection::LeftToRight => 1.,
LayoutDirection::RightToLeft => -1.,
};
close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
let _: () = msg_send![close_button, setFrame: close_button_frame];
@ -534,6 +547,7 @@ impl MacWindow {
show,
display_id,
window_min_size,
layout_direction,
}: WindowParams,
executor: ForegroundExecutor,
renderer_context: renderer::Context,
@ -660,6 +674,7 @@ impl MacWindow {
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
layout_direction: layout_direction,
})));
(*native_window).set_ivar(

View file

@ -1,8 +1,9 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
ForegroundExecutor, Keymap, LayoutDirection, NoopTextSystem, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, TestDisplay, TestWindow,
WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@ -38,6 +39,7 @@ pub(crate) struct TestPlatform {
#[cfg(target_os = "windows")]
bitmap_factory: std::mem::ManuallyDrop<IWICImagingFactory>,
weak: Weak<Self>,
default_layout_direction: RefCell<LayoutDirection>,
}
#[derive(Clone)]
@ -119,6 +121,7 @@ impl TestPlatform {
#[cfg(target_os = "windows")]
bitmap_factory,
text_system,
default_layout_direction: Default::default(),
})
}
@ -426,6 +429,14 @@ impl Platform for TestPlatform {
fn open_with_system(&self, _path: &Path) {
unimplemented!()
}
fn set_default_layout_direction(&self, direction: LayoutDirection) {
*self.default_layout_direction.borrow_mut() = direction
}
fn get_default_layout_direction(&self) -> LayoutDirection {
*self.default_layout_direction.borrow()
}
}
impl TestScreenCaptureSource {

View file

@ -54,6 +54,7 @@ pub(crate) struct WindowsPlatformState {
jump_list: JumpList,
// NOTE: standard cursor handles don't need to close.
pub(crate) current_cursor: Option<HCURSOR>,
default_layout_direction: LayoutDirection,
}
#[derive(Default)]
@ -77,6 +78,7 @@ impl WindowsPlatformState {
callbacks,
jump_list,
current_cursor,
default_layout_direction: LayoutDirection::RightToLeft,
menus: Vec::new(),
}
}
@ -707,6 +709,14 @@ impl Platform for WindowsPlatform {
) -> Vec<SmallVec<[PathBuf; 2]>> {
self.update_jump_list(menus, entries)
}
fn set_default_layout_direction(&self, direction: LayoutDirection) {
self.state.borrow_mut().default_layout_direction = direction;
}
fn get_default_layout_direction(&self) -> LayoutDirection {
self.state.borrow().default_layout_direction
}
}
impl Drop for WindowsPlatform {

View file

@ -1,5 +1,13 @@
#![deny(unsafe_op_in_unsafe_fn)]
use crate::*;
use ::util::ResultExt;
use anyhow::{Context as _, Result};
use async_task::Runnable;
use futures::channel::oneshot::{self, Receiver};
use raw_window_handle as rwh;
use smallvec::SmallVec;
use std::ffi::c_void;
use std::{
cell::RefCell,
num::NonZeroIsize,
@ -9,13 +17,7 @@ use std::{
sync::{Arc, Once},
time::{Duration, Instant},
};
use ::util::ResultExt;
use anyhow::{Context as _, Result};
use async_task::Runnable;
use futures::channel::oneshot::{self, Receiver};
use raw_window_handle as rwh;
use smallvec::SmallVec;
use windows::Win32::Graphics::Dwm::{DWMWA_NONCLIENT_RTL_LAYOUT, DwmSetWindowAttribute};
use windows::{
Win32::{
Foundation::*,
@ -26,8 +28,6 @@ use windows::{
core::*,
};
use crate::*;
pub(crate) struct WindowsWindow(pub Rc<WindowsWindowInner>);
pub struct WindowsWindowState {
@ -167,7 +167,7 @@ impl WindowsWindowState {
fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) {
let placement = unsafe {
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
length: size_of::<WINDOWPLACEMENT>() as u32,
..Default::default()
};
GetWindowPlacement(self.hwnd, &mut placement).log_err();
@ -438,6 +438,18 @@ impl WindowsWindow {
let this = context.inner.take().unwrap()?;
let hwnd = creation_result?;
if params.layout_direction == LayoutDirection::RightToLeft {
let mut true_var = 1_i32;
unsafe {
DwmSetWindowAttribute(
hwnd,
DWMWA_NONCLIENT_RTL_LAYOUT,
&mut true_var as *mut _ as *mut c_void,
size_of::<i32>() as u32,
)?;
}
}
register_drag_drop(&this)?;
configure_dwm_dark_mode(hwnd, appearance);
this.state.borrow_mut().border_offset.update(hwnd)?;
@ -600,7 +612,7 @@ impl PlatformWindow for WindowsWindow {
.spawn(async move {
unsafe {
let mut config = TASKDIALOGCONFIG::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _;
config.cbSize = size_of::<TASKDIALOGCONFIG>() as _;
config.hwndParent = handle;
let title;
let main_icon;
@ -1271,7 +1283,7 @@ fn retrieve_window_placement(
border_offset: WindowBorderOffset,
) -> Result<WINDOWPLACEMENT> {
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
length: size_of::<WINDOWPLACEMENT>() as u32,
..Default::default()
};
unsafe { GetWindowPlacement(hwnd, &mut placement)? };
@ -1321,7 +1333,7 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
let mut data = WINDOWCOMPOSITIONATTRIBDATA {
attrib: 0x13,
pv_data: &accent as *const _ as *mut _,
cb_data: std::mem::size_of::<AccentPolicy>(),
cb_data: size_of::<AccentPolicy>(),
};
let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
}

View file

@ -1,4 +1,5 @@
use std::{
fmt::Debug,
hash::{Hash, Hasher},
iter, mem,
ops::Range,
@ -254,6 +255,9 @@ pub struct Style {
/// The text style of this element
pub text: TextStyleRefinement,
/// The bidi style of this element
pub bidi: BidiStyleRefinement,
/// The mouse cursor style shown when the mouse pointer is over an element.
pub mouse_cursor: Option<CursorStyle>,
@ -496,6 +500,86 @@ impl TextStyle {
}
}
/// The direction of layout.
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
pub enum LayoutDirection {
/// Left-to-right layout.
#[default]
LeftToRight,
/// Right-to-left layout.
RightToLeft,
}
impl LayoutDirection {
/// Sets the flex direction on a style, given the bidi direction
pub fn apply_flex_direction(self, mut style: Style) -> Style {
match self {
LayoutDirection::LeftToRight => style,
LayoutDirection::RightToLeft => {
style.flex_direction = style.flex_direction.flip_horizontal();
style
}
}
}
/// Sets the margin and padding orientations on a style, given the bidi direction
pub fn apply_spacing_direction(self, mut style: Style) -> Style {
match self {
LayoutDirection::LeftToRight => style,
LayoutDirection::RightToLeft => {
style.margin = self.apply_edge_direction(style.margin);
style.padding = self.apply_edge_direction(style.padding);
style
}
}
}
/// Sets the border orientations on a style, given the bidi direction
pub fn apply_border_direction(self, mut style: Style) -> Style {
match self {
LayoutDirection::LeftToRight => style,
LayoutDirection::RightToLeft => {
style.border_widths = self.apply_edge_direction(style.border_widths);
style
}
}
}
/// Sets the flex direction on a TextAlign, given the bidi direction
pub fn apply_text_align_direction(self, mut text_align: TextAlign) -> TextAlign {
match self {
LayoutDirection::LeftToRight => text_align,
LayoutDirection::RightToLeft => match text_align {
TextAlign::Left => TextAlign::Right,
TextAlign::Center => TextAlign::Center,
TextAlign::Right => TextAlign::Left,
},
}
}
/// Maps a set of edges to the correct orientation based on the bidi direction
pub fn apply_edge_direction<T>(self, mut edge: Edges<T>) -> Edges<T>
where
T: Clone + Debug + Default + PartialEq,
{
match self {
LayoutDirection::LeftToRight => edge,
LayoutDirection::RightToLeft => {
std::mem::swap(&mut edge.left, &mut edge.right);
edge
}
}
}
}
/// The properties used to change the layout direction in GPUI
#[derive(Refineable, Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct BidiStyle {
/// The direction of layout.
pub dir: LayoutDirection,
}
/// A highlight style to apply, similar to a `TextStyle` except
/// for a single font, uniformly sized and spaced text.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
@ -555,6 +639,15 @@ impl Style {
}
}
/// Get the bidi style in this element style.
pub fn bidi_style(&self) -> Option<&BidiStyleRefinement> {
if self.bidi.is_some() {
Some(&self.bidi)
} else {
None
}
}
/// Get the content mask for this element style, based on the given bounds.
/// If the element does not hide its overflow, this will return `None`.
pub fn overflow_mask(
@ -773,6 +866,7 @@ impl Default for Style {
corner_radii: Corners::default(),
box_shadow: Default::default(),
text: TextStyleRefinement::default(),
bidi: BidiStyleRefinement::default(),
mouse_cursor: None,
opacity: None,
grid_rows: None,
@ -1166,6 +1260,18 @@ pub enum FlexDirection {
ColumnReverse,
}
impl FlexDirection {
/// Reverse the direction that items will be drawn in.
pub fn flip_horizontal(self) -> Self {
match self {
FlexDirection::Row => FlexDirection::RowReverse,
FlexDirection::Column => FlexDirection::Column,
FlexDirection::RowReverse => FlexDirection::Row,
FlexDirection::ColumnReverse => FlexDirection::ColumnReverse,
}
}
}
/// How children overflowing their container should affect layout
///
/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should

View file

@ -4,6 +4,7 @@ use crate::{
GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement,
TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
};
use crate::{BidiStyleRefinement, LayoutDirection};
pub use gpui_macros::{
border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
overflow_style_methods, padding_style_methods, position_style_methods,
@ -743,6 +744,22 @@ pub trait Styled: Sized {
self
}
/// Sets the drawing direction to LTR
fn dir_ltr(mut self) -> Self {
self.style().bidi = Some(BidiStyleRefinement {
dir: Some(LayoutDirection::LeftToRight),
});
self
}
/// Sets the drawing direction to LTR
fn dir_rtl(mut self) -> Self {
self.style().bidi = Some(BidiStyleRefinement {
dir: Some(LayoutDirection::RightToLeft),
});
self
}
/// Draws a debug border around this element.
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {

View file

@ -292,7 +292,10 @@ impl ToTaffy<taffy::style::Style> for Style {
align_content: self.align_content.map(|x| x.into()),
justify_content: self.justify_content.map(|x| x.into()),
gap: self.gap.to_taffy(rem_size),
flex_direction: self.flex_direction.into(),
flex_direction: match self.bidi.dir.unwrap_or_default() {
crate::LayoutDirection::LeftToRight => self.flex_direction.into(),
crate::LayoutDirection::RightToLeft => self.flex_direction.flip_horizontal().into(),
},
flex_wrap: self.flex_wrap.into(),
flex_basis: self.flex_basis.to_taffy(rem_size),
flex_grow: self.flex_grow,

View file

@ -2,21 +2,21 @@
use crate::Inspector;
use crate::{
Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock,
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero,
KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId,
LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent,
MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
AsyncWindowContext, AvailableSpace, Background, BidiStyle, BidiStyleRefinement, BorderStyle,
Bounds, BoxShadow, Capslock, Context, Corners, CursorStyle, Decorations, DevicePixels,
DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs,
Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke,
KeystrokeEvent, LayoutDirection, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage,
RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, TextStyle,
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@ -835,6 +835,8 @@ pub struct Window {
pub(crate) root: Option<AnyView>,
pub(crate) element_id_stack: SmallVec<[ElementId; 32]>,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
root_layout_direction: LayoutDirection,
pub(crate) bidi_style_stack: Vec<BidiStyleRefinement>,
pub(crate) rendered_entity_stack: Vec<EntityId>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) element_opacity: Option<f32>,
@ -944,6 +946,7 @@ impl Window {
app_id,
window_min_size,
window_decorations,
layout_direction,
} = options;
let bounds = window_bounds
@ -960,6 +963,7 @@ impl Window {
show,
display_id,
window_min_size,
layout_direction,
},
)?;
let display_id = platform_window.display().map(|display| display.id());
@ -1145,6 +1149,8 @@ impl Window {
root: None,
element_id_stack: SmallVec::default(),
text_style_stack: Vec::new(),
root_layout_direction: layout_direction,
bidi_style_stack: Vec::new(),
rendered_entity_stack: Vec::new(),
element_offset_stack: Vec::new(),
content_mask_stack: Vec::new(),
@ -1371,6 +1377,18 @@ impl Window {
style
}
/// The current layout direction. Which is composed of all the style refinements provided to
/// `with_bidi_style` on top of the default window layout direction.
pub fn current_layout_direction(&self) -> LayoutDirection {
let mut style = BidiStyle {
dir: self.root_layout_direction,
};
for refinement in &self.bidi_style_stack {
style.refine(refinement);
}
style.dir
}
/// Check if the platform window is maximized
/// On some platforms (namely Windows) this is different than the bounds being the size of the display
pub fn is_maximized(&self) -> bool {
@ -2274,6 +2292,24 @@ impl Window {
}
}
/// Push a bidi style onto the stack, and call a function with that style active.
/// Use [`Window::bidi_style`] to get the current, combined bidi style. This method
/// should only be called as part of element drawing.
pub fn with_bidi_style<F, R>(&mut self, style: Option<BidiStyleRefinement>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.invalidator.debug_assert_paint_or_prepaint();
if let Some(style) = style {
self.bidi_style_stack.push(style);
let result = f(self);
self.bidi_style_stack.pop();
result
} else {
f(self)
}
}
/// Updates the cursor style at the platform level. This method should only be called
/// during the prepaint phase of element drawing.
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {

View file

@ -26,9 +26,9 @@ use git_ui::git_panel::GitPanel;
use git_ui::project_diff::ProjectDiffToolbar;
use gpui::{
Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding,
ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task,
TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point,
px, retain_all,
LayoutDirection, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions,
image_cache, point, px, retain_all,
};
use image_viewer::ImageInfo;
use language::Capability;
@ -296,6 +296,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
display_id: display.map(|display| display.id()),
window_background: cx.theme().window_background_appearance(),
app_id: Some(app_id.to_owned()),
layout_direction: LayoutDirection::LeftToRight,
window_decorations: Some(window_decorations),
window_min_size: Some(gpui::Size {
width: px(360.0),