Compare commits

...

2 commits

Author SHA1 Message Date
cea4187fc0
all: add support for hy3 like tiling
All checks were successful
build / build (push) Successful in 11m12s
2026-04-10 13:17:00 -04:00
a41dbae899
all: remove traditional i3 titlebars, add corner rounding 2026-04-10 13:17:00 -04:00
57 changed files with 3056 additions and 1048 deletions

View file

@ -20,10 +20,18 @@ pub const TREES: &[Tree] = &[
"tex.frag", "tex.frag",
"out.vert", "out.vert",
"out.frag", "out.frag",
"rounded_fill.frag",
"rounded_fill.vert",
"rounded_tex.frag",
"rounded_tex.vert",
"legacy/fill.frag", "legacy/fill.frag",
"legacy/fill.vert", "legacy/fill.vert",
"legacy/tex.vert", "legacy/tex.vert",
"legacy/tex.frag", "legacy/tex.frag",
"legacy/rounded_fill.frag",
"legacy/rounded_fill.vert",
"legacy/rounded_tex.frag",
"legacy/rounded_tex.vert",
], ],
}, },
]; ];

View file

@ -2025,6 +2025,52 @@ impl ConfigClient {
self.send(&ClientMessage::SetPointerRevertKey { seat, key }); self.send(&ClientMessage::SetPointerRevertKey { seat, key });
} }
pub fn set_corner_radius(&self, radius: f32) {
self.send(&ClientMessage::SetCornerRadius { radius });
}
pub fn get_corner_radius(&self) -> f32 {
let res = self.send_with_response(&ClientMessage::GetCornerRadius);
get_response!(res, 0.0, GetCornerRadius { radius });
radius
}
pub fn seat_toggle_expand(&self, seat: Seat) {
self.send(&ClientMessage::SeatToggleExpand { seat });
}
pub fn seat_toggle_tab(&self, seat: Seat) {
self.send(&ClientMessage::SeatToggleTab { seat });
}
pub fn seat_make_group(&self, seat: Seat, axis: Axis, ephemeral: bool) {
self.send(&ClientMessage::SeatMakeGroup {
seat,
axis,
ephemeral,
});
}
pub fn seat_change_group_opposite(&self, seat: Seat) {
self.send(&ClientMessage::SeatChangeGroupOpposite { seat });
}
pub fn seat_equalize(&self, seat: Seat, recursive: bool) {
self.send(&ClientMessage::SeatEqualize { seat, recursive });
}
pub fn set_autotile(&self, enabled: bool) {
self.send(&ClientMessage::SetAutotile { enabled });
}
pub fn set_tab_title_align(&self, align: u32) {
self.send(&ClientMessage::SetTabTitleAlign { align });
}
pub fn seat_move_tab(&self, seat: Seat, right: bool) {
self.send(&ClientMessage::SeatMoveTab { seat, right });
}
fn handle_msg(&self, msg: &[u8]) { fn handle_msg(&self, msg: &[u8]) {
self.handle_msg2(msg); self.handle_msg2(msg);
self.dispatch_futures(); self.dispatch_futures();

View file

@ -880,6 +880,38 @@ pub enum ClientMessage<'a> {
seat: Seat, seat: Seat,
enabled: bool, enabled: bool,
}, },
SetCornerRadius {
radius: f32,
},
GetCornerRadius,
SeatToggleExpand {
seat: Seat,
},
SeatToggleTab {
seat: Seat,
},
SeatMakeGroup {
seat: Seat,
axis: Axis,
ephemeral: bool,
},
SeatChangeGroupOpposite {
seat: Seat,
},
SeatEqualize {
seat: Seat,
recursive: bool,
},
SetAutotile {
enabled: bool,
},
SetTabTitleAlign {
align: u32,
},
SeatMoveTab {
seat: Seat,
right: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -1136,6 +1168,9 @@ pub enum Response {
ConnectorSupportsArbitraryModes { ConnectorSupportsArbitraryModes {
supports_arbitrary_modes: bool, supports_arbitrary_modes: bool,
}, },
GetCornerRadius {
radius: f32,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -677,6 +677,33 @@ impl Seat {
pub fn unstable_set_mouse_follows_focus(self, enabled: bool) { pub fn unstable_set_mouse_follows_focus(self, enabled: bool) {
get!().seat_set_mouse_follows_focus(self, enabled) get!().seat_set_mouse_follows_focus(self, enabled)
} }
/// Toggles tabbed mode on the focused window's parent container.
pub fn toggle_tab(self) {
get!().seat_toggle_tab(self)
}
/// Wraps the focused child in a new sub-container with the given split axis.
pub fn make_group(self, axis: Axis, ephemeral: bool) {
get!().seat_make_group(self, axis, ephemeral)
}
/// Toggles the parent container's split direction (H↔V).
pub fn change_group_opposite(self) {
get!().seat_change_group_opposite(self)
}
/// Resets all siblings' size factors to equal.
pub fn equalize(self, recursive: bool) {
get!().seat_equalize(self, recursive)
}
/// Move the active tab left or right within the current tab bar.
///
/// Equivalent to hy3's `movewindow` within a tabbed group.
pub fn move_tab(self, right: bool) {
get!().seat_move_tab(self, right)
}
} }
/// A focus-follows-mouse mode. /// A focus-follows-mouse mode.

View file

@ -382,6 +382,42 @@ pub fn toggle_floating_titles() {
get.set_floating_titles(!get.get_floating_titles()); get.set_floating_titles(!get.get_floating_titles());
} }
/// Sets the corner radius for window borders.
///
/// A radius of 0 means square corners. The radius is in logical pixels.
pub fn set_corner_radius(radius: f32) {
get!().set_corner_radius(radius)
}
/// Returns the current corner radius for window borders.
pub fn get_corner_radius() -> f32 {
get!(0.0).get_corner_radius()
}
/// Enables or disables autotiling.
///
/// When enabled, new windows are automatically placed in a perpendicular
/// sub-container if the predicted body would be narrower than tall (or vice versa).
///
/// The default is `false`.
pub fn set_autotile(enabled: bool) {
get!().set_autotile(enabled)
}
/// Sets the horizontal alignment of title text within tab buttons.
///
/// - `"start"` — left-aligned (default)
/// - `"center"` — centered
/// - `"end"` — right-aligned
pub fn set_tab_title_align(align: &str) {
let val = match align {
"center" => 1,
"end" => 2,
_ => 0, // start
};
get!().set_tab_title_align(val)
}
/// Sets a callback to run when this config is unloaded. /// Sets a callback to run when this config is unloaded.
/// ///
/// Only one callback can be set at a time. If another callback is already set, it will be /// Only one callback can be set at a time. If another callback is already set, it will be

View file

@ -302,6 +302,38 @@ pub mod colors {
/// ///
/// Default: `#9d28c67f`. /// Default: `#9d28c67f`.
const 15 => HIGHLIGHT_COLOR, const 15 => HIGHLIGHT_COLOR,
/// The background color of an active (focused) tab.
///
/// Default: `#33ccff40`.
const 16 => TAB_ACTIVE_BACKGROUND_COLOR,
/// The border color of an active (focused) tab.
///
/// Default: `#33ccffee`.
const 17 => TAB_ACTIVE_BORDER_COLOR,
/// The background color of an inactive tab.
///
/// Default: `#222222`.
const 18 => TAB_INACTIVE_BACKGROUND_COLOR,
/// The border color of an inactive tab.
///
/// Default: `#333333`.
const 19 => TAB_INACTIVE_BORDER_COLOR,
/// The text color of an active (focused) tab.
///
/// Default: `#ffffff`.
const 20 => TAB_ACTIVE_TEXT_COLOR,
/// The text color of an inactive tab.
///
/// Default: `#888888`.
const 21 => TAB_INACTIVE_TEXT_COLOR,
/// The background color of the tab bar strip.
///
/// Default: `#111111`.
const 22 => TAB_BAR_BACKGROUND_COLOR,
/// The background color of a tab that has requested attention.
///
/// Default: `#23092c`.
const 23 => TAB_ATTENTION_BACKGROUND_COLOR,
} }
/// Sets the color of GUI element. /// Sets the color of GUI element.
@ -374,5 +406,29 @@ pub mod sized {
/// ///
/// Default: 0 /// Default: 0
const 06 => TITLE_GAP, const 06 => TITLE_GAP,
/// The height of the tab bar in pixels.
///
/// Default: 22
const 07 => TAB_BAR_HEIGHT,
/// The padding between tabs in the tab bar in pixels.
///
/// Default: 6
const 08 => TAB_BAR_PADDING,
/// The corner radius of tabs in the tab bar in pixels.
///
/// Default: 6
const 09 => TAB_BAR_RADIUS,
/// The border width of tabs in the tab bar in pixels.
///
/// Default: 2
const 10 => TAB_BAR_BORDER_WIDTH,
/// The horizontal padding within each tab for text in pixels.
///
/// Default: 4
const 11 => TAB_BAR_TEXT_PADDING,
/// The gap between the tab bar and the window content below in pixels.
///
/// Default: 4
const 12 => TAB_BAR_GAP,
} }
} }

View file

@ -60,8 +60,9 @@ use {
tree::{ tree::{
DisplayNode, NodeIds, OutputNode, TearingMode, Transform, VrrMode, DisplayNode, NodeIds, OutputNode, TearingMode, Transform, VrrMode,
WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions, WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions,
container_render_titles, float_layout, float_titles, output_render_data, float_layout, output_render_data,
placeholder_render_textures, placeholder_render_textures,
container_tab_render_textures,
}, },
user_session::import_environment, user_session::import_environment,
utils::{ utils::{
@ -259,14 +260,13 @@ fn start_compositor2(
theme: Default::default(), theme: Default::default(),
pending_container_layout: Default::default(), pending_container_layout: Default::default(),
pending_container_render_positions: Default::default(), pending_container_render_positions: Default::default(),
pending_container_render_title: Default::default(),
pending_output_render_data: Default::default(), pending_output_render_data: Default::default(),
pending_float_layout: Default::default(), pending_float_layout: Default::default(),
pending_float_titles: Default::default(),
pending_input_popup_positioning: Default::default(), pending_input_popup_positioning: Default::default(),
pending_toplevel_screencasts: Default::default(), pending_toplevel_screencasts: Default::default(),
pending_screencast_reallocs_or_reconfigures: Default::default(), pending_screencast_reallocs_or_reconfigures: Default::default(),
pending_placeholder_render_textures: Default::default(), pending_placeholder_render_textures: Default::default(),
pending_container_tab_render_textures: Default::default(),
dbus: Dbus::new(&engine, &ring, &run_toplevel), dbus: Dbus::new(&engine, &ring, &run_toplevel),
fdcloser: FdCloser::new(), fdcloser: FdCloser::new(),
logger: logger.clone(), logger: logger.clone(),
@ -511,16 +511,16 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
Phase::PostLayout, Phase::PostLayout,
container_render_positions(state.clone()), container_render_positions(state.clone()),
), ),
eng.spawn2(
"container titles",
Phase::PostLayout,
container_render_titles(state.clone()),
),
eng.spawn2( eng.spawn2(
"placeholder textures", "placeholder textures",
Phase::PostLayout, Phase::PostLayout,
placeholder_render_textures(state.clone()), placeholder_render_textures(state.clone()),
), ),
eng.spawn2(
"container tab textures",
Phase::PostLayout,
container_tab_render_textures(state.clone()),
),
eng.spawn2( eng.spawn2(
"output render", "output render",
Phase::PostLayout, Phase::PostLayout,
@ -531,11 +531,6 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
wlr_output_manager_done(state.clone()), wlr_output_manager_done(state.clone()),
), ),
eng.spawn2("float layout", Phase::Layout, float_layout(state.clone())), eng.spawn2("float layout", Phase::Layout, float_layout(state.clone())),
eng.spawn2(
"float titles",
Phase::PostLayout,
float_titles(state.clone()),
),
eng.spawn2("idle", Phase::PostLayout, idle(state.clone())), eng.spawn2("idle", Phase::PostLayout, idle(state.clone())),
eng.spawn2( eng.spawn2(
"input, popup positioning", "input, popup positioning",

View file

@ -1411,24 +1411,20 @@ impl ConfigProxyHandler {
}); });
} }
fn handle_set_show_titles(&self, show: bool) { fn handle_set_show_titles(&self, _show: bool) {
self.state.set_show_titles(show); // no-op: titles have been removed
} }
fn handle_get_show_titles(&self) { fn handle_get_show_titles(&self) {
self.respond(Response::GetShowTitles { self.respond(Response::GetShowTitles { show: false });
show: self.state.theme.show_titles.get(),
});
} }
fn handle_set_floating_titles(&self, floating: bool) { fn handle_set_floating_titles(&self, _floating: bool) {
self.state.set_floating_titles(floating); // no-op: titles have been removed
} }
fn handle_get_floating_titles(&self) { fn handle_get_floating_titles(&self) {
self.respond(Response::GetFloatingTitles { self.respond(Response::GetFloatingTitles { floating: false });
floating: self.state.theme.floating_titles.get(),
});
} }
fn handle_set_bar_position(&self, position: BarPosition) -> Result<(), CphError> { fn handle_set_bar_position(&self, position: BarPosition) -> Result<(), CphError> {
@ -1445,8 +1441,24 @@ impl ConfigProxyHandler {
}); });
} }
fn handle_set_show_float_pin_icon(&self, show: bool) { fn handle_set_corner_radius(&self, radius: f32) {
self.state.set_show_pin_icon(show); use crate::theme::CornerRadius;
let radius = radius.max(0.0).min(1000.0);
self.state
.theme
.corner_radius
.set(CornerRadius::from(radius));
self.state.damage(self.state.root.extents.get());
}
fn handle_get_corner_radius(&self) {
self.respond(Response::GetCornerRadius {
radius: self.state.theme.corner_radius.get().top_left,
});
}
fn handle_set_show_float_pin_icon(&self, _show: bool) {
// no-op: titles have been removed, pin icon was in title bar
} }
fn handle_set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { fn handle_set_workspace_display_order(&self, order: WorkspaceDisplayOrder) {
@ -1745,6 +1757,41 @@ impl ConfigProxyHandler {
Ok(()) Ok(())
} }
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.toggle_tab();
Ok(())
}
fn handle_seat_make_group(
&self,
seat: Seat,
axis: Axis,
ephemeral: bool,
) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.make_group(axis.into(), ephemeral);
Ok(())
}
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.change_group_opposite();
Ok(())
}
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.equalize(recursive);
Ok(())
}
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.move_tab(right);
Ok(())
}
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
let window = self.get_window(window)?; let window = self.get_window(window)?;
self.respond(Response::GetWindowSplit { self.respond(Response::GetWindowSplit {
@ -2452,6 +2499,12 @@ impl ConfigProxyHandler {
BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width, BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width,
GAP => ThemeSized::gap, GAP => ThemeSized::gap,
TITLE_GAP => ThemeSized::title_gap, TITLE_GAP => ThemeSized::title_gap,
TAB_BAR_HEIGHT => ThemeSized::tab_bar_height,
TAB_BAR_PADDING => ThemeSized::tab_bar_padding,
TAB_BAR_RADIUS => ThemeSized::tab_bar_radius,
TAB_BAR_BORDER_WIDTH => ThemeSized::tab_bar_border_width,
TAB_BAR_TEXT_PADDING => ThemeSized::tab_bar_text_padding,
TAB_BAR_GAP => ThemeSized::tab_bar_gap,
_ => return Err(CphError::UnknownSized(sized.0)), _ => return Err(CphError::UnknownSized(sized.0)),
}; };
Ok(sized) Ok(sized)
@ -2496,8 +2549,8 @@ impl ConfigProxyHandler {
self.state.set_bar_font(Some(font)); self.state.set_bar_font(Some(font));
} }
fn handle_set_title_font(&self, font: &str) { fn handle_set_title_font(&self, _font: &str) {
self.state.set_title_font(Some(font)); // no-op: titles have been removed
} }
fn handle_get_font(&self) { fn handle_get_font(&self) {
@ -2529,6 +2582,14 @@ impl ConfigProxyHandler {
BAR_STATUS_TEXT_COLOR => ThemeColor::bar_text, BAR_STATUS_TEXT_COLOR => ThemeColor::bar_text,
ATTENTION_REQUESTED_BACKGROUND_COLOR => ThemeColor::attention_requested_background, ATTENTION_REQUESTED_BACKGROUND_COLOR => ThemeColor::attention_requested_background,
HIGHLIGHT_COLOR => ThemeColor::highlight, HIGHLIGHT_COLOR => ThemeColor::highlight,
TAB_ACTIVE_BACKGROUND_COLOR => ThemeColor::tab_active_background,
TAB_ACTIVE_BORDER_COLOR => ThemeColor::tab_active_border,
TAB_INACTIVE_BACKGROUND_COLOR => ThemeColor::tab_inactive_background,
TAB_INACTIVE_BORDER_COLOR => ThemeColor::tab_inactive_border,
TAB_ACTIVE_TEXT_COLOR => ThemeColor::tab_active_text,
TAB_INACTIVE_TEXT_COLOR => ThemeColor::tab_inactive_text,
TAB_BAR_BACKGROUND_COLOR => ThemeColor::tab_bar_background,
TAB_ATTENTION_BACKGROUND_COLOR => ThemeColor::tab_attention_background,
_ => return Err(CphError::UnknownColor(colorable.0)), _ => return Err(CphError::UnknownColor(colorable.0)),
}; };
Ok(colorable) Ok(colorable)
@ -3310,6 +3371,10 @@ impl ConfigProxyHandler {
.handle_set_bar_position(position) .handle_set_bar_position(position)
.wrn("set_bar_position")?, .wrn("set_bar_position")?,
ClientMessage::GetBarPosition => self.handle_get_bar_position(), ClientMessage::GetBarPosition => self.handle_get_bar_position(),
ClientMessage::SetCornerRadius { radius } => {
self.handle_set_corner_radius(radius)
}
ClientMessage::GetCornerRadius => self.handle_get_corner_radius(),
ClientMessage::SeatFocusHistory { seat, timeline } => self ClientMessage::SeatFocusHistory { seat, timeline } => self
.handle_seat_focus_history(seat, timeline) .handle_seat_focus_history(seat, timeline)
.wrn("seat_focus_history")?, .wrn("seat_focus_history")?,
@ -3432,6 +3497,40 @@ impl ConfigProxyHandler {
} => self } => self
.handle_window_resize(window, dx1, dy1, dx2, dy2) .handle_window_resize(window, dx1, dy1, dx2, dy2)
.wrn("window_resize")?, .wrn("window_resize")?,
ClientMessage::SeatToggleTab { seat } => self
.handle_seat_toggle_tab(seat)
.wrn("seat_toggle_tab")?,
ClientMessage::SeatMakeGroup {
seat,
axis,
ephemeral,
} => self
.handle_seat_make_group(seat, axis, ephemeral)
.wrn("seat_make_group")?,
ClientMessage::SeatChangeGroupOpposite { seat } => self
.handle_seat_change_group_opposite(seat)
.wrn("seat_change_group_opposite")?,
ClientMessage::SeatEqualize { seat, recursive } => self
.handle_seat_equalize(seat, recursive)
.wrn("seat_equalize")?,
ClientMessage::SetAutotile { enabled } => {
self.state.theme.autotile_enabled.set(enabled);
}
ClientMessage::SeatToggleExpand { .. } => {
// Removed feature; kept for binary protocol compatibility.
}
ClientMessage::SetTabTitleAlign { align } => {
use crate::theme::TabTitleAlign;
let val = match align {
1 => TabTitleAlign::Center,
2 => TabTitleAlign::End,
_ => TabTitleAlign::Start,
};
self.state.theme.tab_title_align.set(val);
}
ClientMessage::SeatMoveTab { seat, right } => self
.handle_seat_move_tab(seat, right)
.wrn("seat_move_tab")?,
} }
Ok(()) Ok(())
} }

View file

@ -102,6 +102,8 @@ pub enum GfxApiOpt {
Sync, Sync,
FillRect(FillRect), FillRect(FillRect),
CopyTexture(CopyTexture), CopyTexture(CopyTexture),
RoundedFillRect(RoundedFillRect),
RoundedCopyTexture(RoundedCopyTexture),
} }
pub struct GfxRenderPass { pub struct GfxRenderPass {
@ -289,6 +291,55 @@ pub struct CopyTexture {
pub alpha_mode: AlphaMode, pub alpha_mode: AlphaMode,
} }
#[derive(Debug)]
pub struct RoundedFillRect {
pub rect: FramebufferRect,
pub color: Color,
pub alpha: Option<f32>,
pub render_intent: RenderIntent,
pub cd: Rc<LinearColorDescription>,
/// Size of the rectangle in physical pixels.
pub size: [f32; 2],
/// Per-corner radius in physical pixels: [top_left, top_right, bottom_right, bottom_left].
pub corner_radius: [f32; 4],
/// Border width in physical pixels. 0 means a filled rounded rect (no cutout).
pub border_width: f32,
/// Output scale for antialiasing.
pub scale: f32,
/// Sort order hint within the RoundedFill bucket (lower renders first).
pub z_order: u32,
}
impl RoundedFillRect {
pub fn effective_color(&self) -> Color {
let mut color = self.color;
if let Some(alpha) = self.alpha {
color = color * alpha;
}
color
}
}
pub struct RoundedCopyTexture {
pub tex: Rc<dyn GfxTexture>,
pub source: SampleRect,
pub target: FramebufferRect,
pub buffer_resv: Option<Rc<dyn BufferResv>>,
pub acquire_sync: AcquireSync,
pub release_sync: ReleaseSync,
pub alpha: Option<f32>,
pub opaque: bool,
pub render_intent: RenderIntent,
pub cd: Rc<ColorDescription>,
pub alpha_mode: AlphaMode,
/// Size of the rectangle in physical pixels.
pub size: [f32; 2],
/// Per-corner radius in physical pixels: [top_left, top_right, bottom_right, bottom_left].
pub corner_radius: [f32; 4],
/// Output scale for antialiasing.
pub scale: f32,
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct SyncFile(pub Rc<OwnedFd>); pub struct SyncFile(pub Rc<OwnedFd>);
@ -705,8 +756,8 @@ impl dyn GfxFramebuffer {
let (width, height) = self.logical_size(transform); let (width, height) = self.logical_size(transform);
Rect::new_saturating(0, 0, width, height) Rect::new_saturating(0, 0, width, height)
}, },
icons: None,
stretch: None, stretch: None,
corner_radius: None,
}; };
cursor.render_hardware_cursor(&mut renderer); cursor.render_hardware_cursor(&mut renderer);
self.render( self.render(
@ -1039,8 +1090,8 @@ pub fn create_render_pass(
let (width, height) = logical_size(physical_size, transform); let (width, height) = logical_size(physical_size, transform);
Rect::new_saturating(0, 0, width, height) Rect::new_saturating(0, 0, width, height)
}, },
icons: state.icons.get(state, scale),
stretch: None, stretch: None,
corner_radius: None,
}; };
node.node_render(&mut renderer, 0, 0, None); node.node_render(&mut renderer, 0, 0, None);
if let Some(rect) = cursor_rect { if let Some(rect) = cursor_rect {
@ -1256,6 +1307,8 @@ impl GfxRenderPass {
return None; return None;
} }
GfxApiOpt::CopyTexture(ct) => break 'ct2 ct, GfxApiOpt::CopyTexture(ct) => break 'ct2 ct,
GfxApiOpt::RoundedFillRect(_) => return None,
GfxApiOpt::RoundedCopyTexture(_) => return None,
} }
} }
return None; return None;
@ -1299,6 +1352,8 @@ impl GfxRenderPass {
// Texture could be visible. // Texture could be visible.
return None; return None;
} }
GfxApiOpt::RoundedFillRect(_) => return None,
GfxApiOpt::RoundedCopyTexture(_) => return None,
} }
} }
if let Some(clear) = self.clear if let Some(clear) = self.clear

