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

View file

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

View file

@ -310,3 +310,7 @@ path = "examples/window_shadow.rs"
[[example]] [[example]]
name = "grid_layout" name = "grid_layout"
path = "examples/grid_layout.rs" 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::{ use gpui::{
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window, App, Application, Bounds, Context, DisplayId, Hsla, LayoutDirection, Pixels, SharedString,
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point,
px, rgb, prelude::*, px, rgb,
}; };
struct WindowContent { struct WindowContent {
@ -60,6 +60,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
kind: WindowKind::PopUp, kind: WindowKind::PopUp,
is_movable: false, is_movable: false,
app_id: None, app_id: None,
layout_direction: LayoutDirection::LeftToRight,
window_min_size: None, window_min_size: None,
window_decorations: None, window_decorations: None,
} }

View file

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

View file

@ -1294,13 +1294,15 @@ impl Element for Div {
window, window,
cx, cx,
|style, window, cx| { |style, window, cx| {
window.with_text_style(style.text_style().cloned(), |window| { window.with_bidi_style(style.bidi_style().cloned(), |window| {
child_layout_ids = self window.with_text_style(style.text_style().cloned(), |window| {
.children child_layout_ids = self
.iter_mut() .children
.map(|child| child.request_layout(window, cx)) .iter_mut()
.collect::<SmallVec<_>>(); .map(|child| child.request_layout(window, cx))
window.request_layout(style, child_layout_ids.iter().copied(), 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_bidi_style(style.bidi_style().cloned(), |window| {
window.with_content_mask( window.with_text_style(style.text_style().cloned(), |window| {
style.overflow_mask(bounds, window.rem_size()), window.with_content_mask(
|window| { style.overflow_mask(bounds, window.rem_size()),
let hitbox = if self.should_insert_hitbox(&style, window, cx) { |window| {
Some(window.insert_hitbox(bounds, self.hitbox_behavior)) let hitbox = if self.should_insert_hitbox(&style, window, cx) {
} else { Some(window.insert_hitbox(bounds, self.hitbox_behavior))
None } else {
}; None
};
let scroll_offset = let scroll_offset =
self.clamp_scroll_position(bounds, &style, window, cx); self.clamp_scroll_position(bounds, &style, window, cx);
let result = f(&style, scroll_offset, hitbox, window, cx); let result = f(&style, scroll_offset, hitbox, window, cx);
(result, element_state) (result, element_state)
}, },
) )
})
}) })
}, },
) )
@ -1763,61 +1767,65 @@ impl Interactivity {
window.with_element_opacity(style.opacity, |window| { window.with_element_opacity(style.opacity, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| { style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| { window.with_bidi_style(style.bidi_style().cloned(), |window| {
window.with_content_mask( window.with_text_style(style.text_style().cloned(), |window| {
style.overflow_mask(bounds, window.rem_size()), window.with_content_mask(
|window| { style.overflow_mask(bounds, window.rem_size()),
if let Some(hitbox) = hitbox { |window| {
#[cfg(debug_assertions)] if let Some(hitbox) = hitbox {
self.paint_debug_info( #[cfg(debug_assertions)]
global_id, hitbox, &style, window, cx, self.paint_debug_info(
); global_id, hitbox, &style, window, cx,
);
if let Some(drag) = cx.active_drag.as_ref() { if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style { if let Some(mouse_cursor) = drag.cursor_style {
window.set_window_cursor_style(mouse_cursor); 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 { if let Some(group) = self.group.clone() {
window.set_cursor_style(mouse_cursor, hitbox); 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( window.with_bidi_style(None, |window| {
Some(crate::TextStyleRefinement { window.with_text_style(
color: Some(crate::red()), Some(crate::TextStyleRefinement {
line_height: Some(FONT_SIZE.into()), color: Some(crate::red()),
background_color: Some(crate::white()), line_height: Some(FONT_SIZE.into()),
..Default::default() background_color: Some(crate::white()),
}), ..Default::default()
render_debug_text, }),
) 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 line_height = element_state.line_height;
let mut line_origin = bounds.origin; let mut line_origin = bounds.origin;
let text_style = window.text_style(); 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 { for line in &element_state.lines {
line.paint_background( line.paint_background(
line_origin, line_origin,
line_height, line_height,
text_style.text_align, text_align,
Some(bounds), Some(bounds),
window, window,
cx, cx,
@ -455,7 +458,7 @@ impl TextLayout {
line.paint( line.paint(
line_origin, line_origin,
line_height, line_height,
text_style.text_align, text_align,
Some(bounds), Some(bounds),
window, window,
cx, cx,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,13 @@
#![deny(unsafe_op_in_unsafe_fn)] #![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::{ use std::{
cell::RefCell, cell::RefCell,
num::NonZeroIsize, num::NonZeroIsize,
@ -9,13 +17,7 @@ use std::{
sync::{Arc, Once}, sync::{Arc, Once},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use windows::Win32::Graphics::Dwm::{DWMWA_NONCLIENT_RTL_LAYOUT, DwmSetWindowAttribute};
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::{ use windows::{
Win32::{ Win32::{
Foundation::*, Foundation::*,
@ -26,8 +28,6 @@ use windows::{
core::*, core::*,
}; };
use crate::*;
pub(crate) struct WindowsWindow(pub Rc<WindowsWindowInner>); pub(crate) struct WindowsWindow(pub Rc<WindowsWindowInner>);
pub struct WindowsWindowState { pub struct WindowsWindowState {
@ -167,7 +167,7 @@ impl WindowsWindowState {
fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) { fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) {
let placement = unsafe { let placement = unsafe {
let mut placement = WINDOWPLACEMENT { let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32, length: size_of::<WINDOWPLACEMENT>() as u32,
..Default::default() ..Default::default()
}; };
GetWindowPlacement(self.hwnd, &mut placement).log_err(); GetWindowPlacement(self.hwnd, &mut placement).log_err();
@ -438,6 +438,18 @@ impl WindowsWindow {
let this = context.inner.take().unwrap()?; let this = context.inner.take().unwrap()?;
let hwnd = creation_result?; 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)?; register_drag_drop(&this)?;
configure_dwm_dark_mode(hwnd, appearance); configure_dwm_dark_mode(hwnd, appearance);
this.state.borrow_mut().border_offset.update(hwnd)?; this.state.borrow_mut().border_offset.update(hwnd)?;
@ -600,7 +612,7 @@ impl PlatformWindow for WindowsWindow {
.spawn(async move { .spawn(async move {
unsafe { unsafe {
let mut config = TASKDIALOGCONFIG::default(); let mut config = TASKDIALOGCONFIG::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _; config.cbSize = size_of::<TASKDIALOGCONFIG>() as _;
config.hwndParent = handle; config.hwndParent = handle;
let title; let title;
let main_icon; let main_icon;
@ -1271,7 +1283,7 @@ fn retrieve_window_placement(
border_offset: WindowBorderOffset, border_offset: WindowBorderOffset,
) -> Result<WINDOWPLACEMENT> { ) -> Result<WINDOWPLACEMENT> {
let mut placement = WINDOWPLACEMENT { let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32, length: size_of::<WINDOWPLACEMENT>() as u32,
..Default::default() ..Default::default()
}; };
unsafe { GetWindowPlacement(hwnd, &mut placement)? }; 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 { let mut data = WINDOWCOMPOSITIONATTRIBDATA {
attrib: 0x13, attrib: 0x13,
pv_data: &accent as *const _ as *mut _, 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 _); let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
} }

View file

@ -1,4 +1,5 @@
use std::{ use std::{
fmt::Debug,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
iter, mem, iter, mem,
ops::Range, ops::Range,
@ -254,6 +255,9 @@ pub struct Style {
/// The text style of this element /// The text style of this element
pub text: TextStyleRefinement, 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. /// The mouse cursor style shown when the mouse pointer is over an element.
pub mouse_cursor: Option<CursorStyle>, 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 /// A highlight style to apply, similar to a `TextStyle` except
/// for a single font, uniformly sized and spaced text. /// for a single font, uniformly sized and spaced text.
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[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. /// 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`. /// If the element does not hide its overflow, this will return `None`.
pub fn overflow_mask( pub fn overflow_mask(
@ -773,6 +866,7 @@ impl Default for Style {
corner_radii: Corners::default(), corner_radii: Corners::default(),
box_shadow: Default::default(), box_shadow: Default::default(),
text: TextStyleRefinement::default(), text: TextStyleRefinement::default(),
bidi: BidiStyleRefinement::default(),
mouse_cursor: None, mouse_cursor: None,
opacity: None, opacity: None,
grid_rows: None, grid_rows: None,
@ -1166,6 +1260,18 @@ pub enum FlexDirection {
ColumnReverse, 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 /// 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 /// 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, GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement,
TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
}; };
use crate::{BidiStyleRefinement, LayoutDirection};
pub use gpui_macros::{ pub use gpui_macros::{
border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
overflow_style_methods, padding_style_methods, position_style_methods, overflow_style_methods, padding_style_methods, position_style_methods,
@ -743,6 +744,22 @@ pub trait Styled: Sized {
self 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. /// Draws a debug border around this element.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn debug(mut self) -> Self { 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()), align_content: self.align_content.map(|x| x.into()),
justify_content: self.justify_content.map(|x| x.into()), justify_content: self.justify_content.map(|x| x.into()),
gap: self.gap.to_taffy(rem_size), 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_wrap: self.flex_wrap.into(),
flex_basis: self.flex_basis.to_taffy(rem_size), flex_basis: self.flex_basis.to_taffy(rem_size),
flex_grow: self.flex_grow, flex_grow: self.flex_grow,

View file

@ -2,21 +2,21 @@
use crate::Inspector; use crate::Inspector;
use crate::{ use crate::{
Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, AsyncWindowContext, AvailableSpace, Background, BidiStyle, BidiStyleRefinement, BorderStyle,
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, Bounds, BoxShadow, Capslock, Context, Corners, CursorStyle, Decorations, DevicePixels,
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs,
KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke,
LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, KeystrokeEvent, LayoutDirection, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent,
MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, TextStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
transparent_black, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
}; };
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet}; use collections::{FxHashMap, FxHashSet};
@ -835,6 +835,8 @@ pub struct Window {
pub(crate) root: Option<AnyView>, pub(crate) root: Option<AnyView>,
pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, pub(crate) element_id_stack: SmallVec<[ElementId; 32]>,
pub(crate) text_style_stack: Vec<TextStyleRefinement>, 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) rendered_entity_stack: Vec<EntityId>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>, pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) element_opacity: Option<f32>, pub(crate) element_opacity: Option<f32>,
@ -944,6 +946,7 @@ impl Window {
app_id, app_id,
window_min_size, window_min_size,
window_decorations, window_decorations,
layout_direction,
} = options; } = options;
let bounds = window_bounds let bounds = window_bounds
@ -960,6 +963,7 @@ impl Window {
show, show,
display_id, display_id,
window_min_size, window_min_size,
layout_direction,
}, },
)?; )?;
let display_id = platform_window.display().map(|display| display.id()); let display_id = platform_window.display().map(|display| display.id());
@ -1145,6 +1149,8 @@ impl Window {
root: None, root: None,
element_id_stack: SmallVec::default(), element_id_stack: SmallVec::default(),
text_style_stack: Vec::new(), text_style_stack: Vec::new(),
root_layout_direction: layout_direction,
bidi_style_stack: Vec::new(),
rendered_entity_stack: Vec::new(), rendered_entity_stack: Vec::new(),
element_offset_stack: Vec::new(), element_offset_stack: Vec::new(),
content_mask_stack: Vec::new(), content_mask_stack: Vec::new(),
@ -1371,6 +1377,18 @@ impl Window {
style 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 /// Check if the platform window is maximized
/// On some platforms (namely Windows) this is different than the bounds being the size of the display /// On some platforms (namely Windows) this is different than the bounds being the size of the display
pub fn is_maximized(&self) -> bool { 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 /// Updates the cursor style at the platform level. This method should only be called
/// during the prepaint phase of element drawing. /// during the prepaint phase of element drawing.
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { 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 git_ui::project_diff::ProjectDiffToolbar;
use gpui::{ use gpui::{
Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding,
ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, LayoutDirection, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point, Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions,
px, retain_all, image_cache, point, px, retain_all,
}; };
use image_viewer::ImageInfo; use image_viewer::ImageInfo;
use language::Capability; 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()), display_id: display.map(|display| display.id()),
window_background: cx.theme().window_background_appearance(), window_background: cx.theme().window_background_appearance(),
app_id: Some(app_id.to_owned()), app_id: Some(app_id.to_owned()),
layout_direction: LayoutDirection::LeftToRight,
window_decorations: Some(window_decorations), window_decorations: Some(window_decorations),
window_min_size: Some(gpui::Size { window_min_size: Some(gpui::Size {
width: px(360.0), width: px(360.0),