View file

@ -23,7 +23,7 @@ use {
cmm::cmm_eotf::Eotf, cmm::cmm_eotf::Eotf,
gfx_api::{ gfx_api::{
AcquireSync, CopyTexture, FdSync, FramebufferRect, GfxApiOpt, GfxContext, GfxError, AcquireSync, CopyTexture, FdSync, FramebufferRect, GfxApiOpt, GfxContext, GfxError,
GfxTexture, ReleaseSync, SyncFile, GfxTexture, ReleaseSync, RoundedCopyTexture, RoundedFillRect, SyncFile,
}, },
gfx_apis::gl::{ gfx_apis::gl::{
egl::image::EglImage, egl::image::EglImage,
@ -217,6 +217,14 @@ fn run_ops(fb: &Framebuffer, ops: &[GfxApiOpt]) -> Option<FdSync> {
copy_tex.push(c); copy_tex.push(c);
i += 1; i += 1;
} }
GfxApiOpt::RoundedFillRect(rf) => {
render_rounded_fill(&fb.ctx, rf);
i += 1;
}
GfxApiOpt::RoundedCopyTexture(ct) => {
render_rounded_texture(&fb.ctx, ct);
i += 1;
}
} }
} }
if fill_rect.is_not_empty() { if fill_rect.is_not_empty() {
@ -269,6 +277,12 @@ fn run_ops(fb: &Framebuffer, ops: &[GfxApiOpt]) -> Option<FdSync> {
{ {
resv.set_sync(user, &file); resv.set_sync(user, &file);
} }
if let GfxApiOpt::RoundedCopyTexture(ct) = op
&& ct.release_sync == ReleaseSync::Explicit
&& let Some(resv) = &ct.buffer_resv
{
resv.set_sync(user, &file);
}
} }
return Some(file); return Some(file);
} }
@ -376,6 +390,114 @@ fn render_texture(ctx: &GlRenderContext, tex: &CopyTexture) {
} }
} }
fn render_rounded_fill(ctx: &GlRenderContext, rf: &RoundedFillRect) {
let gles = ctx.ctx.dpy.gles;
let prog = &ctx.rounded_fill_prog;
let pos = rf.rect.to_points();
// geo_pos maps vertex corners to pixel coordinates within the rectangle
let [w, h] = rf.size;
let geo: [[f32; 2]; 4] = [[w, 0.0], [0.0, 0.0], [w, h], [0.0, h]];
let [r, g, b, a] = rf.color.to_array(Eotf::Gamma22);
let a = a * rf.alpha.unwrap_or(1.0);
unsafe {
(gles.glEnable)(GL_BLEND);
(gles.glUseProgram)(prog.prog.prog);
(gles.glUniform4f)(prog.color, r * a, g * a, b * a, a);
(gles.glUniform2f)(prog.size, rf.size[0], rf.size[1]);
(gles.glUniform4f)(
prog.corner_radius,
rf.corner_radius[0],
rf.corner_radius[1],
rf.corner_radius[2],
rf.corner_radius[3],
);
(gles.glUniform1f)(prog.border_width, rf.border_width);
(gles.glUniform1f)(prog.scale, rf.scale);
(gles.glVertexAttribPointer)(prog.pos as _, 2, GL_FLOAT, GL_FALSE, 0, pos.as_ptr() as _);
(gles.glVertexAttribPointer)(prog.geo as _, 2, GL_FLOAT, GL_FALSE, 0, geo.as_ptr() as _);
(gles.glEnableVertexAttribArray)(prog.pos as _);
(gles.glEnableVertexAttribArray)(prog.geo as _);
(gles.glDrawArrays)(GL_TRIANGLE_STRIP, 0, 4);
(gles.glDisableVertexAttribArray)(prog.pos as _);
(gles.glDisableVertexAttribArray)(prog.geo as _);
}
}
fn render_rounded_texture(ctx: &GlRenderContext, ct: &RoundedCopyTexture) {
let Some(texture) = ct.tex.as_gl() else {
log::error!("A non-OpenGL texture was passed into OpenGL");
return;
};
if !texture.gl.contents_valid.get() {
log::error!("Ignoring texture with invalid contents");
return;
}
assert!(rc_eq(&ctx.ctx, &texture.ctx.ctx));
let gles = ctx.ctx.dpy.gles;
unsafe {
handle_explicit_sync(ctx, texture.gl.img.as_ref(), &ct.acquire_sync);
(gles.glActiveTexture)(GL_TEXTURE0);
let target = image_target(texture.gl.external_only);
(gles.glBindTexture)(target, texture.gl.tex);
(gles.glTexParameteri)(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
let progs = match texture.gl.external_only {
true => match &ctx.rounded_tex_external {
Some(p) => p,
_ => {
log::error!(
"Trying to render an external-only texture but context does not support the required extension"
);
return;
}
},
false => &ctx.rounded_tex_internal,
};
let copy_type = match ct.alpha.is_some() {
true => TexCopyType::Multiply,
false => TexCopyType::Identity,
};
let source_type = TexSourceType::HasAlpha;
(gles.glEnable)(GL_BLEND);
let prog = &progs[copy_type][source_type];
(gles.glUseProgram)(prog.prog.prog);
(gles.glUniform1i)(prog.tex, 0);
(gles.glUniform2f)(prog.size, ct.size[0], ct.size[1]);
(gles.glUniform4f)(
prog.corner_radius,
ct.corner_radius[0],
ct.corner_radius[1],
ct.corner_radius[2],
ct.corner_radius[3],
);
(gles.glUniform1f)(prog.scale, ct.scale);
if let Some(alpha) = ct.alpha {
(gles.glUniform1f)(prog.alpha, alpha);
}
let texcoord = ct.source.to_points();
let pos = ct.target.to_points();
let [w, h] = ct.size;
let geo: [[f32; 2]; 4] = [[w, 0.0], [0.0, 0.0], [w, h], [0.0, h]];
(gles.glVertexAttribPointer)(
prog.texcoord as _,
2,
GL_FLOAT,
GL_FALSE,
0,
texcoord.as_ptr() as _,
);
(gles.glVertexAttribPointer)(prog.pos as _, 2, GL_FLOAT, GL_FALSE, 0, pos.as_ptr() as _);
(gles.glVertexAttribPointer)(prog.geo as _, 2, GL_FLOAT, GL_FALSE, 0, geo.as_ptr() as _);
(gles.glEnableVertexAttribArray)(prog.texcoord as _);
(gles.glEnableVertexAttribArray)(prog.pos as _);
(gles.glEnableVertexAttribArray)(prog.geo as _);
(gles.glDrawArrays)(GL_TRIANGLE_STRIP, 0, 4);
(gles.glDisableVertexAttribArray)(prog.texcoord as _);
(gles.glDisableVertexAttribArray)(prog.pos as _);
(gles.glDisableVertexAttribArray)(prog.geo as _);
(gles.glBindTexture)(target, 0);
}
}
fn handle_explicit_sync(ctx: &GlRenderContext, img: Option<&Rc<EglImage>>, sync: &AcquireSync) { fn handle_explicit_sync(ctx: &GlRenderContext, img: Option<&Rc<EglImage>>, sync: &AcquireSync) {
let Some(sync_file) = sync.get_sync_file() else { let Some(sync_file) = sync.get_sync_file() else {
return; return;

View file

@ -125,6 +125,7 @@ dynload! {
glGetAttribLocation: unsafe fn(prog: GLuint, name: *const GLchar) -> GLint, glGetAttribLocation: unsafe fn(prog: GLuint, name: *const GLchar) -> GLint,
glUniform1i: unsafe fn(location: GLint, v0: GLint), glUniform1i: unsafe fn(location: GLint, v0: GLint),
glUniform1f: unsafe fn(location: GLint, v0: GLfloat), glUniform1f: unsafe fn(location: GLint, v0: GLfloat),
glUniform2f: unsafe fn(location: GLint, v0: GLfloat, v1: GLfloat),
glUniform4f: unsafe fn(location: GLint, v0: GLfloat, v1: GLfloat, v2: GLfloat, v3: GLfloat), glUniform4f: unsafe fn(location: GLint, v0: GLfloat, v1: GLfloat, v2: GLfloat, v3: GLfloat),
glVertexAttribPointer: unsafe fn( glVertexAttribPointer: unsafe fn(
index: GLuint, index: GLuint,

View file

@ -60,6 +60,29 @@ impl TexProg {
} }
} }
pub(crate) struct RoundedFillProg {
pub(crate) prog: GlProgram,
pub(crate) pos: GLint,
pub(crate) geo: GLint,
pub(crate) color: GLint,
pub(crate) size: GLint,
pub(crate) corner_radius: GLint,
pub(crate) border_width: GLint,
pub(crate) scale: GLint,
}
pub(crate) struct RoundedTexProg {
pub(crate) prog: GlProgram,
pub(crate) pos: GLint,
pub(crate) texcoord: GLint,
pub(crate) geo: GLint,
pub(crate) tex: GLint,
pub(crate) alpha: GLint,
pub(crate) size: GLint,
pub(crate) corner_radius: GLint,
pub(crate) scale: GLint,
}
#[derive(Copy, Clone, PartialEq, Linearize)] #[derive(Copy, Clone, PartialEq, Linearize)]
pub(in crate::gfx_apis::gl) enum TexCopyType { pub(in crate::gfx_apis::gl) enum TexCopyType {
Identity, Identity,
@ -86,6 +109,12 @@ pub(in crate::gfx_apis::gl) struct GlRenderContext {
pub(crate) fill_prog_pos: GLint, pub(crate) fill_prog_pos: GLint,
pub(crate) fill_prog_color: GLint, pub(crate) fill_prog_color: GLint,
pub(crate) rounded_fill_prog: RoundedFillProg,
pub(crate) rounded_tex_internal:
StaticMap<TexCopyType, StaticMap<TexSourceType, RoundedTexProg>>,
pub(crate) rounded_tex_external:
Option<StaticMap<TexCopyType, StaticMap<TexSourceType, RoundedTexProg>>>,
pub(in crate::gfx_apis::gl) gl_state: RefCell<GfxGlState>, pub(in crate::gfx_apis::gl) gl_state: RefCell<GfxGlState>,
pub(in crate::gfx_apis::gl) buffer_resv_user: BufferResvUser, pub(in crate::gfx_apis::gl) buffer_resv_user: BufferResvUser,
@ -163,6 +192,74 @@ impl GlRenderContext {
include_str!("../shaders/fill.frag.glsl"), include_str!("../shaders/fill.frag.glsl"),
)? )?
}; };
let rounded_fill_prog = unsafe {
let prog = GlProgram::from_shaders(
ctx,
include_str!("../shaders/rounded_fill.vert.glsl"),
include_str!("../shaders/rounded_fill.frag.glsl"),
)?;
RoundedFillProg {
pos: prog.get_attrib_location(c"pos"),
geo: prog.get_attrib_location(c"geo"),
color: prog.get_uniform_location(c"color"),
size: prog.get_uniform_location(c"size"),
corner_radius: prog.get_uniform_location(c"corner_radius"),
border_width: prog.get_uniform_location(c"border_width"),
scale: prog.get_uniform_location(c"scale"),
prog,
}
};
let rounded_tex_vert = include_str!("../shaders/rounded_tex.vert.glsl");
let rounded_tex_frag = include_str!("../shaders/rounded_tex.frag.glsl");
let create_rounded_tex_programs = |external: bool| {
let create_program = |alpha_multiplier: bool, alpha: bool| {
let mut src = String::new();
if external {
src.push_str("#define EXTERNAL\n");
}
if alpha_multiplier {
src.push_str("#define ALPHA_MULTIPLIER\n");
}
if alpha {
src.push_str("#define ALPHA\n");
}
src.push_str(rounded_tex_frag);
unsafe {
let prog = GlProgram::from_shaders(ctx, rounded_tex_vert, &src)?;
let alpha_loc = match alpha_multiplier {
true => prog.get_uniform_location(c"alpha"),
false => 0,
};
Ok::<_, RenderError>(RoundedTexProg {
pos: prog.get_attrib_location(c"pos"),
texcoord: prog.get_attrib_location(c"texcoord"),
geo: prog.get_attrib_location(c"geo"),
tex: prog.get_uniform_location(c"tex"),
alpha: alpha_loc,
size: prog.get_uniform_location(c"size"),
corner_radius: prog.get_uniform_location(c"corner_radius"),
scale: prog.get_uniform_location(c"scale"),
prog,
})
}
};
Ok::<_, RenderError>(static_map! {
TexCopyType::Identity => static_map! {
TexSourceType::Opaque => create_program(false, false)?,
TexSourceType::HasAlpha => create_program(false, true)?,
},
TexCopyType::Multiply => static_map! {
TexSourceType::Opaque => create_program(true, false)?,
TexSourceType::HasAlpha => create_program(true, true)?,
},
})
};
let rounded_tex_internal = create_rounded_tex_programs(false)?;
let rounded_tex_external = if ctx.ext.contains(GL_OES_EGL_IMAGE_EXTERNAL) {
Some(create_rounded_tex_programs(true)?)
} else {
None
};
Ok(Self { Ok(Self {
ctx: ctx.clone(), ctx: ctx.clone(),
gbm: ctx.dpy.gbm.clone(), gbm: ctx.dpy.gbm.clone(),
@ -177,6 +274,10 @@ impl GlRenderContext {
fill_prog_color: unsafe { fill_prog.get_uniform_location(c"color") }, fill_prog_color: unsafe { fill_prog.get_uniform_location(c"color") },
fill_prog, fill_prog,
rounded_fill_prog,
rounded_tex_internal,
rounded_tex_external,
gl_state: Default::default(), gl_state: Default::default(),
buffer_resv_user: Default::default(), buffer_resv_user: Default::default(),

View file

@ -0,0 +1,56 @@
precision mediump float;
varying vec2 v_geo;
uniform vec4 color;
uniform vec2 size;
uniform vec4 corner_radius;
uniform float border_width;
uniform float scale;
float rounding_alpha(vec2 coords, vec2 half_size, vec4 radii) {
float radius;
vec2 center;
if (coords.x < half_size.x && coords.y < half_size.y) {
radius = radii.x; // top-left
center = vec2(radius, radius);
} else if (coords.x >= half_size.x && coords.y < half_size.y) {
radius = radii.y; // top-right
center = vec2(size.x - radius, radius);
} else if (coords.x >= half_size.x && coords.y >= half_size.y) {
radius = radii.z; // bottom-right
center = vec2(size.x - radius, size.y - radius);
} else {
radius = radii.w; // bottom-left
center = vec2(radius, size.y - radius);
}
if (radius == 0.0)
return 1.0;
float dist = distance(coords, center);
float half_px = 0.5 / scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec2 half_size = size / 2.0;
float outer_alpha = rounding_alpha(v_geo, half_size, corner_radius);
float inner_alpha = 0.0;
if (border_width > 0.0) {
vec2 inner_coords = v_geo - vec2(border_width);
vec2 inner_size = size - vec2(border_width * 2.0);
vec2 inner_half = inner_size / 2.0;
vec4 inner_radii = max(corner_radius - vec4(border_width), vec4(0.0));
if (inner_coords.x >= 0.0 && inner_coords.y >= 0.0 &&
inner_coords.x <= inner_size.x && inner_coords.y <= inner_size.y) {
inner_alpha = rounding_alpha(inner_coords, inner_half, inner_radii);
}
}
float a = outer_alpha * (1.0 - inner_alpha);
gl_FragColor = color * a;
}

View file

@ -0,0 +1,8 @@
attribute vec2 pos;
attribute vec2 geo;
varying vec2 v_geo;
void main() {
gl_Position = vec4(pos, 0.0, 1.0);
v_geo = geo;
}

View file

@ -0,0 +1,71 @@
#ifdef EXTERNAL
#extension GL_OES_EGL_image_external : require
#endif
precision mediump float;
varying vec2 v_texcoord;
varying vec2 v_geo;
#ifdef EXTERNAL
uniform samplerExternalOES tex;
#else
uniform sampler2D tex;
#endif
#ifdef ALPHA_MULTIPLIER
uniform float alpha;
#endif
uniform vec2 size;
uniform vec4 corner_radius;
uniform float scale;
float rounding_alpha(vec2 coords, vec2 half_size, vec4 radii) {
float radius;
vec2 center;
if (coords.x < half_size.x && coords.y < half_size.y) {
radius = radii.x; // top-left
center = vec2(radius, radius);
} else if (coords.x >= half_size.x && coords.y < half_size.y) {
radius = radii.y; // top-right
center = vec2(size.x - radius, radius);
} else if (coords.x >= half_size.x && coords.y >= half_size.y) {
radius = radii.z; // bottom-right
center = vec2(size.x - radius, size.y - radius);
} else {
radius = radii.w; // bottom-left
center = vec2(radius, size.y - radius);
}
if (radius == 0.0)
return 1.0;
float dist = distance(coords, center);
float half_px = 0.5 / scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec2 half_size = size / 2.0;
float ra = rounding_alpha(v_geo, half_size, corner_radius);
#ifdef ALPHA
#ifdef ALPHA_MULTIPLIER
gl_FragColor = texture2D(tex, v_texcoord) * alpha * ra;
#else // !ALPHA_MULTIPLIER
gl_FragColor = texture2D(tex, v_texcoord) * ra;
#endif // ALPHA_MULTIPLIER
#else // !ALPHA
#ifdef ALPHA_MULTIPLIER
vec4 tc = texture2D(tex, v_texcoord);
gl_FragColor = vec4(tc.rgb * alpha * ra, alpha * ra);
#else // !ALPHA_MULTIPLIER
vec4 tc = texture2D(tex, v_texcoord);
gl_FragColor = vec4(tc.rgb * ra, ra);
#endif // ALPHA_MULTIPLIER
#endif // ALPHA
}

View file

@ -0,0 +1,11 @@
attribute vec2 pos;
attribute vec2 texcoord;
attribute vec2 geo;
varying vec2 v_texcoord;
varying vec2 v_geo;
void main() {
gl_Position = vec4(pos, 0.0, 1.0);
v_texcoord = texcoord;
v_geo = geo;
}

View file

@ -27,9 +27,14 @@ use {
semaphore::VulkanSemaphore, semaphore::VulkanSemaphore,
shaders::{ shaders::{
ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants, ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants,
InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_TEX_FRAG, LEGACY_TEX_VERT, InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG,
LegacyFillPushConstants, LegacyTexPushConstants, OUT_FRAG, OUT_VERT, LEGACY_ROUNDED_FILL_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT,
OutPushConstants, TEX_FRAG, TEX_VERT, TexPushConstants, TexVertex, VulkanShader, LEGACY_TEX_FRAG, LEGACY_TEX_VERT, LegacyFillPushConstants,
LegacyRoundedFillPushConstants, LegacyRoundedTexPushConstants,
LegacyTexPushConstants, OUT_FRAG, OUT_VERT, OutPushConstants, ROUNDED_FILL_FRAG,
ROUNDED_FILL_VERT, ROUNDED_TEX_FRAG, ROUNDED_TEX_VERT, RoundedFillPushConstants,
RoundedTexPushConstants, TEX_FRAG, TEX_VERT, TexPushConstants, TexVertex,
VulkanShader,
}, },
}, },
io_uring::IoUring, io_uring::IoUring,
@ -104,6 +109,13 @@ pub struct VulkanRenderer {
pub(super) tex_frag_shader: Rc<VulkanShader>, pub(super) tex_frag_shader: Rc<VulkanShader>,
pub(super) out_vert_shader: Option<Rc<VulkanShader>>, pub(super) out_vert_shader: Option<Rc<VulkanShader>>,
pub(super) out_frag_shader: Option<Rc<VulkanShader>>, pub(super) out_frag_shader: Option<Rc<VulkanShader>>,
pub(super) rounded_fill_vert_shader: Rc<VulkanShader>,
pub(super) rounded_fill_frag_shader: Rc<VulkanShader>,
pub(super) rounded_tex_vert_shader: Rc<VulkanShader>,
pub(super) rounded_tex_frag_shader: Rc<VulkanShader>,
pub(super) rounded_fill_pipelines: CopyHashMap<vk::Format, FillPipelines>,
pub(super) rounded_tex_pipelines:
StaticMap<VulkanEotf, CopyHashMap<vk::Format, Rc<TexPipelines>>>,
pub(super) tex_descriptor_set_layouts: ArrayVec<Rc<VulkanDescriptorSetLayout>, 2>, pub(super) tex_descriptor_set_layouts: ArrayVec<Rc<VulkanDescriptorSetLayout>, 2>,
pub(super) out_descriptor_set_layout: Option<Rc<VulkanDescriptorSetLayout>>, pub(super) out_descriptor_set_layout: Option<Rc<VulkanDescriptorSetLayout>>,
pub(super) defunct: Cell<bool>, pub(super) defunct: Cell<bool>,
@ -202,6 +214,8 @@ type Point = [[f32; 2]; 4];
enum VulkanOp { enum VulkanOp {
Fill(VulkanFillOp), Fill(VulkanFillOp),
Tex(VulkanTexOp), Tex(VulkanTexOp),
RoundedFill(VulkanRoundedFillOp),
RoundedTex(VulkanRoundedTexOp),
} }
struct VulkanTexOp { struct VulkanTexOp {
@ -231,6 +245,40 @@ struct VulkanFillOp {
instances: u32, instances: u32,
} }
struct VulkanRoundedFillOp {
target: Point,
color: [f32; 4],
source_type: TexSourceType,
size: [f32; 2],
corner_radius: [f32; 4],
border_width: f32,
scale: f32,
range_address: DeviceAddress,
z_order: u32,
}
struct VulkanRoundedTexOp {
tex: Rc<VulkanImage>,
index: usize,
target: Point,
source: Point,
buffer_resv: Option<Rc<dyn BufferResv>>,
acquire_sync: Option<AcquireSync>,
release_sync: ReleaseSync,
alpha: f32,
source_type: TexSourceType,
copy_type: TexCopyType,
alpha_mode: AlphaMode,
tex_cd: Rc<ColorDescription>,
color_management_data_address: Option<DeviceAddress>,
eotf_args_address: Option<DeviceAddress>,
resource_descriptor_buffer_offset: DeviceAddress,
size: [f32; 2],
corner_radius: [f32; 4],
scale: f32,
range_address: DeviceAddress,
}
#[derive(Copy, Clone, Debug, Linearize, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Linearize, Eq, PartialEq)]
pub(super) enum RenderPass { pub(super) enum RenderPass {
BlendBuffer, BlendBuffer,
@ -295,12 +343,20 @@ impl VulkanDevice {
let tex_frag_shader; let tex_frag_shader;
let out_vert_shader; let out_vert_shader;
let out_frag_shader; let out_frag_shader;
let rounded_fill_vert_shader;
let rounded_fill_frag_shader;
let rounded_tex_vert_shader;
let rounded_tex_frag_shader;
let mut tex_descriptor_set_layouts = ArrayVec::new(); let mut tex_descriptor_set_layouts = ArrayVec::new();
if self.descriptor_buffer.is_some() { if self.descriptor_buffer.is_some() {
tex_vert_shader = self.create_shader(TEX_VERT)?; tex_vert_shader = self.create_shader(TEX_VERT)?;
tex_frag_shader = self.create_shader(TEX_FRAG)?; tex_frag_shader = self.create_shader(TEX_FRAG)?;
fill_vert_shader = self.create_shader(FILL_VERT)?; fill_vert_shader = self.create_shader(FILL_VERT)?;
fill_frag_shader = self.create_shader(FILL_FRAG)?; fill_frag_shader = self.create_shader(FILL_FRAG)?;
rounded_fill_vert_shader = self.create_shader(ROUNDED_FILL_VERT)?;
rounded_fill_frag_shader = self.create_shader(ROUNDED_FILL_FRAG)?;
rounded_tex_vert_shader = self.create_shader(ROUNDED_TEX_VERT)?;
rounded_tex_frag_shader = self.create_shader(ROUNDED_TEX_FRAG)?;
out_vert_shader = Some(self.create_shader(OUT_VERT)?); out_vert_shader = Some(self.create_shader(OUT_VERT)?);
out_frag_shader = Some(self.create_shader(OUT_FRAG)?); out_frag_shader = Some(self.create_shader(OUT_FRAG)?);
tex_descriptor_set_layouts tex_descriptor_set_layouts
@ -311,6 +367,10 @@ impl VulkanDevice {
tex_frag_shader = self.create_shader(LEGACY_TEX_FRAG)?; tex_frag_shader = self.create_shader(LEGACY_TEX_FRAG)?;
fill_vert_shader = self.create_shader(LEGACY_FILL_VERT)?; fill_vert_shader = self.create_shader(LEGACY_FILL_VERT)?;
fill_frag_shader = self.create_shader(LEGACY_FILL_FRAG)?; fill_frag_shader = self.create_shader(LEGACY_FILL_FRAG)?;
rounded_fill_vert_shader = self.create_shader(LEGACY_ROUNDED_FILL_VERT)?;
rounded_fill_frag_shader = self.create_shader(LEGACY_ROUNDED_FILL_FRAG)?;
rounded_tex_vert_shader = self.create_shader(LEGACY_ROUNDED_TEX_VERT)?;
rounded_tex_frag_shader = self.create_shader(LEGACY_ROUNDED_TEX_FRAG)?;
out_vert_shader = None; out_vert_shader = None;
out_frag_shader = None; out_frag_shader = None;
tex_descriptor_set_layouts tex_descriptor_set_layouts
@ -400,6 +460,12 @@ impl VulkanDevice {
tex_frag_shader, tex_frag_shader,
out_vert_shader, out_vert_shader,
out_frag_shader, out_frag_shader,
rounded_fill_vert_shader,
rounded_fill_frag_shader,
rounded_tex_vert_shader,
rounded_tex_frag_shader,
rounded_fill_pipelines: Default::default(),
rounded_tex_pipelines: Default::default(),
tex_descriptor_set_layouts, tex_descriptor_set_layouts,
out_descriptor_set_layout, out_descriptor_set_layout,
defunct: Cell::new(false), defunct: Cell::new(false),
@ -457,6 +523,112 @@ impl VulkanRenderer {
Ok(fill_pipelines) Ok(fill_pipelines)
} }
fn get_or_create_rounded_fill_pipelines(
&self,
format: vk::Format,
) -> Result<FillPipelines, VulkanError> {
if let Some(pl) = self.rounded_fill_pipelines.get(&format) {
return Ok(pl);
}
let create_pipeline = |src_has_alpha| {
let push_size = if self.device.descriptor_buffer.is_some() {
size_of::<RoundedFillPushConstants>()
} else {
size_of::<LegacyRoundedFillPushConstants>()
};
let info = PipelineCreateInfo {
format,
vert: self.rounded_fill_vert_shader.clone(),
frag: self.rounded_fill_frag_shader.clone(),
blend: src_has_alpha,
src_has_alpha,
has_alpha_mult: false,
alpha_mode: AlphaMode::PremultipliedOptical,
eotf: EOTF_LINEAR,
inv_eotf: EOTF_LINEAR,
descriptor_set_layouts: Default::default(),
has_color_management_data: false,
};
self.device.create_pipeline2(info, push_size)
};
let pipelines = Rc::new(static_map! {
TexSourceType::HasAlpha => create_pipeline(true)?,
TexSourceType::Opaque => create_pipeline(false)?,
});
self.rounded_fill_pipelines.set(format, pipelines.clone());
Ok(pipelines)
}
fn get_or_create_rounded_tex_pipelines(
&self,
format: vk::Format,
target_cd: &ColorDescription,
) -> Rc<TexPipelines> {
let eotf = target_cd.eotf.to_vulkan();
let pipelines = &self.rounded_tex_pipelines[eotf];
match pipelines.get(&format) {
Some(pl) => pl,
_ => {
let pl = Rc::new(TexPipelines {
format,
eotf,
pipelines: Default::default(),
});
pipelines.set(format, pl.clone());
pl
}
}
}
fn get_or_create_rounded_tex_pipeline(
&self,
pipelines: &TexPipelines,
tex_cd: &ColorDescription,
tex_copy_type: TexCopyType,
tex_source_type: TexSourceType,
mut tex_alpha_mode: AlphaMode,
has_color_management_data: bool,
) -> Result<Rc<VulkanPipeline>, VulkanError> {
if tex_source_type == TexSourceType::Opaque {
tex_alpha_mode = AlphaMode::PremultipliedElectrical;
}
let key = TexPipelineKey {
tex_copy_type,
tex_source_type,
tex_alpha_mode,
eotf: tex_cd.eotf.to_vulkan(),
has_color_management_data,
};
if let Some(pl) = pipelines.pipelines.get(&key) {
return Ok(pl);
}
let has_alpha_mult = match tex_copy_type {
TexCopyType::Identity => false,
TexCopyType::Multiply => true,
};
let push_size = if self.device.descriptor_buffer.is_some() {
size_of::<RoundedTexPushConstants>()
} else {
size_of::<LegacyRoundedTexPushConstants>()
};
let info = PipelineCreateInfo {
format: pipelines.format,
vert: self.rounded_tex_vert_shader.clone(),
frag: self.rounded_tex_frag_shader.clone(),
blend: true, // always blend since corners are transparent
src_has_alpha: true, // rounding makes everything have alpha
has_alpha_mult,
alpha_mode: key.tex_alpha_mode,
eotf: key.eotf.to_vulkan(),
inv_eotf: pipelines.eotf.to_vulkan(),
descriptor_set_layouts: self.tex_descriptor_set_layouts.clone(),
has_color_management_data,
};
let pl = self.device.create_pipeline2(info, push_size)?;
pipelines.pipelines.set(key, pl.clone());
Ok(pl)
}
fn get_or_create_tex_pipelines( fn get_or_create_tex_pipelines(
&self, &self,
format: vk::Format, format: vk::Format,
@ -665,23 +837,34 @@ impl VulkanRenderer {
RenderPass::FrameBuffer => fb_inv_eotf_args_descriptor, RenderPass::FrameBuffer => fb_inv_eotf_args_descriptor,
}; };
for cmd in &mut memory.ops[pass] { for cmd in &mut memory.ops[pass] {
let VulkanOp::Tex(c) = cmd else { let (tex, resource_offset, cm_addr, eotf_addr) = match cmd {
continue; VulkanOp::Tex(c) => (
&c.tex,
&mut c.resource_descriptor_buffer_offset,
&c.color_management_data_address,
&c.eotf_args_address,
),
VulkanOp::RoundedTex(c) => (
&c.tex,
&mut c.resource_descriptor_buffer_offset,
&c.color_management_data_address,
&c.eotf_args_address,
),
_ => continue,
}; };
let tex = &c.tex; *resource_offset = resource_writer.next_offset();
c.resource_descriptor_buffer_offset = resource_writer.next_offset();
let mut writer = resource_writer.add_set(tex_descriptor_set_layout); let mut writer = resource_writer.add_set(tex_descriptor_set_layout);
writer.write( writer.write(
tex_descriptor_set_layout.offsets[0], tex_descriptor_set_layout.offsets[0],
tex.sampled_image_descriptor.as_ref().unwrap(), tex.sampled_image_descriptor.as_ref().unwrap(),
); );
if let Some(addr) = c.color_management_data_address { if let Some(addr) = *cm_addr {
writer.write( writer.write(
tex_descriptor_set_layout.offsets[1], tex_descriptor_set_layout.offsets[1],
get_ub_descriptor!(addr, ColorManagementData), get_ub_descriptor!(addr, ColorManagementData),
); );
} }
if let Some(addr) = c.eotf_args_address { if let Some(addr) = *eotf_addr {
writer.write( writer.write(
tex_descriptor_set_layout.offsets[2], tex_descriptor_set_layout.offsets[2],
get_ub_descriptor!(addr, EotfArgs), get_ub_descriptor!(addr, EotfArgs),
@ -741,12 +924,19 @@ impl VulkanRenderer {
enum Key { enum Key {
Fill { color: [u32; 4] }, Fill { color: [u32; 4] },
Tex(usize), Tex(usize),
RoundedFill { z_order: u32, color: [u32; 4] },
RoundedTex(usize),
} }
match o { match o {
VulkanOp::Fill(f) => Key::Fill { VulkanOp::Fill(f) => Key::Fill {
color: f.color.map(|c| c.to_bits()), color: f.color.map(|c| c.to_bits()),
}, },
VulkanOp::Tex(t) => Key::Tex(t.index), VulkanOp::Tex(t) => Key::Tex(t.index),
VulkanOp::RoundedFill(f) => Key::RoundedFill {
z_order: f.z_order,
color: f.color.map(|c| c.to_bits()),
},
VulkanOp::RoundedTex(t) => Key::RoundedTex(t.index),
} }
}); });
let mops = &mut memory.ops[pass]; let mops = &mut memory.ops[pass];
@ -782,6 +972,22 @@ impl VulkanRenderer {
} }
mops.push(VulkanOp::Tex(c)); mops.push(VulkanOp::Tex(c));
} }
VulkanOp::RoundedFill(mut f) => {
f.range_address = memory.data_buffer.len() as DeviceAddress;
memory.data_buffer.extend_from_slice(uapi::as_bytes(&f.target));
mops.push(VulkanOp::RoundedFill(f));
}
VulkanOp::RoundedTex(mut c) => {
c.range_address = memory.data_buffer.len() as DeviceAddress;
let vertex = TexVertex {
pos: c.target,
tex_pos: c.source,
};
memory
.data_buffer
.extend_from_slice(uapi::as_bytes(&vertex));
mops.push(VulkanOp::RoundedTex(c));
}
} }
} }
} }
@ -917,6 +1123,106 @@ impl VulkanRenderer {
})); }));
} }
} }
GfxApiOpt::RoundedFillRect(rf) => {
let target = rf.rect.to_points();
for pass in RenderPass::variants() {
let Some(bounds) = memory.paint_bounds[pass] else {
continue;
};
if !bounds.intersects(&target) {
continue;
}
let target_cd = match pass {
RenderPass::BlendBuffer => blend_cd,
RenderPass::FrameBuffer => fb_cd,
};
let tf = target_cd.eotf;
let color = memory.color_transforms.apply_to_color(
&rf.cd,
target_cd,
rf.render_intent,
rf.color,
);
let color = color.to_array2(tf, rf.alpha);
let source_type = TexSourceType::HasAlpha;
memory.ops_tmp[pass].push(VulkanOp::RoundedFill(VulkanRoundedFillOp {
target,
color,
source_type,
size: rf.size,
corner_radius: rf.corner_radius,
border_width: rf.border_width,
scale: rf.scale,
range_address: 0,
z_order: rf.z_order,
}));
}
}
GfxApiOpt::RoundedCopyTexture(ct) => {
let tex = ct.tex.clone().into_vk(&self.device.device)?;
if tex.contents_are_undefined.get() {
log::warn!("Ignoring undefined texture");
continue;
}
if tex.queue_state.get().acquire(QueueFamily::Gfx) == QueueTransfer::Impossible
{
log::warn!("Ignoring texture owned by different queue");
continue;
}
let target = ct.target.to_points();
let source = ct.source.to_points();
for pass in RenderPass::variants() {
let Some(bounds) = memory.paint_bounds[pass] else {
continue;
};
if !bounds.intersects(&target) {
continue;
}
let copy_type = match ct.alpha.is_some() {
true => TexCopyType::Multiply,
false => TexCopyType::Identity,
};
let source_type = TexSourceType::HasAlpha;
let target_cd = match pass {
RenderPass::BlendBuffer => blend_cd,
RenderPass::FrameBuffer => fb_cd,
};
let color_management_data_address = memory.color_transforms.get_offset(
&ct.cd.linear,
target_cd,
ct.render_intent,
self.device.uniform_buffer_offset_mask,
&mut memory.uniform_buffer_writer,
);
let eotf_args_address = memory.eotf_args_cache.get_offset(
&ct.cd,
false,
self.device.uniform_buffer_offset_mask,
&mut memory.uniform_buffer_writer,
);
memory.ops_tmp[pass].push(VulkanOp::RoundedTex(VulkanRoundedTexOp {
tex: tex.clone(),
index,
target,
source,
buffer_resv: ct.buffer_resv.clone(),
acquire_sync: Some(ct.acquire_sync.clone()),
release_sync: ct.release_sync,
alpha: ct.alpha.unwrap_or_default(),
source_type,
copy_type,
alpha_mode: ct.alpha_mode,
tex_cd: ct.cd.clone(),
color_management_data_address,
eotf_args_address,
resource_descriptor_buffer_offset: 0,
size: ct.size,
corner_radius: ct.corner_radius,
scale: ct.scale,
range_address: 0,
}));
}
}
} }
} }
sync(memory); sync(memory);
@ -998,6 +1304,12 @@ impl VulkanRenderer {
VulkanOp::Tex(c) => { VulkanOp::Tex(c) => {
c.range_address += buffer.buffer.address; c.range_address += buffer.buffer.address;
} }
VulkanOp::RoundedFill(f) => {
f.range_address += buffer.buffer.address;
}
VulkanOp::RoundedTex(c) => {
c.range_address += buffer.buffer.address;
}
} }
} }
} }
@ -1029,9 +1341,16 @@ impl VulkanRenderer {
} }
for ops in memory.ops.values_mut() { for ops in memory.ops.values_mut() {
for op in ops { for op in ops {
if let VulkanOp::Tex(c) = op { match op {
adj!(&mut c.color_management_data_address); VulkanOp::Tex(c) => {
adj!(&mut c.eotf_args_address); adj!(&mut c.color_management_data_address);
adj!(&mut c.eotf_args_address);
}
VulkanOp::RoundedTex(c) => {
adj!(&mut c.color_management_data_address);
adj!(&mut c.eotf_args_address);
}
_ => {}
} }
} }
} }
@ -1051,8 +1370,12 @@ impl VulkanRenderer {
let execution = self.allocate_point(); let execution = self.allocate_point();
for pass in RenderPass::variants() { for pass in RenderPass::variants() {
for cmd in &mut memory.ops[pass] { for cmd in &mut memory.ops[pass] {
if let VulkanOp::Tex(c) = cmd { let tex_data = match cmd {
let tex = &c.tex; VulkanOp::Tex(c) => Some((&c.tex, &mut c.buffer_resv, &mut c.acquire_sync, c.release_sync)),
VulkanOp::RoundedTex(c) => Some((&c.tex, &mut c.buffer_resv, &mut c.acquire_sync, c.release_sync)),
_ => None,
};
if let Some((tex, buffer_resv, acquire_sync, release_sync)) = tex_data {
if tex.execution_version.replace(execution) == execution { if tex.execution_version.replace(execution) == execution {
continue; continue;
} }
@ -1066,9 +1389,9 @@ impl VulkanRenderer {
} }
memory.textures.push(UsedTexture { memory.textures.push(UsedTexture {
tex: tex.clone(), tex: tex.clone(),
resv: c.buffer_resv.take(), resv: buffer_resv.take(),
acquire_sync: c.acquire_sync.take().unwrap(), acquire_sync: acquire_sync.take().unwrap(),
release_sync: c.release_sync, release_sync,
}); });
} }
} }
@ -1301,6 +1624,10 @@ impl VulkanRenderer {
let memory = &*self.memory.borrow(); let memory = &*self.memory.borrow();
let fill_pl = self.get_or_create_fill_pipelines(target.format.vk_format)?; let fill_pl = self.get_or_create_fill_pipelines(target.format.vk_format)?;
let tex_pl = self.get_or_create_tex_pipelines(target.format.vk_format, target_cd); let tex_pl = self.get_or_create_tex_pipelines(target.format.vk_format, target_cd);
let rounded_fill_pl =
self.get_or_create_rounded_fill_pipelines(target.format.vk_format)?;
let rounded_tex_pl =
self.get_or_create_rounded_tex_pipelines(target.format.vk_format, target_cd);
let dev = &self.device.device; let dev = &self.device.device;
let mut current_pipeline = None; let mut current_pipeline = None;
let mut bind = |pipeline: &VulkanPipeline| { let mut bind = |pipeline: &VulkanPipeline| {
@ -1421,6 +1748,134 @@ impl VulkanRenderer {
} }
} }
} }
VulkanOp::RoundedFill(r) => {
let pipeline = &rounded_fill_pl[r.source_type];
bind(pipeline);
if self.device.descriptor_buffer.is_some() {
let push = RoundedFillPushConstants {
color: r.color,
vertices: r.range_address,
size: r.size,
corner_radius: r.corner_radius,
border_width: r.border_width,
scale: r.scale,
};
unsafe {
dev.cmd_push_constants(
buf,
pipeline.pipeline_layout,
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
0,
uapi::as_bytes(&push),
);
dev.cmd_draw(buf, 4, 1, 0, 0);
}
} else {
let push = LegacyRoundedFillPushConstants {
pos: r.target,
color: r.color,
size_x: r.size[0],
size_y: r.size[1],
corner_radius_tl: r.corner_radius[0],
corner_radius_tr: r.corner_radius[1],
corner_radius_br: r.corner_radius[2],
corner_radius_bl: r.corner_radius[3],
border_width: r.border_width,
scale: r.scale,
};
unsafe {
dev.cmd_push_constants(
buf,
pipeline.pipeline_layout,
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
0,
uapi::as_bytes(&push),
);
dev.cmd_draw(buf, 4, 1, 0, 0);
}
}
}
VulkanOp::RoundedTex(c) => {
let tex = &c.tex;
let pipeline = self.get_or_create_rounded_tex_pipeline(
&rounded_tex_pl,
&c.tex_cd,
c.copy_type,
c.source_type,
c.alpha_mode,
c.color_management_data_address.is_some(),
)?;
bind(&pipeline);
let image_info = DescriptorImageInfo::default()
.image_view(tex.texture_view)
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
if let Some(db) = &self.device.descriptor_buffer {
let push = RoundedTexPushConstants {
vertices: c.range_address,
alpha: c.alpha,
size_x: c.size[0],
size_y: c.size[1],
corner_radius_tl: c.corner_radius[0],
corner_radius_tr: c.corner_radius[1],
corner_radius_br: c.corner_radius[2],
corner_radius_bl: c.corner_radius[3],
scale: c.scale,
};
unsafe {
db.cmd_set_descriptor_buffer_offsets(
buf,
PipelineBindPoint::GRAPHICS,
pipeline.pipeline_layout,
0,
&[0, 1],
&[0, c.resource_descriptor_buffer_offset],
);
dev.cmd_push_constants(
buf,
pipeline.pipeline_layout,
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
0,
uapi::as_bytes(&push),
);
dev.cmd_draw(buf, 4, 1, 0, 0);
}
} else {
let write_descriptor_set = WriteDescriptorSet::default()
.descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(slice::from_ref(&image_info));
unsafe {
self.device.push_descriptor.cmd_push_descriptor_set(
buf,
PipelineBindPoint::GRAPHICS,
pipeline.pipeline_layout,
0,
slice::from_ref(&write_descriptor_set),
);
}
let push = LegacyRoundedTexPushConstants {
pos: c.target,
tex_pos: c.source,
alpha: c.alpha,
size_x: c.size[0],
size_y: c.size[1],
corner_radius_tl: c.corner_radius[0],
corner_radius_tr: c.corner_radius[1],
corner_radius_br: c.corner_radius[2],
corner_radius_bl: c.corner_radius[3],
scale: c.scale,
};
unsafe {
dev.cmd_push_constants(
buf,
pipeline.pipeline_layout,
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
0,
uapi::as_bytes(&push),
);
dev.cmd_draw(buf, 4, 1, 0, 0);
}
}
}
} }
} }
Ok(()) Ok(())
@ -1933,6 +2388,14 @@ impl VulkanRenderer {
}; };
(opaque, c.target) (opaque, c.target)
} }
GfxApiOpt::RoundedFillRect(rf) => {
// Rounded rects are never fully opaque due to AA at corners,
// but they do paint pixels and need paint regions.
(false, rf.rect)
}
GfxApiOpt::RoundedCopyTexture(ct) => {
(false, ct.target)
}
}; };
if opaque || bb.is_none() { if opaque || bb.is_none() {
tag |= 1; tag |= 1;

View file

@ -15,6 +15,18 @@ pub const LEGACY_FILL_VERT: &[u8] = include_bytes!("shaders_bin/legacy_fill.vert
pub const LEGACY_FILL_FRAG: &[u8] = include_bytes!("shaders_bin/legacy_fill.frag.spv"); pub const LEGACY_FILL_FRAG: &[u8] = include_bytes!("shaders_bin/legacy_fill.frag.spv");
pub const LEGACY_TEX_VERT: &[u8] = include_bytes!("shaders_bin/legacy_tex.vert.spv"); pub const LEGACY_TEX_VERT: &[u8] = include_bytes!("shaders_bin/legacy_tex.vert.spv");
pub const LEGACY_TEX_FRAG: &[u8] = include_bytes!("shaders_bin/legacy_tex.frag.spv"); pub const LEGACY_TEX_FRAG: &[u8] = include_bytes!("shaders_bin/legacy_tex.frag.spv");
pub const ROUNDED_FILL_VERT: &[u8] = include_bytes!("shaders_bin/rounded_fill.vert.spv");
pub const ROUNDED_FILL_FRAG: &[u8] = include_bytes!("shaders_bin/rounded_fill.frag.spv");
pub const ROUNDED_TEX_VERT: &[u8] = include_bytes!("shaders_bin/rounded_tex.vert.spv");
pub const ROUNDED_TEX_FRAG: &[u8] = include_bytes!("shaders_bin/rounded_tex.frag.spv");
pub const LEGACY_ROUNDED_FILL_VERT: &[u8] =
include_bytes!("shaders_bin/legacy_rounded_fill.vert.spv");
pub const LEGACY_ROUNDED_FILL_FRAG: &[u8] =
include_bytes!("shaders_bin/legacy_rounded_fill.frag.spv");
pub const LEGACY_ROUNDED_TEX_VERT: &[u8] =
include_bytes!("shaders_bin/legacy_rounded_tex.vert.spv");
pub const LEGACY_ROUNDED_TEX_FRAG: &[u8] =
include_bytes!("shaders_bin/legacy_rounded_tex.frag.spv");
pub struct VulkanShader { pub struct VulkanShader {
pub(super) device: Rc<VulkanDevice>, pub(super) device: Rc<VulkanDevice>,
@ -99,6 +111,69 @@ pub struct LegacyTexPushConstants {
unsafe impl Packed for LegacyTexPushConstants {} unsafe impl Packed for LegacyTexPushConstants {}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct RoundedFillPushConstants {
pub color: [f32; 4],
pub vertices: DeviceAddress,
pub size: [f32; 2],
pub corner_radius: [f32; 4],
pub border_width: f32,
pub scale: f32,
}
unsafe impl Packed for RoundedFillPushConstants {}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct LegacyRoundedFillPushConstants {
pub pos: [[f32; 2]; 4],
pub color: [f32; 4],
pub size_x: f32,
pub size_y: f32,
pub corner_radius_tl: f32,
pub corner_radius_tr: f32,
pub corner_radius_br: f32,
pub corner_radius_bl: f32,
pub border_width: f32,
pub scale: f32,
}
unsafe impl Packed for LegacyRoundedFillPushConstants {}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct RoundedTexPushConstants {
pub vertices: DeviceAddress,
pub alpha: f32,
pub size_x: f32,
pub size_y: f32,
pub corner_radius_tl: f32,
pub corner_radius_tr: f32,
pub corner_radius_br: f32,
pub corner_radius_bl: f32,
pub scale: f32,
}
unsafe impl Packed for RoundedTexPushConstants {}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct LegacyRoundedTexPushConstants {
pub pos: [[f32; 2]; 4],
pub tex_pos: [[f32; 2]; 4],
pub alpha: f32,
pub size_x: f32,
pub size_y: f32,
pub corner_radius_tl: f32,
pub corner_radius_tr: f32,
pub corner_radius_br: f32,
pub corner_radius_bl: f32,
pub scale: f32,
}
unsafe impl Packed for LegacyRoundedTexPushConstants {}
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
#[repr(C)] #[repr(C)]
pub struct OutPushConstants { pub struct OutPushConstants {

View file

@ -0,0 +1,12 @@
layout(push_constant, std430) uniform Data {
layout(offset = 0) vec2 pos[4];
layout(offset = 32) vec4 color;
layout(offset = 48) float size_x;
layout(offset = 52) float size_y;
layout(offset = 56) float corner_radius_tl;
layout(offset = 60) float corner_radius_tr;
layout(offset = 64) float corner_radius_br;
layout(offset = 68) float corner_radius_bl;
layout(offset = 72) float border_width;
layout(offset = 76) float scale;
} data;

View file

@ -0,0 +1,48 @@
#version 450
#include "../frag_spec_const.glsl"
#include "rounded_fill.common.glsl"
layout(location = 0) in vec2 geo_pos;
layout(location = 0) out vec4 out_color;
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
if (coords.x < 0.0 || coords.y < 0.0 || coords.x > size.x || coords.y > size.y) {
return 0.0;
}
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (coords.x > size.x - corner_radius.y && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (coords.x > size.x - corner_radius.z && coords.y > size.y - corner_radius.z) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && coords.y > size.y - corner_radius.w) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float half_px = 0.5 / data.scale;
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec2 size = vec2(data.size_x, data.size_y);
vec4 corner_radius = vec4(data.corner_radius_tl, data.corner_radius_tr, data.corner_radius_br, data.corner_radius_bl);
float outer_alpha = rounding_alpha(geo_pos, size, corner_radius);
float alpha = outer_alpha;
if (data.border_width > 0.0) {
vec2 inner_coords = geo_pos - vec2(data.border_width);
vec2 inner_size = size - vec2(2.0 * data.border_width);
vec4 inner_radius = max(corner_radius - vec4(data.border_width), vec4(0.0));
float inner_alpha = rounding_alpha(inner_coords, inner_size, inner_radius);
alpha = outer_alpha * (1.0 - inner_alpha);
}
out_color = data.color * alpha;
}

View file

@ -0,0 +1,17 @@
#version 450
#include "rounded_fill.common.glsl"
layout(location = 0) out vec2 geo_pos;
void main() {
vec2 size = vec2(data.size_x, data.size_y);
vec2 pos;
switch (gl_VertexIndex) {
case 0: pos = data.pos[0]; geo_pos = vec2(size.x, 0.0); break;
case 1: pos = data.pos[1]; geo_pos = vec2(0.0, 0.0); break;
case 2: pos = data.pos[2]; geo_pos = vec2(size.x, size.y); break;
case 3: pos = data.pos[3]; geo_pos = vec2(0.0, size.y); break;
}
gl_Position = vec4(pos, 0.0, 1.0);
}

View file

@ -0,0 +1,12 @@
layout(push_constant, std430) uniform Data {
layout(offset = 0) vec2 pos[4];
layout(offset = 32) vec2 tex_pos[4];
layout(offset = 64) float mul;
layout(offset = 68) float size_x;
layout(offset = 72) float size_y;
layout(offset = 76) float corner_radius_tl;
layout(offset = 80) float corner_radius_tr;
layout(offset = 84) float corner_radius_br;
layout(offset = 88) float corner_radius_bl;
layout(offset = 92) float scale;
} data;

View file

@ -0,0 +1,51 @@
#version 450
#include "../frag_spec_const.glsl"
#include "rounded_tex.common.glsl"
layout(set = 0, binding = 0) uniform sampler2D tex;
layout(location = 0) in vec2 tex_pos;
layout(location = 1) in vec2 geo_pos;
layout(location = 0) out vec4 out_color;
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
if (coords.x < 0.0 || coords.y < 0.0 || coords.x > size.x || coords.y > size.y) {
return 0.0;
}
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (coords.x > size.x - corner_radius.y && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (coords.x > size.x - corner_radius.z && coords.y > size.y - corner_radius.z) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && coords.y > size.y - corner_radius.w) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float half_px = 0.5 / data.scale;
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec2 size = vec2(data.size_x, data.size_y);
vec4 corner_radius = vec4(data.corner_radius_tl, data.corner_radius_tr, data.corner_radius_br, data.corner_radius_bl);
vec4 c = textureLod(tex, tex_pos, 0);
if (has_alpha_multiplier) {
if (src_has_alpha) {
c *= data.mul;
} else {
c = vec4(c.rgb * data.mul, data.mul);
}
}
float ra = rounding_alpha(geo_pos, size, corner_radius);
c *= ra;
out_color = c;
}

View file

@ -0,0 +1,18 @@
#version 450
#include "rounded_tex.common.glsl"
layout(location = 0) out vec2 tex_pos;
layout(location = 1) out vec2 geo_pos;
void main() {
vec2 size = vec2(data.size_x, data.size_y);
vec2 pos;
switch (gl_VertexIndex) {
case 0: pos = data.pos[0]; tex_pos = data.tex_pos[0]; geo_pos = vec2(size.x, 0.0); break;
case 1: pos = data.pos[1]; tex_pos = data.tex_pos[1]; geo_pos = vec2(0.0, 0.0); break;
case 2: pos = data.pos[2]; tex_pos = data.tex_pos[2]; geo_pos = vec2(size.x, size.y); break;
case 3: pos = data.pos[3]; tex_pos = data.tex_pos[3]; geo_pos = vec2(0.0, size.y); break;
}
gl_Position = vec4(pos, 0.0, 1.0);
}

View file

@ -0,0 +1,14 @@
#extension GL_EXT_buffer_reference : require
layout(buffer_reference, buffer_reference_align = 8, std430) readonly buffer Vertices {
vec2 pos[][4];
};
layout(push_constant, std430) uniform Data {
vec4 color;
Vertices vertices;
vec2 size;
vec4 corner_radius;
float border_width;
float scale;
} data;

View file

@ -0,0 +1,45 @@
#version 450
#include "rounded_fill.common.glsl"
layout(location = 0) in vec2 geo_pos;
layout(location = 0) out vec4 out_color;
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
if (coords.x < 0.0 || coords.y < 0.0 || coords.x > size.x || coords.y > size.y) {
return 0.0;
}
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (coords.x > size.x - corner_radius.y && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (coords.x > size.x - corner_radius.z && coords.y > size.y - corner_radius.z) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && coords.y > size.y - corner_radius.w) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float half_px = 0.5 / data.scale;
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
float outer_alpha = rounding_alpha(geo_pos, data.size, data.corner_radius);
float alpha = outer_alpha;
if (data.border_width > 0.0) {
vec2 inner_coords = geo_pos - vec2(data.border_width);
vec2 inner_size = data.size - vec2(2.0 * data.border_width);
vec4 inner_radius = max(data.corner_radius - vec4(data.border_width), vec4(0.0));
float inner_alpha = rounding_alpha(inner_coords, inner_size, inner_radius);
alpha = outer_alpha * (1.0 - inner_alpha);
}
out_color = data.color * alpha;
}

View file

@ -0,0 +1,16 @@
#version 450
#include "rounded_fill.common.glsl"
layout(location = 0) out vec2 geo_pos;
void main() {
vec2 pos = data.vertices.pos[gl_InstanceIndex][gl_VertexIndex];
gl_Position = vec4(pos, 0.0, 1.0);
switch (gl_VertexIndex) {
case 0: geo_pos = vec2(data.size.x, 0.0); break;
case 1: geo_pos = vec2(0.0, 0.0); break;
case 2: geo_pos = vec2(data.size.x, data.size.y); break;
case 3: geo_pos = vec2(0.0, data.size.y); break;
}
}

View file

@ -0,0 +1,22 @@
#extension GL_EXT_buffer_reference : require
struct Vertex {
vec2 pos[4];
vec2 tex_pos[4];
};
layout(buffer_reference, buffer_reference_align = 8, std430) readonly buffer Vertices {
Vertex vertices[];
};
layout(push_constant, std430) uniform Data {
layout(offset = 0) Vertices vertices;
layout(offset = 8) float mul;
layout(offset = 12) float size_x;
layout(offset = 16) float size_y;
layout(offset = 20) float corner_radius_tl;
layout(offset = 24) float corner_radius_tr;
layout(offset = 28) float corner_radius_br;
layout(offset = 32) float corner_radius_bl;
layout(offset = 36) float scale;
} data;

View file

@ -0,0 +1,76 @@
#version 450
#extension GL_EXT_scalar_block_layout : require
#define TEX_SET 1
#include "frag_spec_const.glsl"
#include "rounded_tex.common.glsl"
#include "tex_set.glsl"
#include "eotfs.glsl"
#include "alpha_modes.glsl"
layout(set = 0, binding = 0) uniform sampler sam;
layout(location = 0) in vec2 tex_pos;
layout(location = 1) in vec2 geo_pos;
layout(location = 0) out vec4 out_color;
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
if (coords.x < 0.0 || coords.y < 0.0 || coords.x > size.x || coords.y > size.y) {
return 0.0;
}
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (coords.x > size.x - corner_radius.y && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (coords.x > size.x - corner_radius.z && coords.y > size.y - corner_radius.z) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && coords.y > size.y - corner_radius.w) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float half_px = 0.5 / data.scale;
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
void main() {
vec2 size = vec2(data.size_x, data.size_y);
vec4 corner_radius = vec4(data.corner_radius_tl, data.corner_radius_tr, data.corner_radius_br, data.corner_radius_bl);
vec4 c = textureLod(sampler2D(tex, sam), tex_pos, 0);
if (eotf != inv_eotf || has_matrix || alpha_mode != AM_PREMULTIPLIED_ELECTRICAL) {
vec3 rgb = c.rgb;
if (src_has_alpha && alpha_mode == AM_PREMULTIPLIED_ELECTRICAL) {
rgb /= mix(c.a, 1.0, c.a == 0.0);
}
rgb = apply_eotf(rgb);
if (src_has_alpha && alpha_mode == AM_PREMULTIPLIED_OPTICAL) {
rgb /= mix(c.a, 1.0, c.a == 0.0);
}
if (has_matrix) {
rgb = (cm_data.matrix * vec4(rgb, 1.0)).rgb;
}
rgb = apply_inv_eotf(rgb);
if (src_has_alpha) {
rgb *= c.a;
}
c.rgb = rgb;
}
if (has_alpha_multiplier) {
if (src_has_alpha) {
c *= data.mul;
} else {
c = vec4(c.rgb * data.mul, data.mul);
}
}
float ra = rounding_alpha(geo_pos, size, corner_radius);
c *= ra;
out_color = c;
}

View file

@ -0,0 +1,19 @@
#version 450
#include "rounded_tex.common.glsl"
layout(location = 0) out vec2 tex_pos;
layout(location = 1) out vec2 geo_pos;
void main() {
vec2 size = vec2(data.size_x, data.size_y);
Vertex vertex = data.vertices.vertices[gl_InstanceIndex];
gl_Position = vec4(vertex.pos[gl_VertexIndex], 0.0, 1.0);
tex_pos = vertex.tex_pos[gl_VertexIndex];
switch (gl_VertexIndex) {
case 0: geo_pos = vec2(size.x, 0.0); break;
case 1: geo_pos = vec2(0.0, 0.0); break;
case 2: geo_pos = vec2(size.x, size.y); break;
case 3: geo_pos = vec2(0.0, size.y); break;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -7,12 +7,24 @@ f93524fd077bc9984702b1e0f92232f80bfe28a0a92439dc164c1ea41fd16d64 src/gfx_apis/vu
c315a064b48dd5bdb607a6b79c30d31b6e59ffec69e93d50ab875abf97c41bbf src/gfx_apis/vulkan/shaders/legacy/fill.common.glsl c315a064b48dd5bdb607a6b79c30d31b6e59ffec69e93d50ab875abf97c41bbf src/gfx_apis/vulkan/shaders/legacy/fill.common.glsl
590d061b97446fc501158609eaf098b71bc7b328c008b586ff36613ce690d618 src/gfx_apis/vulkan/shaders/legacy/fill.frag 590d061b97446fc501158609eaf098b71bc7b328c008b586ff36613ce690d618 src/gfx_apis/vulkan/shaders/legacy/fill.frag
ad22a79e1a88a12daa40c0a2b953084c129a408297c8ca544d60e0b6001470b9 src/gfx_apis/vulkan/shaders/legacy/fill.vert ad22a79e1a88a12daa40c0a2b953084c129a408297c8ca544d60e0b6001470b9 src/gfx_apis/vulkan/shaders/legacy/fill.vert
b77838c0aac9ec90ae76cd0d94d3891d72d9a30b09ce77009afd9f4e567dd042 src/gfx_apis/vulkan/shaders/legacy/rounded_fill.common.glsl
fa39734aea1c96960f5dc95b999ae2fa5576ecf4b527fd70ee0f643c8ddcc452 src/gfx_apis/vulkan/shaders/legacy/rounded_fill.frag
c1914cc00fb4827f65cd55bd0737d159fe44a098a3085a500822fc91cc2bfcad src/gfx_apis/vulkan/shaders/legacy/rounded_fill.vert
bd249cf170b72cd833e92a7719e88da0a91e563956579707e693679b443d73d5 src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl
28f3249e0d974a332b2926fb7565930627a093d6ac21ca17f2bf191740d299bd src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag
6ef0bde549dc163cd08f68d975071f5d74213c07ccc4a06b30c6f179b2f848ae src/gfx_apis/vulkan/shaders/legacy/rounded_tex.vert
e0a8769dd7938dd02e66db9e9048ed6bef8f8c42671f2e2c7a7976a6d498f685 src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl e0a8769dd7938dd02e66db9e9048ed6bef8f8c42671f2e2c7a7976a6d498f685 src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl
0e7c72ea11671065842c8b4ad4131a7df33b427dc0ea76bf5a896546f6636cb0 src/gfx_apis/vulkan/shaders/legacy/tex.frag 0e7c72ea11671065842c8b4ad4131a7df33b427dc0ea76bf5a896546f6636cb0 src/gfx_apis/vulkan/shaders/legacy/tex.frag
4402f7ccdbb9fb52fb6cda3aab13cf89e2980c79b541f8be0463efd64a5f98ed src/gfx_apis/vulkan/shaders/legacy/tex.vert 4402f7ccdbb9fb52fb6cda3aab13cf89e2980c79b541f8be0463efd64a5f98ed src/gfx_apis/vulkan/shaders/legacy/tex.vert
3ba5d05c2b95099e5424b3ade5d1c31d431f5730b1d0b51a9fb5f8afc4ea14b4 src/gfx_apis/vulkan/shaders/out.common.glsl 3ba5d05c2b95099e5424b3ade5d1c31d431f5730b1d0b51a9fb5f8afc4ea14b4 src/gfx_apis/vulkan/shaders/out.common.glsl
5069f619c7d722815a022e2d84720a2d8290af49a3ed49ea0cd26b52115cc39a src/gfx_apis/vulkan/shaders/out.frag 5069f619c7d722815a022e2d84720a2d8290af49a3ed49ea0cd26b52115cc39a src/gfx_apis/vulkan/shaders/out.frag
0adc7e12328c15fb3e7e6c8b8701a182223c2f15337e14131f41dd247e697809 src/gfx_apis/vulkan/shaders/out.vert 0adc7e12328c15fb3e7e6c8b8701a182223c2f15337e14131f41dd247e697809 src/gfx_apis/vulkan/shaders/out.vert
9202d5c9fc4ce0d5f40ed147f245bd037728c9e060ea46a0f0a1767ca55e6c48 src/gfx_apis/vulkan/shaders/rounded_fill.common.glsl
9085625d2afb1365685ae79a58108bf6566573ed94d9913397cf74dc6ef9b6e8 src/gfx_apis/vulkan/shaders/rounded_fill.frag
7665319a706e514f125d80f51f10b643f01cdae54d8a6ea56c218f78de7c0ecb src/gfx_apis/vulkan/shaders/rounded_fill.vert
dd100d048c0b380c913cffd7ac48fed3a341b3cb052302a11c369967f38aba9a src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl
454f34754ea4102190821c2d168dedd8c6bf624f1712b6136d902428f801a1e9 src/gfx_apis/vulkan/shaders/rounded_tex.frag
21b18ba369b505b9aedb8cf2e7e31bc417f6704fd2daac353b0db52f9ae44c70 src/gfx_apis/vulkan/shaders/rounded_tex.vert
e22d4d3318a350def8ef19c7b27dc6a308a84c2fe9d7c02b81107f72073cd481 src/gfx_apis/vulkan/shaders/tex.common.glsl e22d4d3318a350def8ef19c7b27dc6a308a84c2fe9d7c02b81107f72073cd481 src/gfx_apis/vulkan/shaders/tex.common.glsl
1f196cee646a934072beb3e5648a5042c035953d9a0c26b0a22e330c2f8bb994 src/gfx_apis/vulkan/shaders/tex.frag 1f196cee646a934072beb3e5648a5042c035953d9a0c26b0a22e330c2f8bb994 src/gfx_apis/vulkan/shaders/tex.frag
423cf327c9fcc4070dbf75321c1224a1589b6cf3d2f1ea5e8bd0362e1a9f3aa1 src/gfx_apis/vulkan/shaders/tex.vert 423cf327c9fcc4070dbf75321c1224a1589b6cf3d2f1ea5e8bd0362e1a9f3aa1 src/gfx_apis/vulkan/shaders/tex.vert

View file

@ -1,4 +1,5 @@
#![allow(clippy::excessive_precision)] #![allow(clippy::excessive_precision)]
#![allow(dead_code)]
use { use {
crate::{ crate::{
@ -22,12 +23,14 @@ pub struct Icons {
icons: CopyHashMap<i32, Option<Rc<SizedIcons>>>, icons: CopyHashMap<i32, Option<Rc<SizedIcons>>>,
} }
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, Linearize)] #[derive(Copy, Clone, Debug, Linearize)]
pub enum IconState { pub enum IconState {
Active, Active,
Passive, Passive,
} }
#[allow(dead_code)]
pub struct SizedIcons { pub struct SizedIcons {
pub pin_unfocused_title: StaticMap<IconState, Rc<dyn GfxTexture>>, pub pin_unfocused_title: StaticMap<IconState, Rc<dyn GfxTexture>>,
pub pin_focused_title: StaticMap<IconState, Rc<dyn GfxTexture>>, pub pin_focused_title: StaticMap<IconState, Rc<dyn GfxTexture>>,

View file

@ -83,7 +83,7 @@ use {
rect::Rect, rect::Rect,
state::{DeviceHandlerData, State}, state::{DeviceHandlerData, State},
tree::{ tree::{
ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, NodeLayer, ContainerNode, ContainerSplit, ChangeGroupAction, Direction, FoundNode, Node, NodeId, NodeLayer,
NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode, NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode,
generic_node_visitor, toplevel_create_split, toplevel_parent_container, generic_node_visitor, toplevel_create_split, toplevel_parent_container,
toplevel_set_floating, toplevel_set_workspace, toplevel_set_floating, toplevel_set_workspace,
@ -745,6 +745,40 @@ impl WlSeatGlobal {
toplevel_create_split(&self.state, tl, axis); toplevel_create_split(&self.state, tl, axis);
} }
pub fn toggle_tab(&self) {
if let Some(c) = self.kb_parent_container() {
c.change_group(ChangeGroupAction::ToggleTab);
}
}
pub fn make_group(&self, axis: ContainerSplit, ephemeral: bool) {
if let Some(c) = self.kb_parent_container() {
c.make_group(axis, ephemeral);
}
}
pub fn change_group_opposite(&self) {
if let Some(c) = self.kb_parent_container() {
c.change_group(ChangeGroupAction::Opposite);
}
}
pub fn equalize(&self, recursive: bool) {
if let Some(c) = self.kb_parent_container() {
if recursive {
c.equalize_recursive();
} else {
c.equalize();
}
}
}
pub fn move_tab(&self, right: bool) {
if let Some(c) = self.kb_parent_container() {
c.move_tab(right);
}
}
pub fn focus_parent(self: &Rc<Self>) { pub fn focus_parent(self: &Rc<Self>) {
if let Some(tl) = self.keyboard_node.get().node_toplevel() if let Some(tl) = self.keyboard_node.get().node_toplevel()
&& let Some(parent) = tl.tl_data().parent.get() && let Some(parent) = tl.tl_data().parent.get()
@ -1320,6 +1354,7 @@ impl WlSeatGlobal {
.start_drag(self, origin, source, icon, serial) .start_drag(self, origin, source, icon, serial)
} }
#[allow(dead_code)]
pub fn start_tile_drag(self: &Rc<Self>, tl: &Rc<dyn ToplevelNode>) { pub fn start_tile_drag(self: &Rc<Self>, tl: &Rc<dyn ToplevelNode>) {
if self.state.ui_drag_enabled.get() { if self.state.ui_drag_enabled.get() {
self.pointer_owner.start_tile_drag(self, tl); self.pointer_owner.start_tile_drag(self, tl);

View file

@ -215,6 +215,7 @@ impl PointerOwnerHolder {
} }
} }
#[allow(dead_code)]
pub fn start_tile_drag(&self, seat: &Rc<WlSeatGlobal>, tl: &Rc<dyn ToplevelNode>) { pub fn start_tile_drag(&self, seat: &Rc<WlSeatGlobal>, tl: &Rc<dyn ToplevelNode>) {
self.owner.get().start_tile_drag(seat, tl); self.owner.get().start_tile_drag(seat, tl);
} }
@ -288,6 +289,7 @@ trait PointerOwner {
fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) { fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
let _ = seat; let _ = seat;
} }
#[allow(dead_code)]
fn start_tile_drag(&self, seat: &Rc<WlSeatGlobal>, tl: &Rc<dyn ToplevelNode>) { fn start_tile_drag(&self, seat: &Rc<WlSeatGlobal>, tl: &Rc<dyn ToplevelNode>) {
let _ = seat; let _ = seat;
let _ = tl; let _ = tl;
@ -732,6 +734,7 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static {
let _ = seat; let _ = seat;
} }
#[allow(dead_code)]
fn start_tile_drag( fn start_tile_drag(
&self, &self,
grab: &SimpleGrabPointerOwner<Self>, grab: &SimpleGrabPointerOwner<Self>,

View file

@ -528,6 +528,8 @@ impl GfxFramebuffer for TestGfxFb {
GfxApiOpt::Sync => {} GfxApiOpt::Sync => {}
GfxApiOpt::FillRect(f) => fill_rect(&f, staging), GfxApiOpt::FillRect(f) => fill_rect(&f, staging),
GfxApiOpt::CopyTexture(c) => copy_texture(&c, staging)?, GfxApiOpt::CopyTexture(c) => copy_texture(&c, staging)?,
GfxApiOpt::RoundedFillRect(_) => {}
GfxApiOpt::RoundedCopyTexture(_) => {}
} }
} }
copy_from_staging(staging); copy_from_staging(staging);

View file

@ -2,7 +2,6 @@ use {
crate::{ crate::{
cmm::cmm_render_intent::RenderIntent, cmm::cmm_render_intent::RenderIntent,
gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect}, gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect},
icons::{IconState, SizedIcons},
ifs::wl_surface::{ ifs::wl_surface::{
SurfaceBuffer, WlSurface, SurfaceBuffer, WlSurface,
x_surface::xwindow::Xwindow, x_surface::xwindow::Xwindow,
@ -13,10 +12,10 @@ use {
renderer::renderer_base::RendererBase, renderer::renderer_base::RendererBase,
scale::Scale, scale::Scale,
state::State, state::State,
theme::Color, theme::{Color, CornerRadius},
tree::{ tree::{
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
ToplevelNodeBase, WorkspaceNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
}, },
}, },
std::{ops::Deref, rc::Rc, slice}, std::{ops::Deref, rc::Rc, slice},
@ -29,8 +28,8 @@ pub struct Renderer<'a> {
pub state: &'a State, pub state: &'a State,
pub logical_extents: Rect, pub logical_extents: Rect,
pub pixel_extents: Rect, pub pixel_extents: Rect,
pub icons: Option<Rc<SizedIcons>>,
pub stretch: Option<(i32, i32)>, pub stretch: Option<(i32, i32)>,
pub corner_radius: Option<CornerRadius>,
} }
impl Renderer<'_> { impl Renderer<'_> {
@ -278,68 +277,126 @@ impl Renderer<'_> {
self.render_tl_aux(placeholder.tl_data(), bounds, true); self.render_tl_aux(placeholder.tl_data(), bounds, true);
} }
fn render_container_decorations(&mut self, container: &ContainerNode, x: i32, y: i32) { fn render_tab_bar(&mut self, tab_bar: &TabBar, x: i32, y: i32, _container_width: i32) {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
let rd = container.render_data.borrow_mut(); let scalef = self.base.scalef as f32;
let c = self.state.theme.colors.unfocused_title_background.get();
self.base let radius = self.state.theme.sizes.tab_bar_radius.get();
.fill_boxes2(&rd.title_rects, &c, srgb, perceptual, x, y); let border_width = self.state.theme.sizes.tab_bar_border_width.get();
let c = self.state.theme.colors.focused_title_background.get(); let text_padding = self.state.theme.sizes.tab_bar_text_padding.get();
self.base let bar_height = tab_bar.height;
.fill_boxes2(&rd.active_title_rects, &c, srgb, perceptual, x, y); let render_scale = tab_bar.render_scale;
let c = self.state.theme.colors.attention_requested_background.get();
self.base // Vulkan sorts ops: Fill < Tex < RoundedFill (by z_order, color) < RoundedTex.
.fill_boxes2(&rd.attention_title_rects, &c, srgb, perceptual, x, y); // We use:
let c = self.state.theme.colors.separator.get(); // FillRect tiny strip for Vulkan paint regions (hidden)
self.base // RoundedFillRect z0 solid rounded bg
.fill_boxes2(&rd.underline_rects, &c, srgb, perceptual, x, y); // RoundedFillRect z1 rounded border ring (on top of bg)
let c = self.state.theme.colors.border.get(); // RoundedCopyTexture title text (on top of everything)
self.base for entry in &tab_bar.entries {
.fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y); let (bg_color, border_color, _text_color) =
if let Some(lar) = &rd.last_active_rect { TabBar::entry_colors(self.state, entry);
let c = self let ex = entry.x.get();
.state let ew = entry.width.get();
.theme let tab_rect = Rect::new_sized_saturating(ex, 0, ew, bar_height);
.colors let tab_cr = CornerRadius::from(radius as f32);
.focused_inactive_title_background
.get(); // Tiny FillRect strip to establish Vulkan paint regions (visually hidden
// behind the RoundedFillRect bg that renders later).
let strip = Rect::new_sized_saturating(ex + radius, bar_height / 2, (ew - 2 * radius).max(1), 1);
self.base self.base
.fill_boxes2(std::slice::from_ref(lar), &c, srgb, perceptual, x, y); .fill_boxes2(slice::from_ref(&strip), &bg_color, srgb, perceptual, x, y);
}
if let Some(titles) = rd.titles.get(&self.base.scale) { // Rounded solid bg fill (z_order=0, renders first among RoundedFill).
for title in titles { self.base.fill_rounded_rect_z(
let rect = title.rect.move_(x, y); tab_rect.move_(x, y),
let bounds = self.base.scale_rect(rect); &bg_color,
let (x, y) = self.base.scale_point(rect.x1(), rect.y1()); None,
self.base.render_texture( srgb,
&title.tex, perceptual,
tab_cr.scaled_by(scalef),
0.0,
0,
);
// Rounded border ring on top (z_order=1, renders after bg).
if border_width > 0 {
self.base.fill_rounded_rect_z(
tab_rect.move_(x, y),
&border_color,
None, None,
x, srgb,
y, perceptual,
tab_cr.scaled_by(scalef),
border_width as f32 * scalef,
1,
);
}
// Title text as RoundedCopyTexture (sorts after all RoundedFill).
let tex_ref = entry.title_texture.borrow();
if let Some(tex) = tex_ref.as_ref()
&& let Some(texture) = tex.texture()
{
use crate::theme::TabTitleAlign;
let (tw, _th) = texture.size();
let tex_width = (tw as f64 / render_scale.to_f64()).round() as i32;
let tab_inner = ew - 2 * (text_padding + border_width);
let text_x = match self.state.theme.tab_title_align.get() {
TabTitleAlign::Start => x + ex + text_padding + border_width,
TabTitleAlign::Center => {
x + ex + border_width + (tab_inner.max(0) - tex_width).max(0) / 2 + text_padding.min(tab_inner.max(0) / 2)
}
TabTitleAlign::End => {
let end_x = x + ex + ew - tex_width - text_padding - border_width;
end_x.max(x + ex + border_width)
}
};
let (tx, ty) = self.base.scale_point(text_x, y);
self.base.render_rounded_texture(
&texture,
None,
tx,
ty,
None, None,
None, None,
self.base.scale, render_scale,
Some(&bounds), None,
None, None,
AcquireSync::None, AcquireSync::None,
ReleaseSync::None, ReleaseSync::None,
false, self.state.color_manager.srgb_gamma22(),
srgb_srgb,
perceptual, perceptual,
AlphaMode::PremultipliedElectrical, AlphaMode::PremultipliedElectrical,
CornerRadius::from(0.0_f32),
); );
} }
} }
} }
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) { fn render_container_decorations(&mut self, container: &ContainerNode, x: i32, y: i32) {
let floating = self.state.theme.floating_titles.get(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual;
let rd = container.render_data.borrow_mut();
let c = self.state.theme.colors.border.get();
self.base
.fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y);
}
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
self.render_container_decorations(container, x, y); self.render_container_decorations(container, x, y);
if let Some(child) = container.mono_child.get() { if let Some(child) = container.mono_child.get() {
// Render tab bar if present.
{
let tab_bar = container.tab_bar.borrow();
if let Some(tb) = tab_bar.as_ref() {
self.render_tab_bar(tb, x, y, container.width.get());
}
}
let mb = container.mono_body.get(); let mb = container.mono_body.get();
if self.state.theme.sizes.gap.get() != 0 { if self.state.theme.sizes.gap.get() != 0 {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
@ -355,44 +412,37 @@ impl Renderer<'_> {
let full_w = mb.width(); let full_w = mb.width();
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
if floating { if !child.node.node_is_container() {
if !child.node.node_is_container() { let cr = self.state.theme.corner_radius.get();
let body_frame = [ let frame_y = mb.y1();
Rect::new_sized_saturating(mb.x1() - bw, mb.y1(), bw, mb.y2() - mb.y1()), let frame_h = mb.height();
Rect::new_sized_saturating(mb.x2(), mb.y1(), bw, mb.y2() - mb.y1()), if cr.is_zero() {
Rect::new_sized_saturating(mb.x1() - bw, mb.y1() - bw, full_w + 2 * bw, bw), let frame_rects = [
Rect::new_sized_saturating(mb.x1() - bw, mb.y2(), full_w + 2 * bw, bw), Rect::new_sized_saturating(mb.x1() - bw, frame_y, bw, frame_h),
Rect::new_sized_saturating(mb.x2(), frame_y, bw, frame_h),
Rect::new_sized_saturating(mb.x1() - bw, frame_y - bw, full_w + 2 * bw, bw),
Rect::new_sized_saturating(mb.x1() - bw, frame_y + frame_h, full_w + 2 * bw, bw),
]; ];
self.base.fill_boxes2(&body_frame, c, srgb, perceptual, x, y); self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y);
} else {
let outer = Rect::new_sized_saturating(
mb.x1() - bw,
frame_y - bw,
full_w + 2 * bw,
frame_h + 2 * bw,
);
let scalef = self.base.scalef as f32;
let scaled_cr = cr.scaled_by(scalef);
self.base.fill_rounded_rect(
outer.move_(x, y),
c,
None,
srgb,
perceptual,
scaled_cr,
bw as f32 * scalef,
);
} }
let th = self.state.theme.title_height();
if th > 0 {
for tab in container.children.iter() {
let tr = tab.title_rect.get();
let tc = if tab.active.get() {
&focused_border_color
} else {
&border_color
};
let tw = tr.width();
let tab_frame = [
Rect::new_sized_saturating(tr.x1() - bw, tr.y1() - bw, tw + 2 * bw, bw),
Rect::new_sized_saturating(tr.x1() - bw, tr.y1(), bw, th),
Rect::new_sized_saturating(tr.x2(), tr.y1(), bw, th),
Rect::new_sized_saturating(tr.x1() - bw, tr.y1() + th, tw + 2 * bw, bw),
];
self.base.fill_boxes2(&tab_frame, tc, srgb, perceptual, x, y);
}
}
} else if !child.node.node_is_container() {
let full_h = mb.y2();
let frame_rects = [
Rect::new_sized_saturating(mb.x1() - bw, 0, bw, full_h),
Rect::new_sized_saturating(mb.x2(), 0, bw, full_h),
Rect::new_sized_saturating(mb.x1() - bw, -bw, full_w + 2 * bw, bw),
Rect::new_sized_saturating(mb.x1() - bw, full_h, full_w + 2 * bw, bw),
];
self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y);
} }
} }
let body = mb.move_(x, y); let body = mb.move_(x, y);
@ -403,28 +453,36 @@ impl Renderer<'_> {
} else { } else {
None None
}; };
if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() {
let cr = self.state.theme.corner_radius.get();
if !cr.is_zero() {
let scalef = self.base.scalef as f32;
let bw = self.state.theme.sizes.border_width.get();
let inner_cr = cr.expanded_by(-(bw as f32)).scaled_by(scalef);
self.corner_radius = Some(inner_cr);
}
}
child child
.node .node
.node_render(self, x + content.x1(), y + content.y1(), Some(&body)); .node_render(self, x + content.x1(), y + content.y1(), Some(&body));
self.stretch = None; self.stretch = None;
} else { } else {
let gap = self.state.theme.sizes.gap.get(); let gap = self.state.theme.sizes.gap.get();
let (srgb_srgb, bw, border_color, focused_border_color, tpuh) = if gap != 0 { let (srgb_srgb, bw, border_color, focused_border_color) = if gap != 0 {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let bw = self.state.theme.sizes.border_width.get(); let bw = self.state.theme.sizes.border_width.get();
let border_color = self.state.theme.colors.border.get(); let border_color = self.state.theme.colors.border.get();
let focused_border_color = self.state.theme.colors.focused_title_background.get(); let focused_border_color = self.state.theme.colors.focused_title_background.get();
let tpuh = self.state.theme.title_plus_underline_height();
( (
Some(srgb_srgb), Some(srgb_srgb),
bw, bw,
border_color, border_color,
focused_border_color, focused_border_color,
tpuh,
) )
} else { } else {
(None, 0, Color::SOLID_BLACK, Color::SOLID_BLACK, 0) (None, 0, Color::SOLID_BLACK, Color::SOLID_BLACK)
}; };
let cr = self.state.theme.corner_radius.get();
for child in container.children.iter() { for child in container.children.iter() {
let body = child.body.get(); let body = child.body.get();
if body.x1() >= container.width.get() || body.y1() >= container.height.get() { if body.x1() >= container.width.get() || body.y1() >= container.height.get() {
@ -432,45 +490,42 @@ impl Renderer<'_> {
} }
if let Some(srgb_srgb) = srgb_srgb { if let Some(srgb_srgb) = srgb_srgb {
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
let c = if floating { let c = if child.border_color_is_focused.get() {
if child.active.get() { &focused_border_color } else { &border_color }
} else if child.border_color_is_focused.get() {
&focused_border_color &focused_border_color
} else { } else {
&border_color &border_color
}; };
let title_rect = child.title_rect.get(); if !child.node.node_is_container() && gap != 0 {
let full_w = body.width(); let full_w = body.width();
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
if floating && tpuh > 0 { let full_h = body.height();
let tw = title_rect.width(); if cr.is_zero() {
let title_h = title_rect.height(); let frame_rects = [
let title_frame = [ Rect::new_sized_saturating(body.x1() - bw, body.y1(), bw, full_h),
Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1() - bw, tw + 2 * bw, bw), Rect::new_sized_saturating(body.x2(), body.y1(), bw, full_h),
Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1(), bw, title_h),
Rect::new_sized_saturating(title_rect.x2(), title_rect.y1(), bw, title_h),
Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1() + title_h, tw + 2 * bw, bw),
];
self.base.fill_boxes2(&title_frame, c, srgb, perceptual, x, y);
if !child.node.node_is_container() && gap != 0 {
let body_frame = [
Rect::new_sized_saturating(body.x1() - bw, body.y1(), bw, body.y2() - body.y1()),
Rect::new_sized_saturating(body.x2(), body.y1(), bw, body.y2() - body.y1()),
Rect::new_sized_saturating(body.x1() - bw, body.y1() - bw, full_w + 2 * bw, bw), Rect::new_sized_saturating(body.x1() - bw, body.y1() - bw, full_w + 2 * bw, bw),
Rect::new_sized_saturating(body.x1() - bw, body.y2(), full_w + 2 * bw, bw), Rect::new_sized_saturating(body.x1() - bw, body.y2(), full_w + 2 * bw, bw),
]; ];
self.base.fill_boxes2(&body_frame, c, srgb, perceptual, x, y); self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y);
} else {
let outer = Rect::new_sized_saturating(
body.x1() - bw,
body.y1() - bw,
full_w + 2 * bw,
full_h + 2 * bw,
);
let scalef = self.base.scalef as f32;
let scaled_cr = cr.scaled_by(scalef);
self.base.fill_rounded_rect(
outer.move_(x, y),
c,
None,
srgb,
perceptual,
scaled_cr,
bw as f32 * scalef,
);
} }
} else if !child.node.node_is_container() && gap != 0 {
let top_y = if tpuh > 0 { title_rect.y1() } else { body.y1() };
let full_h = body.y2() - top_y;
let frame_rects = [
Rect::new_sized_saturating(body.x1() - bw, top_y, bw, full_h),
Rect::new_sized_saturating(body.x2(), top_y, bw, full_h),
Rect::new_sized_saturating(body.x1() - bw, top_y - bw, full_w + 2 * bw, bw),
Rect::new_sized_saturating(body.x1() - bw, body.y2(), full_w + 2 * bw, bw),
];
self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y);
} }
} }
let content = child.content.get(); let content = child.content.get();
@ -479,6 +534,11 @@ impl Renderer<'_> {
} else { } else {
None None
}; };
if !cr.is_zero() && !child.node.node_is_container() && gap != 0 {
let scalef = self.base.scalef as f32;
let inner_cr = cr.expanded_by(-(bw as f32)).scaled_by(scalef);
self.corner_radius = Some(inner_cr);
}
let body = body.move_(x, y); let body = body.move_(x, y);
let body = self.base.scale_rect(body); let body = self.base.scale_rect(body);
child child
@ -642,28 +702,49 @@ impl Renderer<'_> {
let cd = surface.color_description(); let cd = surface.color_description();
let intent = surface.render_intent(); let intent = surface.render_intent();
let alpha_mode = surface.alpha_mode(); let alpha_mode = surface.alpha_mode();
let corner_radius = self.corner_radius.take();
if let Some(tex) = buf.get_texture(surface) { if let Some(tex) = buf.get_texture(surface) {
let mut opaque = surface.opaque(); if let Some(cr) = corner_radius {
if !opaque && tex.format().has_alpha { self.base.render_rounded_texture(
opaque = self.bounds_are_opaque(x, y, bounds, surface); &tex,
alpha,
x,
y,
Some(tpoints),
Some(tsize),
self.base.scale,
bounds,
Some(buffer.clone()),
AcquireSync::Unnecessary,
buffer.release_sync,
&cd,
intent,
alpha_mode,
cr,
);
} else {
let mut opaque = surface.opaque();
if !opaque && tex.format().has_alpha {
opaque = self.bounds_are_opaque(x, y, bounds, surface);
}
self.base.render_texture(
&tex,
alpha,
x,
y,
Some(tpoints),
Some(tsize),
self.base.scale,
bounds,
Some(buffer.clone()),
AcquireSync::Unnecessary,
buffer.release_sync,
opaque,
&cd,
intent,
alpha_mode,
);
} }
self.base.render_texture(
&tex,
alpha,
x,
y,
Some(tpoints),
Some(tsize),
self.base.scale,
bounds,
Some(buffer.clone()),
AcquireSync::Unnecessary,
buffer.release_sync,
opaque,
&cd,
intent,
alpha_mode,
);
} else if let Some(color) = &buf.color { } else if let Some(color) = &buf.color {
if let Some(rect) = Rect::new_sized(x, y, tsize.0, tsize.1) { if let Some(rect) = Rect::new_sized(x, y, tsize.0, tsize.1) {
let rect = match bounds { let rect = match bounds {
@ -691,128 +772,60 @@ impl Renderer<'_> {
}; };
let pos = floating.position.get(); let pos = floating.position.get();
let theme = &self.state.theme; let theme = &self.state.theme;
let th = theme.title_height();
let tpuh = theme.title_plus_underline_height();
let tuh = theme.title_underline_height();
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let bc = if floating.active.get() { let bc = if floating.active.get() {
theme.colors.focused_title_background.get() theme.colors.focused_title_background.get()
} else { } else {
theme.colors.border.get() theme.colors.border.get()
}; };
let tc = if floating.active.get() {
theme.colors.focused_title_background.get()
} else if floating.attention_requested.get() {
theme.colors.attention_requested_background.get()
} else {
theme.colors.unfocused_title_background.get()
};
let uc = theme.colors.separator.get();
let gap = theme.sizes.gap.get();
let floating_title = theme.floating_titles.get() && gap != 0;
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
if floating_title { let cr = theme.corner_radius.get();
let title_frame = [ if cr.is_zero() {
Rect::new_sized_saturating(x, y, pos.width(), bw),
Rect::new_sized_saturating(x, y + bw, bw, th),
Rect::new_sized_saturating(x + pos.width() - bw, y + bw, bw, th),
Rect::new_sized_saturating(x, y + bw + th, pos.width(), bw),
];
self.base.fill_boxes(&title_frame, &bc, srgb, perceptual);
let title_bg = [Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, th)];
self.base.fill_boxes(&title_bg, &tc, srgb, perceptual);
let body_top = y + tpuh;
let body_inner_top = y + bw + tpuh;
let body_inner_height = pos.height() - 2 * bw - tpuh;
let body_frame = [
Rect::new_sized_saturating(x, body_top, pos.width(), bw),
Rect::new_sized_saturating(x, body_inner_top, bw, body_inner_height),
Rect::new_sized_saturating(x + pos.width() - bw, body_inner_top, bw, body_inner_height),
Rect::new_sized_saturating(x, y + pos.height() - bw, pos.width(), bw),
];
self.base.fill_boxes(&body_frame, &bc, srgb, perceptual);
} else {
let borders = [ let borders = [
Rect::new_sized_saturating(x, y, pos.width(), bw), Rect::new_sized_saturating(x, y, pos.width(), bw),
Rect::new_sized_saturating(x, y + bw, bw, pos.height() - bw), Rect::new_sized_saturating(x, y + bw, bw, pos.height() - bw),
Rect::new_sized_saturating(x + pos.width() - bw, y + bw, bw, pos.height() - bw), Rect::new_sized_saturating(
Rect::new_sized_saturating(x + bw, y + pos.height() - bw, pos.width() - 2 * bw, bw), x + pos.width() - bw,
y + bw,
bw,
pos.height() - bw,
),
Rect::new_sized_saturating(
x + bw,
y + pos.height() - bw,
pos.width() - 2 * bw,
bw,
),
]; ];
self.base.fill_boxes(&borders, &bc, srgb, perceptual); self.base.fill_boxes(&borders, &bc, srgb, perceptual);
let title = [Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, th)]; } else {
self.base.fill_boxes(&title, &tc, srgb, perceptual); let outer = Rect::new_sized_saturating(x, y, pos.width(), pos.height());
let title_underline = [Rect::new_sized_saturating(x + bw, y + bw + th, pos.width() - 2 * bw, tuh)]; let scalef = self.base.scalef as f32;
self.base.fill_boxes(&title_underline, &uc, srgb, perceptual); let scaled_cr = cr.scaled_by(scalef);
} self.base.fill_rounded_rect(
let rect = floating.title_rect.get().move_(x, y); outer,
let bounds = self.base.scale_rect(rect); &bc,
let (mut x1, y1) = rect.position();
let is_pinned = floating.pinned_link.borrow().is_some();
if is_pinned || self.state.show_pin_icon.get() {
let (x, y) = self.base.scale_point(x1, y1);
if let Some(icons) = &self.icons {
let icon = if floating.active.get() {
&icons.pin_focused_title
} else if floating.attention_requested.get() {
&icons.pin_attention_requested
} else {
&icons.pin_unfocused_title
};
let state = match is_pinned {
true => IconState::Active,
false => IconState::Passive,
};
self.base.render_texture(
&icon[state],
None,
x,
y,
None,
None,
self.base.scale,
Some(&bounds),
None,
AcquireSync::None,
ReleaseSync::None,
false,
srgb_srgb,
perceptual,
AlphaMode::PremultipliedElectrical,
);
}
x1 += th;
}
if let Some(title) = floating.title_textures.borrow().get(&self.base.scale)
&& let Some(texture) = title.texture()
{
let (x, y) = self.base.scale_point(x1, y1);
self.base.render_texture(
&texture,
None, None,
x, srgb,
y,
None,
None,
self.base.scale,
Some(&bounds),
None,
AcquireSync::None,
ReleaseSync::None,
false,
srgb_srgb,
perceptual, perceptual,
AlphaMode::PremultipliedElectrical, scaled_cr,
bw as f32 * scalef,
); );
} }
let body = Rect::new_sized_saturating( let body = Rect::new_sized_saturating(
x + bw, x + bw,
y + bw + tpuh, y + bw,
pos.width() - 2 * bw, pos.width() - 2 * bw,
pos.height() - 2 * bw - tpuh, pos.height() - 2 * bw,
); );
let scissor_body = self.base.scale_rect(body); let scissor_body = self.base.scale_rect(body);
if !cr.is_zero() {
let scalef = self.base.scalef as f32;
let inner_cr = cr.expanded_by(-(bw as f32)).scaled_by(scalef);
self.corner_radius = Some(inner_cr);
}
child.node_render(self, body.x1(), body.y1(), Some(&scissor_body)); child.node_render(self, body.x1(), body.y1(), Some(&scissor_body));
} }

View file

@ -6,11 +6,11 @@ use {
}, },
gfx_api::{ gfx_api::{
AcquireSync, AlphaMode, BufferResv, CopyTexture, FillRect, FramebufferRect, GfxApiOpt, AcquireSync, AlphaMode, BufferResv, CopyTexture, FillRect, FramebufferRect, GfxApiOpt,
GfxTexture, ReleaseSync, SampleRect, GfxTexture, ReleaseSync, RoundedCopyTexture, RoundedFillRect, SampleRect,
}, },
rect::Rect, rect::Rect,
scale::Scale, scale::Scale,
theme::Color, theme::{Color, CornerRadius},
tree::Transform, tree::Transform,
}, },
std::rc::Rc, std::rc::Rc,
@ -250,6 +250,138 @@ impl RendererBase<'_> {
})); }));
} }
pub fn fill_rounded_rect(
&mut self,
rect: Rect,
color: &Color,
alpha: Option<f32>,
cd: &Rc<LinearColorDescription>,
render_intent: RenderIntent,
corner_radius: CornerRadius,
border_width: f32,
) {
self.fill_rounded_rect_z(rect, color, alpha, cd, render_intent, corner_radius, border_width, 0)
}
pub fn fill_rounded_rect_z(
&mut self,
rect: Rect,
color: &Color,
alpha: Option<f32>,
cd: &Rc<LinearColorDescription>,
render_intent: RenderIntent,
corner_radius: CornerRadius,
border_width: f32,
z_order: u32,
) {
if *color == Color::TRANSPARENT {
return;
}
let rect = self.scale_rect(rect);
let width = (rect.x2() - rect.x1()) as f32;
let height = (rect.y2() - rect.y1()) as f32;
let scale = self.scalef as f32;
let fitted = corner_radius.fit_to(width, height);
let cr: [f32; 4] = fitted.into();
self.ops
.push(GfxApiOpt::RoundedFillRect(RoundedFillRect {
rect: FramebufferRect::new(
rect.x1() as f32,
rect.y1() as f32,
rect.x2() as f32,
rect.y2() as f32,
self.transform,
self.fb_width,
self.fb_height,
),
color: *color,
alpha,
render_intent,
cd: cd.clone(),
size: [width, height],
corner_radius: cr,
border_width,
scale,
z_order,
}));
}
pub fn render_rounded_texture(
&mut self,
texture: &Rc<dyn GfxTexture>,
alpha: Option<f32>,
x: i32,
y: i32,
tpoints: Option<SampleRect>,
tsize: Option<(i32, i32)>,
tscale: Scale,
bounds: Option<&Rect>,
buffer_resv: Option<Rc<dyn BufferResv>>,
acquire_sync: AcquireSync,
release_sync: ReleaseSync,
cd: &Rc<ColorDescription>,
render_intent: RenderIntent,
alpha_mode: AlphaMode,
corner_radius: CornerRadius,
) {
let mut texcoord = tpoints.unwrap_or_else(SampleRect::identity);
let (twidth, theight) = if let Some(size) = tsize {
size
} else {
let (mut w, mut h) = texcoord.buffer_transform.maybe_swap(texture.size());
if tscale != self.scale {
let tscale = tscale.to_f64();
w = (w as f64 * self.scalef / tscale).round() as _;
h = (h as f64 * self.scalef / tscale).round() as _;
}
(w, h)
};
let mut target_x = [x, x + twidth];
let mut target_y = [y, y + theight];
if let Some(bounds) = bounds
&& bound_target(&mut target_x, &mut target_y, &mut texcoord, bounds)
{
return;
}
let target = FramebufferRect::new(
target_x[0] as f32,
target_y[0] as f32,
target_x[1] as f32,
target_y[1] as f32,
self.transform,
self.fb_width,
self.fb_height,
);
let width = (target_x[1] - target_x[0]) as f32;
let height = (target_y[1] - target_y[0]) as f32;
let scale = self.scalef as f32;
let fitted = corner_radius.fit_to(width, height);
let cr: [f32; 4] = fitted.into();
self.ops
.push(GfxApiOpt::RoundedCopyTexture(RoundedCopyTexture {
tex: texture.clone(),
source: texcoord,
target,
alpha,
buffer_resv,
acquire_sync,
release_sync,
opaque: false,
render_intent,
cd: cd.clone(),
alpha_mode,
size: [width, height],
corner_radius: cr,
scale,
}));
}
pub fn sync(&mut self) { pub fn sync(&mut self) {
self.ops.push(GfxApiOpt::Sync); self.ops.push(GfxApiOpt::Sync);
} }

View file

@ -189,14 +189,13 @@ pub struct State {
pub theme: Theme, pub theme: Theme,
pub pending_container_layout: AsyncQueue<Rc<ContainerNode>>, pub pending_container_layout: AsyncQueue<Rc<ContainerNode>>,
pub pending_container_render_positions: AsyncQueue<Rc<ContainerNode>>, pub pending_container_render_positions: AsyncQueue<Rc<ContainerNode>>,
pub pending_container_render_title: AsyncQueue<Rc<ContainerNode>>,
pub pending_output_render_data: AsyncQueue<Rc<OutputNode>>, pub pending_output_render_data: AsyncQueue<Rc<OutputNode>>,
pub pending_float_layout: AsyncQueue<Rc<FloatNode>>, pub pending_float_layout: AsyncQueue<Rc<FloatNode>>,
pub pending_float_titles: AsyncQueue<Rc<FloatNode>>,
pub pending_input_popup_positioning: AsyncQueue<Rc<ZwpInputPopupSurfaceV2>>, pub pending_input_popup_positioning: AsyncQueue<Rc<ZwpInputPopupSurfaceV2>>,
pub pending_toplevel_screencasts: AsyncQueue<Rc<JayScreencast>>, pub pending_toplevel_screencasts: AsyncQueue<Rc<JayScreencast>>,
pub pending_screencast_reallocs_or_reconfigures: AsyncQueue<Rc<JayScreencast>>, pub pending_screencast_reallocs_or_reconfigures: AsyncQueue<Rc<JayScreencast>>,
pub pending_placeholder_render_textures: AsyncQueue<Rc<PlaceholderNode>>, pub pending_placeholder_render_textures: AsyncQueue<Rc<PlaceholderNode>>,
pub pending_container_tab_render_textures: AsyncQueue<Rc<ContainerNode>>,
pub dbus: Dbus, pub dbus: Dbus,
pub fdcloser: Arc<FdCloser>, pub fdcloser: Arc<FdCloser>,
pub logger: Option<Arc<Logger>>, pub logger: Option<Arc<Logger>>,
@ -275,6 +274,7 @@ pub struct State {
pub color_manager: Rc<ColorManager>, pub color_manager: Rc<ColorManager>,
pub float_above_fullscreen: Cell<bool>, pub float_above_fullscreen: Cell<bool>,
pub icons: Icons, pub icons: Icons,
#[allow(dead_code)]
pub show_pin_icon: Cell<bool>, pub show_pin_icon: Cell<bool>,
pub cl_matcher_manager: ClMatcherManager, pub cl_matcher_manager: ClMatcherManager,
pub tl_matcher_manager: TlMatcherManager, pub tl_matcher_manager: TlMatcherManager,
@ -550,10 +550,6 @@ impl DrmDevData {
struct UpdateTextTexturesVisitor; struct UpdateTextTexturesVisitor;
impl NodeVisitorBase for UpdateTextTexturesVisitor { impl NodeVisitorBase for UpdateTextTexturesVisitor {
fn visit_container(&mut self, node: &Rc<ContainerNode>) { fn visit_container(&mut self, node: &Rc<ContainerNode>) {
node.children
.iter()
.for_each(|c| c.title_tex.borrow_mut().clear());
node.schedule_render_titles();
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_output(&mut self, node: &Rc<OutputNode>) { fn visit_output(&mut self, node: &Rc<OutputNode>) {
@ -561,8 +557,6 @@ impl NodeVisitorBase for UpdateTextTexturesVisitor {
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_float(&mut self, node: &Rc<FloatNode>) { fn visit_float(&mut self, node: &Rc<FloatNode>) {
node.title_textures.borrow_mut().clear();
node.schedule_render_titles();
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_workspace(&mut self, node: &Rc<WorkspaceNode>) { fn visit_workspace(&mut self, node: &Rc<WorkspaceNode>) {
@ -685,10 +679,6 @@ impl State {
struct Walker; struct Walker;
impl NodeVisitorBase for Walker { impl NodeVisitorBase for Walker {
fn visit_container(&mut self, node: &Rc<ContainerNode>) { fn visit_container(&mut self, node: &Rc<ContainerNode>) {
node.render_data.borrow_mut().titles.clear();
node.children
.iter()
.for_each(|c| c.title_tex.borrow_mut().clear());
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_workspace(&mut self, node: &Rc<WorkspaceNode>) { fn visit_workspace(&mut self, node: &Rc<WorkspaceNode>) {
@ -702,7 +692,6 @@ impl State {
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_float(&mut self, node: &Rc<FloatNode>) { fn visit_float(&mut self, node: &Rc<FloatNode>) {
node.title_textures.borrow_mut().clear();
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_placeholder(&mut self, node: &Rc<PlaceholderNode>) { fn visit_placeholder(&mut self, node: &Rc<PlaceholderNode>) {
@ -1118,14 +1107,13 @@ impl State {
self.dbus.clear(); self.dbus.clear();
self.pending_container_layout.clear(); self.pending_container_layout.clear();
self.pending_container_render_positions.clear(); self.pending_container_render_positions.clear();
self.pending_container_render_title.clear();
self.pending_output_render_data.clear(); self.pending_output_render_data.clear();
self.pending_float_layout.clear(); self.pending_float_layout.clear();
self.pending_float_titles.clear();
self.pending_input_popup_positioning.clear(); self.pending_input_popup_positioning.clear();
self.pending_toplevel_screencasts.clear(); self.pending_toplevel_screencasts.clear();
self.pending_screencast_reallocs_or_reconfigures.clear(); self.pending_screencast_reallocs_or_reconfigures.clear();
self.pending_placeholder_render_textures.clear(); self.pending_placeholder_render_textures.clear();
self.pending_container_tab_render_textures.clear();
self.render_ctx_watchers.clear(); self.render_ctx_watchers.clear();
self.workspace_watchers.clear(); self.workspace_watchers.clear();
self.toplevel_lists.clear(); self.toplevel_lists.clear();
@ -1296,8 +1284,8 @@ impl State {
let (width, height) = target.logical_size(target_transform); let (width, height) = target.logical_size(target_transform);
Rect::new_sized_saturating(0, 0, width, height) Rect::new_sized_saturating(0, 0, width, height)
}, },
icons: None,
stretch: None, stretch: None,
corner_radius: None,
}; };
let mut sample_rect = SampleRect::identity(); let mut sample_rect = SampleRect::identity();
sample_rect.buffer_transform = transform; sample_rect.buffer_transform = transform;
@ -1621,8 +1609,7 @@ impl State {
.unwrap_or(c); .unwrap_or(c);
Some(target.predict_child_body_size()) Some(target.predict_child_body_size())
} else { } else {
let tpuh = self.theme.title_plus_underline_height(); Some((pos.width(), pos.height()))
Some((pos.width(), (pos.height() - tpuh).max(0)))
} }
} }
@ -1863,11 +1850,13 @@ impl State {
self.spaces_changed(); self.spaces_changed();
} }
#[allow(dead_code)]
pub fn set_show_titles(&self, show: bool) { pub fn set_show_titles(&self, show: bool) {
self.theme.show_titles.set(show); self.theme.show_titles.set(show);
self.spaces_changed(); self.spaces_changed();
} }
#[allow(dead_code)]
pub fn set_floating_titles(&self, floating: bool) { pub fn set_floating_titles(&self, floating: bool) {
self.theme.floating_titles.set(floating); self.theme.floating_titles.set(floating);
self.spaces_changed(); self.spaces_changed();
@ -1882,13 +1871,9 @@ impl State {
.set(threshold.saturating_mul(threshold)); .set(threshold.saturating_mul(threshold));
} }
#[allow(dead_code)]
pub fn set_show_pin_icon(&self, show: bool) { pub fn set_show_pin_icon(&self, show: bool) {
self.show_pin_icon.set(show); self.show_pin_icon.set(show);
for stacked in self.root.stacked.iter() {
if let Some(float) = stacked.deref().clone().node_into_float() {
float.schedule_render_titles();
}
}
} }
pub fn set_float_above_fullscreen(&self, v: bool) { pub fn set_float_above_fullscreen(&self, v: bool) {
@ -1909,7 +1894,6 @@ impl State {
struct V; struct V;
impl NodeVisitorBase for V { impl NodeVisitorBase for V {
fn visit_container(&mut self, node: &Rc<ContainerNode>) { fn visit_container(&mut self, node: &Rc<ContainerNode>) {
node.schedule_render_titles();
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_output(&mut self, node: &Rc<OutputNode>) { fn visit_output(&mut self, node: &Rc<OutputNode>) {
@ -1917,7 +1901,6 @@ impl State {
node.node_visit_children(self); node.node_visit_children(self);
} }
fn visit_float(&mut self, node: &Rc<FloatNode>) { fn visit_float(&mut self, node: &Rc<FloatNode>) {
node.schedule_render_titles();
node.node_visit_children(self); node.node_visit_children(self);
} }
} }
@ -1943,6 +1926,7 @@ impl State {
self.fonts_changed(); self.fonts_changed();
} }
#[allow(dead_code)]
pub fn set_title_font(&self, font: Option<&str>) { pub fn set_title_font(&self, font: Option<&str>) {
let font = font.map(|font| Arc::new(font.to_string())); let font = font.map(|font| Arc::new(font.to_string()));
self.theme.title_font.set(font); self.theme.title_font.set(font);

View file

@ -415,6 +415,7 @@ impl Shared {
} }
} }
#[allow(dead_code)]
#[derive(PartialEq, Default)] #[derive(PartialEq, Default)]
enum Config<'a> { enum Config<'a> {
#[default] #[default]
@ -519,6 +520,7 @@ impl TextTexture {
self.data.pending_render.set(Some(pending)); self.data.pending_render.set(Some(pending));
} }
#[allow(dead_code)]
pub fn schedule_render( pub fn schedule_render(
&self, &self,
on_completed: Rc<dyn OnCompleted>, on_completed: Rc<dyn OnCompleted>,

View file

@ -454,6 +454,14 @@ colors! {
bar_text = (0xff, 0xff, 0xff), bar_text = (0xff, 0xff, 0xff),
attention_requested_background = (0x23, 0x09, 0x2c), attention_requested_background = (0x23, 0x09, 0x2c),
highlight = (0x9d, 0x28, 0xc6, 0x7f), highlight = (0x9d, 0x28, 0xc6, 0x7f),
tab_active_background = (0x4c, 0x78, 0x99),
tab_active_border = (0x28, 0x55, 0x77),
tab_inactive_background = (0x22, 0x22, 0x22),
tab_inactive_border = (0x33, 0x33, 0x33),
tab_active_text = (0xff, 0xff, 0xff),
tab_inactive_text = (0x88, 0x88, 0x88),
tab_bar_background = (0x00, 0x00, 0x00, 0x00),
tab_attention_background = (0x23, 0x09, 0x2c),
} }
impl StaticText for ThemeColor { impl StaticText for ThemeColor {
@ -476,6 +484,14 @@ impl StaticText for ThemeColor {
ThemeColor::bar_text => "Bar Text", ThemeColor::bar_text => "Bar Text",
ThemeColor::attention_requested_background => "Attention Requested", ThemeColor::attention_requested_background => "Attention Requested",
ThemeColor::highlight => "Highlight", ThemeColor::highlight => "Highlight",
ThemeColor::tab_active_background => "Tab Background (active)",
ThemeColor::tab_active_border => "Tab Border (active)",
ThemeColor::tab_inactive_background => "Tab Background (inactive)",
ThemeColor::tab_inactive_border => "Tab Border (inactive)",
ThemeColor::tab_active_text => "Tab Text (active)",
ThemeColor::tab_inactive_text => "Tab Text (inactive)",
ThemeColor::tab_bar_background => "Tab Bar Background",
ThemeColor::tab_attention_background => "Tab Attention Background",
} }
} }
} }
@ -588,6 +604,12 @@ sizes! {
bar_separator_width = (0, 1000, 1), bar_separator_width = (0, 1000, 1),
gap = (0, 1000, 0), gap = (0, 1000, 0),
title_gap = (0, 1000, 5), title_gap = (0, 1000, 5),
tab_bar_height = (0, 1000, 22),
tab_bar_padding = (0, 1000, 6),
tab_bar_radius = (0, 1000, 6),
tab_bar_border_width = (0, 1000, 2),
tab_bar_text_padding = (0, 1000, 4),
tab_bar_gap = (0, 1000, 4),
} }
impl StaticText for ThemeSized { impl StaticText for ThemeSized {
@ -599,6 +621,12 @@ impl StaticText for ThemeSized {
ThemeSized::bar_separator_width => "Bar Separator Width", ThemeSized::bar_separator_width => "Bar Separator Width",
ThemeSized::gap => "Gap", ThemeSized::gap => "Gap",
ThemeSized::title_gap => "Title Gap", ThemeSized::title_gap => "Title Gap",
ThemeSized::tab_bar_height => "Tab Bar Height",
ThemeSized::tab_bar_padding => "Tab Bar Padding",
ThemeSized::tab_bar_radius => "Tab Bar Radius",
ThemeSized::tab_bar_border_width => "Tab Bar Border Width",
ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding",
ThemeSized::tab_bar_gap => "Tab Bar Gap",
} }
} }
} }
@ -643,6 +671,104 @@ impl Into<ConfigBarPosition> for BarPosition {
} }
} }
/// Per-corner radius for rounded rectangles.
///
/// Each field specifies the radius (in logical pixels) for one corner.
/// A radius of 0 means a square corner.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct CornerRadius {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}
impl From<f32> for CornerRadius {
fn from(value: f32) -> Self {
Self {
top_left: value,
top_right: value,
bottom_right: value,
bottom_left: value,
}
}
}
impl From<CornerRadius> for [f32; 4] {
fn from(cr: CornerRadius) -> Self {
[cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left]
}
}
impl CornerRadius {
/// Shrink or grow all radii by `width`. Radii that are 0 stay 0 (square
/// corners remain square). Negative `width` shrinks; the result is clamped
/// to 0.
pub fn expanded_by(mut self, width: f32) -> Self {
if self.top_left > 0.0 {
self.top_left = (self.top_left + width).max(0.0);
}
if self.top_right > 0.0 {
self.top_right = (self.top_right + width).max(0.0);
}
if self.bottom_right > 0.0 {
self.bottom_right = (self.bottom_right + width).max(0.0);
}
if self.bottom_left > 0.0 {
self.bottom_left = (self.bottom_left + width).max(0.0);
}
self
}
/// Scale all radii by a factor (e.g. for HiDPI).
pub fn scaled_by(self, scale: f32) -> Self {
Self {
top_left: self.top_left * scale,
top_right: self.top_right * scale,
bottom_right: self.bottom_right * scale,
bottom_left: self.bottom_left * scale,
}
}
/// Reduce all radii proportionally so that adjacent corners don't overlap,
/// following the CSS spec algorithm.
pub fn fit_to(self, width: f32, height: f32) -> Self {
let reduction = f32::min(
f32::min(
width / (self.top_left + self.top_right),
width / (self.bottom_left + self.bottom_right),
),
f32::min(
height / (self.top_left + self.bottom_left),
height / (self.top_right + self.bottom_right),
),
);
let reduction = f32::min(1.0, reduction);
Self {
top_left: self.top_left * reduction,
top_right: self.top_right * reduction,
bottom_right: self.bottom_right * reduction,
bottom_left: self.bottom_left * reduction,
}
}
pub fn is_zero(&self) -> bool {
self.top_left == 0.0
&& self.top_right == 0.0
&& self.bottom_right == 0.0
&& self.bottom_left == 0.0
}
}
/// Horizontal alignment of title text inside tab buttons.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum TabTitleAlign {
#[default]
Start,
Center,
End,
}
pub struct Theme { pub struct Theme {
pub colors: ThemeColors, pub colors: ThemeColors,
pub sizes: ThemeSizes, pub sizes: ThemeSizes,
@ -650,9 +776,14 @@ pub struct Theme {
pub bar_font: CloneCell<Option<Arc<String>>>, pub bar_font: CloneCell<Option<Arc<String>>>,
pub title_font: CloneCell<Option<Arc<String>>>, pub title_font: CloneCell<Option<Arc<String>>>,
pub default_font: Arc<String>, pub default_font: Arc<String>,
#[allow(dead_code)]
pub show_titles: Cell<bool>, pub show_titles: Cell<bool>,
#[allow(dead_code)]
pub floating_titles: Cell<bool>, pub floating_titles: Cell<bool>,
pub bar_position: Cell<BarPosition>, pub bar_position: Cell<BarPosition>,
pub corner_radius: Cell<CornerRadius>,
pub autotile_enabled: Cell<bool>,
pub tab_title_align: Cell<TabTitleAlign>,
} }
impl Default for Theme { impl Default for Theme {
@ -668,50 +799,28 @@ impl Default for Theme {
show_titles: Cell::new(true), show_titles: Cell::new(true),
floating_titles: Cell::new(false), floating_titles: Cell::new(false),
bar_position: Default::default(), bar_position: Default::default(),
corner_radius: Cell::new(CornerRadius::default()),
autotile_enabled: Cell::new(false),
tab_title_align: Cell::new(TabTitleAlign::default()),
} }
} }
} }
impl Theme { impl Theme {
pub fn title_font(&self) -> Arc<String> {
self.title_font.get().unwrap_or_else(|| self.font.get())
}
pub fn bar_font(&self) -> Arc<String> { pub fn bar_font(&self) -> Arc<String> {
self.bar_font.get().unwrap_or_else(|| self.font.get()) self.bar_font.get().unwrap_or_else(|| self.font.get())
} }
pub fn title_font(&self) -> Arc<String> {
self.title_font.get().unwrap_or_else(|| self.font.get())
}
pub fn title_height(&self) -> i32 { pub fn title_height(&self) -> i32 {
if self.show_titles.get() { 0
self.sizes.title_height.get()
} else {
0
}
}
pub fn title_underline_height(&self) -> i32 {
if self.show_titles.get() { 1 } else { 0 }
}
pub fn floating_title_top_margin(&self) -> i32 {
if self.floating_titles.get() && self.sizes.gap.get() != 0 {
self.sizes.border_width.get()
} else {
0
}
} }
pub fn title_plus_underline_height(&self) -> i32 { pub fn title_plus_underline_height(&self) -> i32 {
if self.show_titles.get() { 0
if self.floating_titles.get() && self.sizes.gap.get() != 0 {
let bw = self.sizes.border_width.get();
3 * bw + self.sizes.title_height.get() + self.sizes.title_gap.get()
} else {
self.sizes.title_height.get() + 1
}
} else {
0
}
} }
} }

View file

@ -50,6 +50,7 @@ mod float;
mod output; mod output;
mod placeholder; mod placeholder;
mod stacked; mod stacked;
pub mod tab_bar;
mod toplevel; mod toplevel;
mod walker; mod walker;
mod workspace; mod workspace;

File diff suppressed because it is too large Load diff

View file

@ -10,27 +10,22 @@ use {
}, },
rect::Rect, rect::Rect,
renderer::Renderer, renderer::Renderer,
scale::Scale,
state::State, state::State,
text::TextTexture,
tree::{ tree::{
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId,
NodeLayerLink, NodeLocation, OutputNode, PinnedNode, StackedNode, TileDragDestination, NodeLayerLink, NodeLocation, OutputNode, PinnedNode, StackedNode, TileDragDestination,
ToplevelNode, WorkspaceNode, toplevel_set_floating, walker::NodeVisitor, ToplevelNode, WorkspaceNode, toplevel_set_floating, walker::NodeVisitor,
}, },
utils::{ utils::{
asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, clonecell::CloneCell, double_click_state::DoubleClickState,
errorfmt::ErrorFmt, event_listener::LazyEventSource, linkedlist::LinkedNode, event_listener::LazyEventSource, linkedlist::LinkedNode,
on_drop_event::OnDropEvent, smallmap::SmallMapMut,
}, },
}, },
ahash::AHashMap, ahash::AHashMap,
arrayvec::ArrayVec,
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::{Debug, Formatter}, fmt::{Debug, Formatter},
mem, mem,
ops::Deref,
rc::Rc, rc::Rc,
}, },
}; };
@ -50,10 +45,6 @@ pub struct FloatNode {
pub active: Cell<bool>, pub active: Cell<bool>,
pub seat_state: NodeSeatState, pub seat_state: NodeSeatState,
pub layout_scheduled: Cell<bool>, pub layout_scheduled: Cell<bool>,
pub render_titles_scheduled: Cell<bool>,
pub title_rect: Cell<Rect>,
pub title: RefCell<String>,
pub title_textures: RefCell<SmallMapMut<Scale, TextTexture, 2>>,
cursors: RefCell<AHashMap<CursorType, CursorState>>, cursors: RefCell<AHashMap<CursorType, CursorState>>,
pub attention_requested: Cell<bool>, pub attention_requested: Cell<bool>,
pub layout_complete: Rc<LazyEventSource>, pub layout_complete: Rc<LazyEventSource>,
@ -99,17 +90,6 @@ pub async fn float_layout(state: Rc<State>) {
} }
} }
pub async fn float_titles(state: Rc<State>) {
loop {
let node = state.pending_float_titles.pop().await;
if node.render_titles_scheduled.get() {
node.render_titles_scheduled.set(false);
node.render_title_phase1().triggered().await;
node.render_title_phase2();
}
}
}
impl FloatNode { impl FloatNode {
pub fn new( pub fn new(
state: &Rc<State>, state: &Rc<State>,
@ -131,10 +111,6 @@ impl FloatNode {
active: Cell::new(false), active: Cell::new(false),
seat_state: Default::default(), seat_state: Default::default(),
layout_scheduled: Cell::new(false), layout_scheduled: Cell::new(false),
render_titles_scheduled: Cell::new(false),
title_rect: Default::default(),
title: Default::default(),
title_textures: Default::default(),
cursors: Default::default(), cursors: Default::default(),
attention_requested: Cell::new(false), attention_requested: Cell::new(false),
layout_complete: state.post_layout_event_sources.create_source(), layout_complete: state.post_layout_event_sources.create_source(),
@ -162,7 +138,7 @@ impl FloatNode {
} }
pub fn on_colors_changed(self: &Rc<Self>) { pub fn on_colors_changed(self: &Rc<Self>) {
self.schedule_render_titles(); // No title rendering needed
} }
pub fn schedule_layout(self: &Rc<Self>) { pub fn schedule_layout(self: &Rc<Self>) {
@ -179,95 +155,17 @@ impl FloatNode {
let pos = self.position.get(); let pos = self.position.get();
let theme = &self.state.theme; let theme = &self.state.theme;
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let th = theme.title_height();
let tpuh = theme.title_plus_underline_height();
let cpos = Rect::new_sized_saturating( let cpos = Rect::new_sized_saturating(
pos.x1() + bw, pos.x1() + bw,
pos.y1() + bw + tpuh, pos.y1() + bw,
pos.width() - 2 * bw, pos.width() - 2 * bw,
pos.height() - 2 * bw - tpuh, pos.height() - 2 * bw,
); );
let tr = Rect::new_sized_saturating(bw, bw, pos.width() - 2 * bw, th);
child.clone().tl_change_extents(&cpos); child.clone().tl_change_extents(&cpos);
self.title_rect.set(tr);
self.layout_scheduled.set(false); self.layout_scheduled.set(false);
self.schedule_render_titles();
self.layout_complete.trigger(); self.layout_complete.trigger();
} }
pub fn schedule_render_titles(self: &Rc<Self>) {
if !self.render_titles_scheduled.replace(true) {
self.state.pending_float_titles.push(self.clone());
}
}
fn render_title_phase1(&self) -> Rc<AsyncEvent> {
let on_completed = Rc::new(OnDropEvent::default());
let theme = &self.state.theme;
let tc = match self.active.get() {
true => theme.colors.focused_title_text.get(),
false => theme.colors.unfocused_title_text.get(),
};
let font = theme.title_font();
let title = self.title.borrow_mut();
let ctx = match self.state.render_ctx.get() {
Some(c) => c,
_ => return on_completed.event(),
};
let scales = self.state.scales.lock();
let tr = self.title_rect.get();
let tt = &mut *self.title_textures.borrow_mut();
for (scale, _) in scales.iter() {
let tex = tt.get_or_insert_with(*scale, || TextTexture::new(&self.state, &ctx));
let mut th = tr.height();
let mut scalef = None;
let mut width = tr.width();
if self.state.show_pin_icon.get() || self.pinned_link.borrow().is_some() {
width = (width - th).max(0);
}
if *scale != 1 {
let scale = scale.to_f64();
th = (th as f64 * scale).round() as _;
width = (width as f64 * scale).round() as _;
scalef = Some(scale);
}
tex.schedule_render(
on_completed.clone(),
1,
None,
width,
th,
1,
&font,
&title,
tc,
true,
false,
scalef,
);
}
on_completed.event()
}
fn render_title_phase2(&self) {
let theme = &self.state.theme;
let th = theme.title_height();
let bw = theme.sizes.border_width.get();
let title = self.title.borrow();
let tt = &*self.title_textures.borrow();
for (_, tt) in tt {
if let Err(e) = tt.flip() {
log::error!("Could not render title {}: {}", title, ErrorFmt(e));
}
}
let pos = self.position.get();
if self.visible.get() && pos.width() >= 2 * bw {
let tr =
Rect::new_sized_saturating(pos.x1() + bw, pos.y1() + bw, pos.width() - 2 * bw, th);
self.state.damage(tr);
}
}
fn pointer_move( fn pointer_move(
self: &Rc<Self>, self: &Rc<Self>,
id: CursorType, id: CursorType,
@ -280,7 +178,6 @@ impl FloatNode {
let y = y.round_down(); let y = y.round_down();
let theme = &self.state.theme; let theme = &self.state.theme;
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let tpuh = theme.title_plus_underline_height();
let mut seats = self.cursors.borrow_mut(); let mut seats = self.cursors.borrow_mut();
let seat_state = seats.entry(id).or_insert_with(|| CursorState { let seat_state = seats.entry(id).or_insert_with(|| CursorState {
cursor: KnownCursor::Default, cursor: KnownCursor::Default,
@ -316,7 +213,7 @@ impl FloatNode {
} }
OpType::ResizeTop => { OpType::ResizeTop => {
y1 += y - seat_state.dist_ver; y1 += y - seat_state.dist_ver;
y1 = y1.min(y2 - 2 * bw - tpuh); y1 = y1.min(y2 - 2 * bw);
} }
OpType::ResizeRight => { OpType::ResizeRight => {
x2 += x - pos.width() + seat_state.dist_hor; x2 += x - pos.width() + seat_state.dist_hor;
@ -324,31 +221,31 @@ impl FloatNode {
} }
OpType::ResizeBottom => { OpType::ResizeBottom => {
y2 += y - pos.height() + seat_state.dist_ver; y2 += y - pos.height() + seat_state.dist_ver;
y2 = y2.max(y1 + 2 * bw + tpuh); y2 = y2.max(y1 + 2 * bw);
} }
OpType::ResizeTopLeft => { OpType::ResizeTopLeft => {
x1 += x - seat_state.dist_hor; x1 += x - seat_state.dist_hor;
y1 += y - seat_state.dist_ver; y1 += y - seat_state.dist_ver;
x1 = x1.min(x2 - 2 * bw); x1 = x1.min(x2 - 2 * bw);
y1 = y1.min(y2 - 2 * bw - tpuh); y1 = y1.min(y2 - 2 * bw);
} }
OpType::ResizeTopRight => { OpType::ResizeTopRight => {
x2 += x - pos.width() + seat_state.dist_hor; x2 += x - pos.width() + seat_state.dist_hor;
y1 += y - seat_state.dist_ver; y1 += y - seat_state.dist_ver;
x2 = x2.max(x1 + 2 * bw); x2 = x2.max(x1 + 2 * bw);
y1 = y1.min(y2 - 2 * bw - tpuh); y1 = y1.min(y2 - 2 * bw);
} }
OpType::ResizeBottomLeft => { OpType::ResizeBottomLeft => {
x1 += x - seat_state.dist_hor; x1 += x - seat_state.dist_hor;
y2 += y - pos.height() + seat_state.dist_ver; y2 += y - pos.height() + seat_state.dist_ver;
x1 = x1.min(x2 - 2 * bw); x1 = x1.min(x2 - 2 * bw);
y2 = y2.max(y1 + 2 * bw + tpuh); y2 = y2.max(y1 + 2 * bw);
} }
OpType::ResizeBottomRight => { OpType::ResizeBottomRight => {
x2 += x - pos.width() + seat_state.dist_hor; x2 += x - pos.width() + seat_state.dist_hor;
y2 += y - pos.height() + seat_state.dist_ver; y2 += y - pos.height() + seat_state.dist_ver;
x2 = x2.max(x1 + 2 * bw); x2 = x2.max(x1 + 2 * bw);
y2 = y2.max(y1 + 2 * bw + tpuh); y2 = y2.max(y1 + 2 * bw);
} }
} }
let new_pos = Rect::new_saturating(x1, y1, x2, y2); let new_pos = Rect::new_saturating(x1, y1, x2, y2);
@ -474,18 +371,12 @@ impl FloatNode {
self.schedule_layout(); self.schedule_layout();
} }
fn update_child_title(self: &Rc<Self>, title: &str) { fn update_child_title(self: &Rc<Self>, _title: &str) {
let mut t = self.title.borrow_mut(); // No title rendering
if t.deref() != title {
t.clear();
t.push_str(title);
self.schedule_render_titles();
}
} }
fn update_child_active(self: &Rc<Self>, active: bool) { fn update_child_active(self: &Rc<Self>, active: bool) {
if self.active.replace(active) != active { if self.active.replace(active) != active {
self.schedule_render_titles();
if active { if active {
self.restack(); self.restack();
} }
@ -542,7 +433,6 @@ impl FloatNode {
if let Some(tl) = self.child.get() { if let Some(tl) = self.child.get() {
tl.tl_data().pinned.set(pl.is_some()); tl.tl_data().pinned.set(pl.is_some());
} }
self.schedule_render_titles();
} }
fn button( fn button(
@ -558,34 +448,6 @@ impl FloatNode {
Some(s) => s, Some(s) => s,
_ => return, _ => return,
}; };
let bw = self.state.theme.sizes.border_width.get();
let th = self.state.theme.title_height();
let mut is_icon_press = false;
if pressed && cursor_data.x >= bw && cursor_data.y >= bw && cursor_data.y < bw + th {
enum FloatIcon {
Pin,
}
let mut icons = ArrayVec::<FloatIcon, 1>::new();
if self.state.show_pin_icon.get() || self.pinned_link.borrow().is_some() {
icons.push(FloatIcon::Pin);
}
let mut x2 = bw + th;
let icon = 'icon: {
for icon in icons {
if cursor_data.x < x2 {
break 'icon Some(icon);
}
x2 += th;
}
None
};
if let Some(icon) = icon {
is_icon_press = true;
match icon {
FloatIcon::Pin => self.toggle_pinned(),
}
}
}
if !cursor_data.op_active { if !cursor_data.op_active {
if !pressed { if !pressed {
return; return;
@ -601,7 +463,6 @@ impl FloatNode {
cursor_data.x, cursor_data.x,
cursor_data.y, cursor_data.y,
) && cursor_data.op_type == OpType::Move ) && cursor_data.op_type == OpType::Move
&& !is_icon_press
&& let Some(tl) = self.child.get() && let Some(tl) = self.child.get()
{ {
drop(cursors); drop(cursors);
@ -653,11 +514,10 @@ impl FloatNode {
let child = self.child.get()?; let child = self.child.get()?;
let theme = &self.state.theme.sizes; let theme = &self.state.theme.sizes;
let bw = theme.border_width.get(); let bw = theme.border_width.get();
let tpuh = self.state.theme.title_plus_underline_height();
let pos = self.position.get(); let pos = self.position.get();
let body = Rect::new( let body = Rect::new(
pos.x1() + bw, pos.x1() + bw,
pos.y1() + bw + tpuh, pos.y1() + bw,
pos.x2() - bw, pos.x2() - bw,
pos.y2() - bw, pos.y2() - bw,
)?; )?;
@ -713,8 +573,8 @@ impl Node for FloatNode {
NodeLayerLink::Stacked(l) NodeLayerLink::Stacked(l)
} }
fn node_child_title_changed(self: Rc<Self>, _child: &dyn Node, title: &str) { fn node_child_title_changed(self: Rc<Self>, _child: &dyn Node, _title: &str) {
self.update_child_title(title); // No title rendering
} }
fn node_accepts_focus(&self) -> bool { fn node_accepts_focus(&self) -> bool {
@ -738,13 +598,12 @@ impl Node for FloatNode {
usecase: FindTreeUsecase, usecase: FindTreeUsecase,
) -> FindTreeResult { ) -> FindTreeResult {
let theme = &self.state.theme; let theme = &self.state.theme;
let tpuh = theme.title_plus_underline_height();
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let pos = self.position.get(); let pos = self.position.get();
if x < bw || x >= pos.width() - bw { if x < bw || x >= pos.width() - bw {
return FindTreeResult::AcceptsInput; return FindTreeResult::AcceptsInput;
} }
if y < bw + tpuh || y >= pos.height() - bw { if y < bw || y >= pos.height() - bw {
return FindTreeResult::AcceptsInput; return FindTreeResult::AcceptsInput;
} }
let child = match self.child.get() { let child = match self.child.get() {
@ -752,7 +611,7 @@ impl Node for FloatNode {
_ => return FindTreeResult::Other, _ => return FindTreeResult::Other,
}; };
let x = x - bw; let x = x - bw;
let y = y - bw - tpuh; let y = y - bw;
tree.push(FoundNode { tree.push(FoundNode {
node: child.clone(), node: child.clone(),
x, x,
@ -934,9 +793,8 @@ impl ContainingNode for FloatNode {
fn cnode_set_child_position(self: Rc<Self>, _child: &dyn Node, x: i32, y: i32) { fn cnode_set_child_position(self: Rc<Self>, _child: &dyn Node, x: i32, y: i32) {
let theme = &self.state.theme; let theme = &self.state.theme;
let tpuh = theme.title_plus_underline_height();
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let (x, y) = (x - bw, y - tpuh - bw); let (x, y) = (x - bw, y - bw);
let pos = self.position.get(); let pos = self.position.get();
if pos.position() != (x, y) { if pos.position() != (x, y) {
let new_pos = pos.at_point(x, y); let new_pos = pos.at_point(x, y);
@ -956,7 +814,6 @@ impl ContainingNode for FloatNode {
new_y2: Option<i32>, new_y2: Option<i32>,
) { ) {
let theme = &self.state.theme; let theme = &self.state.theme;
let tpuh = theme.title_plus_underline_height();
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let pos = self.position.get(); let pos = self.position.get();
let mut x1 = pos.x1(); let mut x1 = pos.x1();
@ -970,10 +827,10 @@ impl ContainingNode for FloatNode {
x2 = (v + bw).max(x1 + bw + bw); x2 = (v + bw).max(x1 + bw + bw);
} }
if let Some(v) = new_y1 { if let Some(v) = new_y1 {
y1 = (v - tpuh - bw).min(y2 - bw - tpuh - bw); y1 = (v - bw).min(y2 - bw - bw);
} }
if let Some(v) = new_y2 { if let Some(v) = new_y2 {
y2 = (v + bw).max(y1 + bw + tpuh + bw); y2 = (v + bw).max(y1 + bw + bw);
} }
let new_pos = Rect::new_saturating(x1, y1, x2, y2); let new_pos = Rect::new_saturating(x1, y1, x2, y2);
if new_pos != pos { if new_pos != pos {

113
src/tree/tab_bar.rs Normal file
View file

@ -0,0 +1,113 @@
use {
crate::{
scale::Scale,
state::State,
text::TextTexture,
theme::Color,
tree::NodeId,
},
std::{cell::{Cell, RefCell}, rc::Rc},
};
/// A single entry (tab) within a tab bar.
pub struct TabBarEntry {
/// The node ID of the child this tab represents.
pub child_id: NodeId,
/// The display title of the tab.
pub title: String,
/// Pre-rendered text texture for the tab title.
pub title_texture: Rc<RefCell<Option<TextTexture>>>,
/// Whether this is the active (visible) tab.
pub active: bool,
/// Whether this tab's window has requested attention.
pub attention_requested: bool,
/// X offset of this tab within the tab bar (relative to tab bar start).
pub x: Cell<i32>,
/// Width of this tab in pixels.
pub width: Cell<i32>,
}
/// A tab bar rendered above a container in mono (tabbed) mode.
pub struct TabBar {
/// The individual tab entries.
pub entries: Vec<TabBarEntry>,
/// Height of the tab bar in pixels (from theme).
pub height: i32,
/// The output scale at which text textures were rendered.
pub render_scale: Scale,
}
impl TabBar {
/// Create a new empty tab bar.
pub fn new(height: i32, render_scale: Scale) -> Self {
Self {
entries: Vec::new(),
height,
render_scale,
}
}
/// Recompute the positions and widths of all tab entries.
///
/// `total_width` is the available width for the entire tab bar.
pub fn layout_entries(&self, total_width: i32, padding: i32) {
let n = self.entries.len() as i32;
if n == 0 {
return;
}
let total_padding = padding * (n + 1);
let available = (total_width - total_padding).max(0);
let per_tab = available / n;
let mut remainder = available - per_tab * n;
let mut x = padding;
for entry in &self.entries {
let w = if remainder > 0 {
remainder -= 1;
per_tab + 1
} else {
per_tab
};
entry.x.set(x);
entry.width.set(w);
x += w + padding;
}
}
/// Find the tab entry index at the given x coordinate (relative to tab bar).
///
/// Returns `None` if the coordinate is in padding between tabs or out of bounds.
pub fn entry_at_x(&self, x: i32) -> Option<usize> {
for (i, entry) in self.entries.iter().enumerate() {
let ex = entry.x.get();
let ew = entry.width.get();
if x >= ex && x < ex + ew {
return Some(i);
}
}
None
}
/// Get the colors for a tab entry based on its state.
pub fn entry_colors(state: &State, entry: &TabBarEntry) -> (Color, Color, Color) {
let theme = &state.theme;
if entry.attention_requested {
(
theme.colors.tab_attention_background.get(),
theme.colors.tab_inactive_border.get(),
theme.colors.tab_active_text.get(),
)
} else if entry.active {
(
theme.colors.tab_active_background.get(),
theme.colors.tab_active_border.get(),
theme.colors.tab_active_text.get(),
)
} else {
(
theme.colors.tab_inactive_background.get(),
theme.colors.tab_inactive_border.get(),
theme.colors.tab_inactive_text.get(),
)
}
}
}

View file

@ -23,7 +23,7 @@ use {
}, },
ahash::AHashMap, ahash::AHashMap,
jay_config::{ jay_config::{
Axis, Direction, Workspace, Direction, Workspace,
client::ClientCapabilities, client::ClientCapabilities,
input::{ input::{
FallbackOutputMode, LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile, FallbackOutputMode, LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile,
@ -60,15 +60,10 @@ pub enum SimpleCommand {
Quit, Quit,
ReloadConfigSo, ReloadConfigSo,
ReloadConfigToml, ReloadConfigToml,
Split(Axis),
ToggleFloating, ToggleFloating,
SetFloating(bool), SetFloating(bool),
ToggleFullscreen, ToggleFullscreen,
SetFullscreen(bool), SetFullscreen(bool),
ToggleMono,
SetMono(bool),
ToggleSplit,
SetSplit(Axis),
Forward(bool), Forward(bool),
EnableWindowManagement(bool), EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool), SetFloatAboveFullscreen(bool),
@ -94,6 +89,17 @@ pub enum SimpleCommand {
ReloadSimpleIm, ReloadSimpleIm,
EnableUnicodeInput, EnableUnicodeInput,
WarpMouseToFocus, WarpMouseToFocus,
ToggleTab,
MakeGroupH,
MakeGroupV,
MakeGroupTab,
ChangeGroupOpposite,
Equalize,
EqualizeRecursive,
MoveTabLeft,
MoveTabRight,
SetAutotile(bool),
ToggleAutotile,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -228,6 +234,22 @@ pub struct Theme {
pub gap: Option<i32>, pub gap: Option<i32>,
pub floating_titles: Option<bool>, pub floating_titles: Option<bool>,
pub title_gap: Option<i32>, pub title_gap: Option<i32>,
pub corner_radius: Option<f32>,
pub tab_active_bg_color: Option<Color>,
pub tab_active_border_color: Option<Color>,
pub tab_inactive_bg_color: Option<Color>,
pub tab_inactive_border_color: Option<Color>,
pub tab_active_text_color: Option<Color>,
pub tab_inactive_text_color: Option<Color>,
pub tab_bar_bg_color: Option<Color>,
pub tab_attention_bg_color: Option<Color>,
pub tab_bar_height: Option<i32>,
pub tab_bar_padding: Option<i32>,
pub tab_bar_radius: Option<i32>,
pub tab_bar_border_width: Option<i32>,
pub tab_bar_text_padding: Option<i32>,
pub tab_bar_gap: Option<i32>,
pub tab_title_align: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -33,7 +33,6 @@ use {
}, },
indexmap::IndexMap, indexmap::IndexMap,
jay_config::{ jay_config::{
Axis::{Horizontal, Vertical},
Direction, get_workspace, Direction, get_workspace,
input::{LayerDirection, Timeline}, input::{LayerDirection, Timeline},
}, },
@ -115,14 +114,6 @@ impl ActionParser<'_> {
"move-down" => Move(Down), "move-down" => Move(Down),
"move-up" => Move(Up), "move-up" => Move(Up),
"move-right" => Move(Right), "move-right" => Move(Right),
"split-horizontal" => Split(Horizontal),
"split-vertical" => Split(Vertical),
"toggle-split" => ToggleSplit,
"tile-horizontal" => SetSplit(Horizontal),
"tile-vertical" => SetSplit(Vertical),
"toggle-mono" => ToggleMono,
"show-single" => SetMono(true),
"show-all" => SetMono(false),
"toggle-fullscreen" => ToggleFullscreen, "toggle-fullscreen" => ToggleFullscreen,
"enter-fullscreen" => SetFullscreen(true), "enter-fullscreen" => SetFullscreen(true),
"exit-fullscreen" => SetFullscreen(false), "exit-fullscreen" => SetFullscreen(false),
@ -172,6 +163,18 @@ impl ActionParser<'_> {
"reload-simple-im" => ReloadSimpleIm, "reload-simple-im" => ReloadSimpleIm,
"enable-unicode-input" => EnableUnicodeInput, "enable-unicode-input" => EnableUnicodeInput,
"warp-mouse-to-focus" => WarpMouseToFocus, "warp-mouse-to-focus" => WarpMouseToFocus,
"toggle-tab" => ToggleTab,
"make-group-h" => MakeGroupH,
"make-group-v" => MakeGroupV,
"make-group-tab" => MakeGroupTab,
"change-group-opposite" => ChangeGroupOpposite,
"equalize" => Equalize,
"equalize-recursive" => EqualizeRecursive,
"move-tab-left" => MoveTabLeft,
"move-tab-right" => MoveTabRight,
"enable-autotile" => SetAutotile(true),
"disable-autotile" => SetAutotile(false),
"toggle-autotile" => ToggleAutotile,
_ => { _ => {
return Err( return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -3,7 +3,7 @@ use {
config::{ config::{
Theme, Theme,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, bol, opt, recover, s32, str, val}, extractor::{Extractor, ExtractorError, bol, fltorint, opt, recover, s32, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::color::ColorParser, parsers::color::ColorParser,
}, },
@ -97,7 +97,45 @@ impl Parser for ThemeParser<'_> {
recover(opt(bol("floating-titles"))), recover(opt(bol("floating-titles"))),
), ),
))?; ))?;
let (title_gap,) = ext.extract((recover(opt(s32("title-gap"))),))?; let (title_gap, corner_radius) = ext.extract((
recover(opt(s32("title-gap"))),
recover(opt(fltorint("corner-radius"))),
))?;
let (
(
tab_active_bg_color,
tab_active_border_color,
tab_inactive_bg_color,
tab_inactive_border_color,
tab_active_text_color,
tab_inactive_text_color,
tab_bar_bg_color,
tab_attention_bg_color,
tab_bar_height,
tab_bar_padding,
),
(tab_bar_radius, tab_bar_border_width, tab_bar_text_padding, tab_bar_gap, tab_title_align_val),
) = ext.extract((
(
opt(val("tab-active-bg-color")),
opt(val("tab-active-border-color")),
opt(val("tab-inactive-bg-color")),
opt(val("tab-inactive-border-color")),
opt(val("tab-active-text-color")),
opt(val("tab-inactive-text-color")),
opt(val("tab-bar-bg-color")),
opt(val("tab-attention-bg-color")),
recover(opt(s32("tab-bar-height"))),
recover(opt(s32("tab-bar-padding"))),
),
(
recover(opt(s32("tab-bar-radius"))),
recover(opt(s32("tab-bar-border-width"))),
recover(opt(s32("tab-bar-text-padding"))),
recover(opt(s32("tab-bar-gap"))),
recover(opt(str("tab-title-align"))),
),
))?;
macro_rules! color { macro_rules! color {
($e:expr) => { ($e:expr) => {
match $e { match $e {
@ -152,6 +190,22 @@ impl Parser for ThemeParser<'_> {
gap: gap.despan(), gap: gap.despan(),
floating_titles: floating_titles.despan(), floating_titles: floating_titles.despan(),
title_gap: title_gap.despan(), title_gap: title_gap.despan(),
corner_radius: corner_radius.map(|v| v.value as f32),
tab_active_bg_color: color!(tab_active_bg_color),
tab_active_border_color: color!(tab_active_border_color),
tab_inactive_bg_color: color!(tab_inactive_bg_color),
tab_inactive_border_color: color!(tab_inactive_border_color),
tab_active_text_color: color!(tab_active_text_color),
tab_inactive_text_color: color!(tab_inactive_text_color),
tab_bar_bg_color: color!(tab_bar_bg_color),
tab_attention_bg_color: color!(tab_attention_bg_color),
tab_bar_height: tab_bar_height.despan(),
tab_bar_padding: tab_bar_padding.despan(),
tab_bar_radius: tab_bar_radius.despan(),
tab_bar_border_width: tab_bar_border_width.despan(),
tab_bar_text_padding: tab_bar_text_padding.despan(),
tab_bar_gap: tab_bar_gap.despan(),
tab_title_align: tab_title_align_val.map(|v| v.value.to_string()),
}) })
} }
} }

View file

@ -23,6 +23,7 @@ use {
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
error_reporter::Report, error_reporter::Report,
jay_config::{ jay_config::{
Axis,
client::Client, client::Client,
config, config_dir, config, config_dir,
exec::{Command, set_env, unset_env}, exec::{Command, set_env, unset_env},
@ -40,7 +41,7 @@ use {
set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled,
set_float_above_fullscreen, set_idle, set_idle_grace_period, set_float_above_fullscreen, set_idle, set_idle_grace_period,
set_floating_titles, set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, set_floating_titles, set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon,
set_show_titles, set_show_titles, set_corner_radius, set_autotile, set_tab_title_align,
set_ui_drag_enabled, set_ui_drag_threshold, set_ui_drag_enabled, set_ui_drag_threshold,
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
@ -169,11 +170,6 @@ impl Action {
Action::SimpleCommand { cmd } => match cmd { Action::SimpleCommand { cmd } => match cmd {
SimpleCommand::Focus(dir) => b.new(move || s.focus(dir)), SimpleCommand::Focus(dir) => b.new(move || s.focus(dir)),
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
SimpleCommand::Split(axis) => window_or_seat!(s, s.create_split(axis)),
SimpleCommand::ToggleSplit => window_or_seat!(s, s.toggle_split()),
SimpleCommand::SetSplit(b) => window_or_seat!(s, s.set_split(b)),
SimpleCommand::ToggleMono => window_or_seat!(s, s.toggle_mono()),
SimpleCommand::SetMono(b) => window_or_seat!(s, s.set_mono(b)),
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
@ -259,6 +255,35 @@ impl Action {
let persistent = state.persistent.clone(); let persistent = state.persistent.clone();
b.new(move || persistent.seat.warp_mouse_to_focus()) b.new(move || persistent.seat.warp_mouse_to_focus())
} }
SimpleCommand::ToggleTab => b.new(move || s.toggle_tab()),
SimpleCommand::MakeGroupH => {
b.new(move || s.make_group(Axis::Horizontal, true))
}
SimpleCommand::MakeGroupV => {
b.new(move || s.make_group(Axis::Vertical, true))
}
SimpleCommand::MakeGroupTab => {
b.new(move || {
s.make_group(Axis::Horizontal, true);
s.toggle_tab();
})
}
SimpleCommand::ChangeGroupOpposite => {
b.new(move || s.change_group_opposite())
}
SimpleCommand::Equalize => b.new(move || s.equalize(false)),
SimpleCommand::EqualizeRecursive => b.new(move || s.equalize(true)),
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
SimpleCommand::SetAutotile(enabled) => {
b.new(move || set_autotile(enabled))
}
SimpleCommand::ToggleAutotile => {
b.new(move || {
// Toggle not directly supported; set to true
set_autotile(true)
})
}
}, },
Action::Multi { actions } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -997,6 +1022,14 @@ impl State {
color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color); color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color);
color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color); color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color);
color!(HIGHLIGHT_COLOR, highlight_color); color!(HIGHLIGHT_COLOR, highlight_color);
color!(TAB_ACTIVE_BACKGROUND_COLOR, tab_active_bg_color);
color!(TAB_ACTIVE_BORDER_COLOR, tab_active_border_color);
color!(TAB_INACTIVE_BACKGROUND_COLOR, tab_inactive_bg_color);
color!(TAB_INACTIVE_BORDER_COLOR, tab_inactive_border_color);
color!(TAB_ACTIVE_TEXT_COLOR, tab_active_text_color);
color!(TAB_INACTIVE_TEXT_COLOR, tab_inactive_text_color);
color!(TAB_BAR_BACKGROUND_COLOR, tab_bar_bg_color);
color!(TAB_ATTENTION_BACKGROUND_COLOR, tab_attention_bg_color);
macro_rules! size { macro_rules! size {
($sized:ident, $field:ident) => { ($sized:ident, $field:ident) => {
if let Some(size) = theme.$field { if let Some(size) = theme.$field {
@ -1010,6 +1043,12 @@ impl State {
size!(BAR_SEPARATOR_WIDTH, bar_separator_width); size!(BAR_SEPARATOR_WIDTH, bar_separator_width);
size!(GAP, gap); size!(GAP, gap);
size!(TITLE_GAP, title_gap); size!(TITLE_GAP, title_gap);
size!(TAB_BAR_HEIGHT, tab_bar_height);
size!(TAB_BAR_PADDING, tab_bar_padding);
size!(TAB_BAR_RADIUS, tab_bar_radius);
size!(TAB_BAR_BORDER_WIDTH, tab_bar_border_width);
size!(TAB_BAR_TEXT_PADDING, tab_bar_text_padding);
size!(TAB_BAR_GAP, tab_bar_gap);
macro_rules! font { macro_rules! font {
($fun:ident, $field:ident) => { ($fun:ident, $field:ident) => {
if let Some(font) = &theme.$field { if let Some(font) = &theme.$field {
@ -1020,6 +1059,12 @@ impl State {
font!(set_font, font); font!(set_font, font);
font!(set_title_font, title_font); font!(set_title_font, title_font);
font!(set_bar_font, bar_font); font!(set_bar_font, bar_font);
if let Some(radius) = theme.corner_radius {
set_corner_radius(radius);
}
if let Some(ref align) = theme.tab_title_align {
set_tab_title_align(align);
}
} }
fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) { fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) {

View file

@ -2426,6 +2426,11 @@ Theme:
kind: string kind: string
required: false required: false
description: The name of the font to use in the bar. Defaults to `font` if not set. description: The name of the font to use in the bar. Defaults to `font` if not set.
corner-radius:
kind: number
minimum: 0
required: false
description: The corner radius for window borders in logical pixels. Defaults to 0 (square corners).
Config: Config: