Compare commits

...

10 Commits

Author SHA1 Message Date
Shoaib Merchant 6d9e637217 Merge pull request #1 from namana-mecha/main
feat: adds wayland window integration in bevy
2025-09-11 15:30:17 +05:30
Naman Agrawal acf9fd122f feat: Add basic support for foreign top level management.
- Allow users to minimize other applications by sending a
`ForeignToplevelEvent`.
2025-09-01 11:04:34 +05:30
Naman Agrawal 541e0e646f feat: add support to trigger refresh through external events.
- Allow users to trigger a dispatch by using
`Res<ExternalEventDispatcher>`.
2025-08-28 17:30:43 +05:30
Naman Agrawal a91508672e cleanup: add session lock import to prelude
- Add `SessionLockWindow` and `SessionLockEvent` to
`bevy_wayland::prelude::*`
2025-08-28 11:21:16 +05:30
Naman Agrawal 556222135c Merge pull request #1 from akshayr-mecha/cleanup
cleanup: adds prelude module
2025-08-28 11:17:39 +05:30
akshayr-mecha 6564845183 cleanup: adds prelude module 2025-08-28 11:16:08 +05:30
Naman Agrawal 3259ef0d53 feat: add feature to create session lock surfaces.
- Allow users to create session lock surfaces by passing an event
`SessionLockEvent::Lock` and unlock them using
`SessionLockEvent::Unlock`.
- Other surfaces of the application should be destroyed before the
session lock is aquired. The wayland client is only allowed to render to
the lock surfaces while the lock is active.
2025-08-28 11:11:48 +05:30
Naman Agrawal fc96e7a8ac feat: layer shell size now has the option to inherit from the bevy window.
- We had to always manually specify the size of the layer shell window,
now it can be inherited from the bevy window by setting
`layer_shell_settings.size = LayerShellWindowSize::Inherit` which is the
default.
- You can still manully set the layer shell size by setting
`layer_shell_settings.size = LayerShellWindowSize::Fixed(width,
height)`.
2025-08-27 11:11:28 +05:30
Naman Agrawal 494b2cdf52 chore: removed unused imports 2025-08-27 11:08:29 +05:30
Naman Agrawal ce15260e3d chore: simplified layer shell example
- The layer shell example now opens only one window instead of opening
multiple windows.
2025-08-27 11:07:24 +05:30
14 changed files with 1129 additions and 1077 deletions
Generated
+215 -972
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -4,8 +4,17 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.16.1"
bevy = { version = "0.16.1", default-features = false, features = [
"bevy_ui",
"bevy_window",
"trace",
"bevy_winit",
"wayland",
"multi_threaded",
] }
lazy_static = "1.5.0"
raw-window-handle = "0.6.2"
smithay-client-toolkit = "0.20.0"
wayland-backend = { version = "0.3.11", features = ["client_system"] }
wayland-protocols-wlr = "0.3.9"
+93
View File
@@ -0,0 +1,93 @@
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+136
View File
@@ -0,0 +1,136 @@
use std::time::Duration;
use bevy::{color::palettes::basic::*, prelude::*, window::WindowResolution, winit::WinitPlugin};
use bevy_wayland::{prelude::*, ExternalEventDispatcher};
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.build()
.disable::<WinitPlugin>()
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(400.0, 400.0),
present_mode: bevy::window::PresentMode::AutoVsync,
..Default::default()
}),
..Default::default()
}),
WaylandPlugin,
))
.add_systems(Startup, (setup, external_tick_sender))
.add_systems(Update, (button_system, exit_on_esc))
.run();
}
fn external_tick_sender(external_event_dispatcher: Res<ExternalEventDispatcher>) {
let displatcher = external_event_dispatcher.clone();
let mut count = 5;
std::thread::spawn(move || loop {
println!("Spawned Thread");
std::thread::sleep(Duration::from_secs(1));
displatcher.dispatch().unwrap();
count -= 1;
if count < 0 {
return;
}
});
}
#[allow(clippy::type_complexity)]
fn button_system(
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&Children,
),
(Changed<Interaction>, With<Button>),
>,
mut text_query: Query<&mut Text>,
) {
//info!("Button system was called!!");
for (interaction, mut color, mut border_color, children) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
**text = "Press".to_string();
*color = PRESSED_BUTTON.into();
border_color.0 = RED.into();
}
Interaction::Hovered => {
**text = "Hover".to_string();
*color = HOVERED_BUTTON.into();
border_color.0 = Color::WHITE;
}
Interaction::None => {
**text = "Button".to_string();
*color = NORMAL_BUTTON.into();
border_color.0 = Color::BLACK;
}
}
}
}
fn setup(mut commands: Commands, assets: Res<AssetServer>, windows: Query<Entity, With<Window>>) {
for entity in &windows {
commands.entity(entity).insert((LayerShellSettings {
anchor: Anchor::TOP | Anchor::LEFT,
layer: Layer::Bottom,
..Default::default()
},));
}
// ui camera
commands.spawn(Camera2d);
commands.spawn(button(&assets));
}
fn exit_on_esc(keys: Res<ButtonInput<KeyCode>>) {
if keys.just_pressed(KeyCode::Escape) {
std::process::exit(0);
}
}
fn button(asset_server: &AssetServer) -> impl Bundle + use<> {
(
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Button,
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)],
)
}
+122
View File
@@ -0,0 +1,122 @@
use bevy::{color::palettes::basic::*, prelude::*, window::WindowResolution, winit::WinitPlugin};
use bevy_wayland::{foreign_toplevel_manager::ForeignToplevelEvent, prelude::*};
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.build()
.disable::<WinitPlugin>()
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(400.0, 400.0),
present_mode: bevy::window::PresentMode::AutoVsync,
..Default::default()
}),
..Default::default()
}),
WaylandPlugin,
))
.add_systems(Startup, setup)
.add_systems(Update, (button_system, exit_on_esc))
.run();
}
#[allow(clippy::type_complexity)]
fn button_system(
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&Children,
),
(Changed<Interaction>, With<Button>),
>,
mut foreign_toplevel_event_writer: EventWriter<ForeignToplevelEvent>,
mut text_query: Query<&mut Text>,
) {
//info!("Button system was called!!");
for (interaction, mut color, mut border_color, children) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
**text = "Press".to_string();
*color = PRESSED_BUTTON.into();
border_color.0 = RED.into();
foreign_toplevel_event_writer.write(ForeignToplevelEvent::MinimizeOthers);
}
Interaction::Hovered => {
**text = "Hover".to_string();
*color = HOVERED_BUTTON.into();
border_color.0 = Color::WHITE;
}
Interaction::None => {
**text = "Button".to_string();
*color = NORMAL_BUTTON.into();
border_color.0 = Color::BLACK;
}
}
}
}
fn setup(mut commands: Commands, assets: Res<AssetServer>, windows: Query<Entity, With<Window>>) {
for entity in &windows {
commands.entity(entity).insert((LayerShellSettings {
anchor: Anchor::TOP | Anchor::LEFT,
layer: Layer::Top,
..Default::default()
},));
}
// ui camera
commands.spawn(Camera2d);
commands.spawn(button(&assets));
}
fn exit_on_esc(keys: Res<ButtonInput<KeyCode>>) {
if keys.just_pressed(KeyCode::Escape) {
std::process::exit(0);
}
}
fn button(asset_server: &AssetServer) -> impl Bundle + use<> {
(
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Button,
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)],
)
}
+205
View File
@@ -0,0 +1,205 @@
use bevy::{
prelude::*,
window::{exit_on_all_closed, WindowRef},
winit::WinitPlugin,
};
use bevy_wayland::prelude::*;
use smithay_client_toolkit::shell::wlr_layer::{Anchor, Layer};
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.build()
.disable::<WinitPlugin>()
.set(WindowPlugin {
primary_window: None,
..Default::default()
}),
WaylandPlugin,
))
.add_systems(Startup, setup)
.add_systems(
Update,
(
button_system,
exit_on_esc,
setup_session_lock_windows,
exit_on_all_closed,
),
)
.run();
}
#[allow(clippy::type_complexity)]
fn button_system(
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&Children,
),
(Changed<Interaction>, With<UnlockButton>),
>,
mut text_query: Query<&mut Text>,
mut session_lock_event_writer: EventWriter<SessionLockEvent>,
) {
for (interaction, mut color, mut border_color, children) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
session_lock_event_writer.write(SessionLockEvent::Unlock);
}
Interaction::Hovered => {
**text = "Click to unlock".to_string();
*color = HOVERED_BUTTON.into();
border_color.0 = Color::WHITE;
}
Interaction::None => {
**text = "Click to unlock".to_string();
*color = NORMAL_BUTTON.into();
border_color.0 = Color::BLACK;
}
}
}
}
#[derive(Component)]
struct LockButton;
#[derive(Component)]
struct UnlockButton;
fn setup(
mut commands: Commands,
assets: Res<AssetServer>,
windows: Query<Entity, With<Window>>,
mut session_lock_event_writer: EventWriter<SessionLockEvent>,
) {
session_lock_event_writer.write(SessionLockEvent::Lock);
for entity in &windows {
commands.entity(entity).insert((LayerShellSettings {
anchor: Anchor::TOP | Anchor::LEFT,
layer: Layer::Bottom,
..Default::default()
},));
}
// ui camera
commands.spawn(Camera2d);
commands.spawn(lock_button(&assets));
}
#[derive(Component)]
struct ConfiguredWindow;
#[derive(Component)]
struct SessionLockCamera;
fn setup_session_lock_windows(
mut commands: Commands,
asset_server: Res<AssetServer>,
windows: Query<(Entity, &SessionLockWindow), Without<ConfiguredWindow>>,
) {
for (entity, _) in &windows {
let camera = commands
.spawn((
Camera2d,
Camera {
target: bevy::render::camera::RenderTarget::Window(WindowRef::Entity(entity)),
..Default::default()
},
SessionLockCamera,
))
.id();
commands.entity(entity).insert(ConfiguredWindow);
commands.spawn(unlock_button(&asset_server, camera));
}
}
fn exit_on_esc(keys: Res<ButtonInput<KeyCode>>) {
if keys.just_pressed(KeyCode::Escape) {
std::process::exit(0);
}
}
fn lock_button(asset_server: &AssetServer) -> impl Bundle + use<> {
(
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Button,
LockButton,
Node {
width: Val::Px(250.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)],
)
}
fn unlock_button(asset_server: &AssetServer, camera: Entity) -> impl Bundle + use<> {
(
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
UiTargetCamera(camera),
children![(
Button,
UnlockButton,
Node {
width: Val::Px(250.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)],
)
}
@@ -6,8 +6,7 @@ use bevy::{
window::{WindowCreated, WindowResolution},
winit::WinitPlugin,
};
use bevy_wayland::{input_region::InputRegion, layer_shell::LayerShellSettings, WaylandPlugin};
use smithay_client_toolkit::shell::wlr_layer::{Anchor, Layer};
use bevy_wayland::prelude::*;
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
@@ -28,12 +27,8 @@ fn main() {
}),
WaylandPlugin,
))
.init_resource::<NewWindowInfo>()
.add_systems(Startup, setup)
.add_systems(
Update,
(button_system, spawn_window, setup_new_window, exit_on_esc),
)
.add_systems(Update, (button_system, exit_on_esc))
.run();
}
@@ -72,18 +67,13 @@ fn button_system(
}
}
#[derive(Component, Deref, DerefMut)]
struct WindowTimer(Timer);
fn setup(mut commands: Commands, assets: Res<AssetServer>, windows: Query<Entity, With<Window>>) {
for entity in &windows {
commands.entity(entity).insert((
LayerShellSettings {
commands.entity(entity).insert((LayerShellSettings {
anchor: Anchor::TOP | Anchor::LEFT,
layer: Layer::Bottom,
..Default::default()
},
InputRegion(Rect::new(0., 0., 200., 200.)),
));
},));
}
// ui camera
commands.spawn(Camera2d);
@@ -96,69 +86,6 @@ fn exit_on_esc(keys: Res<ButtonInput<KeyCode>>) {
}
}
fn spawn_window(
mut commands: Commands,
mut windows: Query<(Entity, &mut WindowTimer)>,
mut new_window_info: ResMut<NewWindowInfo>,
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
) {
if keys.pressed(KeyCode::KeyN) {
println!("Pressed");
let new_window_entity = commands
.spawn((
Window {
title: "UI Only Window".to_string(),
resolution: (400., 50.).into(),
..default()
},
LayerShellSettings {
layer: Layer::Top,
anchor: Anchor::TOP | Anchor::LEFT,
..default()
},
WindowTimer(Timer::new(Duration::from_secs(5), TimerMode::Once)),
))
.id();
new_window_info.entity = Some(new_window_entity);
new_window_info.is_setup_pending = true;
}
}
#[derive(Resource, Default)]
struct NewWindowInfo {
entity: Option<Entity>,
is_setup_pending: bool,
}
fn setup_new_window(
mut commands: Commands,
mut window_created_events: EventReader<WindowCreated>,
mut new_window_info: ResMut<NewWindowInfo>,
asset_server: Res<AssetServer>, // For fonts
) {
for event in window_created_events.read() {
if Some(event.window) == new_window_info.entity && new_window_info.is_setup_pending {
info!(
"New UI window created (ID: {:?}), setting up its camera and UI.",
event.window
);
commands.spawn((
Camera {
target: bevy::render::camera::RenderTarget::Window(
bevy::window::WindowRef::Entity(event.window),
),
clear_color: ClearColorConfig::Custom(Color::default()),
..default()
},
Camera2d,
));
new_window_info.is_setup_pending = false; // Mark as setup complete
}
}
}
fn button(asset_server: &AssetServer) -> impl Bundle + use<> {
(
Node {
+96
View File
@@ -0,0 +1,96 @@
use bevy::prelude::*;
use smithay_client_toolkit::{
reexports::client::{event_created_child, Dispatch, QueueHandle},
registry::RegistryState,
};
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1},
zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
};
use crate::WaylandState;
#[derive(Debug, Copy, Clone, Event)]
pub enum ForeignToplevelEvent {
MinimizeOthers,
}
#[derive(Default, Deref, DerefMut)]
struct ForeignToplevels(Vec<ZwlrForeignToplevelHandleV1>);
pub struct ForeignToplevelManagerPlugin;
impl Plugin for ForeignToplevelManagerPlugin {
fn build(&self, app: &mut App) {
let registry_state = app.world().non_send_resource::<RegistryState>();
let queue_handle = app.world().non_send_resource::<QueueHandle<WaylandState>>();
let foreign_top_level_manager =
registry_state.bind_one::<ZwlrForeignToplevelManagerV1, _, _>(queue_handle, 2..=3, ());
if let Ok(foreign_top_level_manager) = foreign_top_level_manager {
info!("Foreign toplevel manager was bound!");
app.insert_non_send_resource(foreign_top_level_manager);
app.insert_non_send_resource(ForeignToplevels::default());
app.add_event::<ForeignToplevelEvent>();
app.add_systems(Update, foreign_top_level_event_handler);
} else {
let bind_error = foreign_top_level_manager.err().unwrap();
error!("Couldn't bind foreign toplevel manager! {:?}", bind_error);
}
}
}
fn foreign_top_level_event_handler(
foreign_top_levels: NonSendMut<ForeignToplevels>,
mut events: EventReader<ForeignToplevelEvent>,
) {
for event in events.read() {
match event {
ForeignToplevelEvent::MinimizeOthers => {
info!("Minimizing other windows");
for toplevel in foreign_top_levels.iter() {
toplevel.set_minimized();
}
}
}
}
}
impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for WaylandState {
fn event(
state: &mut Self,
_proxy: &ZwlrForeignToplevelManagerV1,
event: <ZwlrForeignToplevelManagerV1 as smithay_client_toolkit::reexports::client::Proxy>::Event,
_data: &(),
_conn: &smithay_client_toolkit::reexports::client::Connection,
_qhandle: &QueueHandle<Self>,
) {
let mut foreign_toplevels = state
.world_mut()
.non_send_resource_mut::<ForeignToplevels>();
match event {
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } => {
foreign_toplevels.push(toplevel);
},
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::Event::Finished => {},
_ => {},
}
}
event_created_child!(WaylandState, ZwlrForeignToplevelManagerV1, [
// Opcode 0 is the `toplevel` event. It creates a new `zwlr_foreign_toplevel_handle_v1`.
zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
()
)
]);
}
impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for WaylandState {
fn event(
_state: &mut Self,
_proxy: &ZwlrForeignToplevelHandleV1,
_event: <ZwlrForeignToplevelHandleV1 as smithay_client_toolkit::reexports::client::Proxy>::Event,
_data: &(),
_conn: &smithay_client_toolkit::reexports::client::Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
+3 -6
View File
@@ -1,10 +1,7 @@
use bevy::{core_pipeline::core_2d::graph::input, prelude::*};
use smithay_client_toolkit::{
compositor::{CompositorState, Region},
reexports::client::{protocol::wl_compositor::WlCompositor, QueueHandle},
};
use bevy::prelude::*;
use smithay_client_toolkit::compositor::{CompositorState, Region};
use crate::{input_region, surface_handler::WaylandSurfaces, WaylandState};
use crate::surface_handler::WaylandSurfaces;
#[derive(Component, Deref)]
pub struct InputRegion(pub Rect);
+32 -9
View File
@@ -1,7 +1,7 @@
use bevy::{platform::collections::HashMap, prelude::*, ui::update};
use bevy::{platform::collections::HashMap, prelude::*};
use smithay_client_toolkit::{
delegate_layer,
reexports::client::{globals::GlobalList, Connection, Proxy, QueueHandle},
reexports::client::{globals::GlobalList, QueueHandle},
shell::{
wlr_layer::{
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
@@ -21,12 +21,18 @@ struct LayerShellWindows(HashMap<Entity, LayerShellWindow>);
struct LayerShellWindow {
layer_surface: LayerSurface,
layer_shell_settings: LayerShellSettings,
window_size: (u32, u32),
}
impl LayerShellWindow {
fn new(layer_surface: LayerSurface, layer_shell_settings: LayerShellSettings) -> Self {
fn new(
layer_surface: LayerSurface,
layer_shell_settings: LayerShellSettings,
window_size: (u32, u32),
) -> Self {
let mut layer_shell_window = Self {
layer_surface,
layer_shell_settings,
window_size,
};
layer_shell_window.sync();
layer_shell_window
@@ -42,8 +48,12 @@ impl LayerShellWindow {
self.layer_surface
.set_exclusive_zone(self.layer_shell_settings.exclusive_zone);
let (width, height) = self.layer_shell_settings.size;
self.layer_surface.set_size(400, 400);
if let LayerShellWindowSize::Fixed(width, height) = self.layer_shell_settings.size {
self.layer_surface.set_size(width, height);
} else {
self.layer_surface
.set_size(self.window_size.0, self.window_size.1);
}
let (top, right, bottom, left) = self.layer_shell_settings.margin;
self.layer_surface.set_margin(top, right, bottom, left);
@@ -59,6 +69,13 @@ impl LayerShellWindow {
}
}
#[derive(Default, Eq, PartialEq, Clone, Debug)]
pub enum LayerShellWindowSize {
#[default]
Inherit,
Fixed(u32, u32),
}
#[derive(Component, Debug, Clone, PartialEq, Eq)]
pub struct LayerShellSettings {
/// Defines where the layer surface should be anchored to the screen.
@@ -66,7 +83,7 @@ pub struct LayerShellSettings {
/// You can anchor the layer surface to any combination of the top, bottom, left, and right edges of the screen.
pub anchor: Anchor,
/// Defines the size of the layer surface in pixels.
pub size: (u32, u32),
pub size: LayerShellWindowSize,
/// Defines the amount of exclusive space the layer surface should reserve.
///
/// Other surfaces will not be placed in this area. A negative value means that the layer surface
@@ -118,7 +135,7 @@ fn assign_layer_shell_role(
windows: Query<(Entity, &Window, &LayerShellSettings), Without<SurfaceConfigured>>,
mut layer_shell_windows: NonSendMut<LayerShellWindows>,
) {
for (entity, _window, layer_shell_settings) in &windows {
for (entity, window, layer_shell_settings) in &windows {
let window_wrapper = wayland_surfaces.get_window_wrapper(entity);
let surface = window_wrapper
.expect("tried to assign role before creating surface!")
@@ -136,7 +153,11 @@ fn assign_layer_shell_role(
let _ = layer_shell_windows.insert(
entity,
LayerShellWindow::new(layer, layer_shell_settings.clone()),
LayerShellWindow::new(
layer,
layer_shell_settings.clone(),
(window.width() as u32, window.height() as u32),
),
);
commands.entity(entity).insert(SurfaceConfigured);
@@ -147,8 +168,10 @@ fn update_layer_shell_settings(
mut layer_shell_windows: NonSendMut<LayerShellWindows>,
windows: Query<(Entity, &Window, &LayerShellSettings), Without<SurfaceConfigured>>,
) {
for (entity, _window, layer_shell_settings) in &windows {
for (entity, window, layer_shell_settings) in &windows {
let layer_shell_window = layer_shell_windows.get_mut(&entity).unwrap();
let window_size = (window.width() as u32, window.height() as u32);
layer_shell_window.window_size = window_size;
layer_shell_window.set_settings(layer_shell_settings.clone());
}
}
+54 -9
View File
@@ -1,9 +1,14 @@
use std::{
sync::mpsc::SendError,
time::{Duration, Instant},
};
use bevy::{app::PluginsState, prelude::*};
use smithay_client_toolkit::{
delegate_registry,
output::OutputState,
reexports::{
calloop::EventLoop,
calloop::{self, channel::Sender, EventLoop},
calloop_wayland_source::WaylandSource,
client::{globals::registry_queue_init, Connection},
},
@@ -12,29 +17,62 @@ use smithay_client_toolkit::{
seat::SeatState,
};
pub mod foreign_toplevel_manager;
mod input_handler;
pub mod input_region;
pub mod layer_shell;
mod output_handler;
pub mod session_lock;
mod surface_handler;
pub mod prelude {
pub use crate::input_region::InputRegion;
pub use crate::layer_shell::{LayerShellSettings, LayerShellWindowSize};
pub use crate::session_lock::{SessionLockEvent, SessionLockWindow};
pub use crate::WaylandPlugin;
pub use smithay_client_toolkit::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer};
}
pub struct Tick;
#[derive(Resource, Clone)]
pub struct ExternalEventDispatcher(Sender<Tick>);
impl ExternalEventDispatcher {
fn new(tx: Sender<Tick>) -> Self {
Self(tx)
}
pub fn dispatch(&self) -> Result<(), SendError<Tick>> {
self.0.send(Tick)
}
}
#[derive(Default)]
pub struct WaylandPlugin;
impl Plugin for WaylandPlugin {
fn build(&self, app: &mut App) {
let connection =
Connection::connect_to_env().expect("failed to connect to wayland socket!");
Connection::connect_to_env().expect("Failed to connect to wayland socket!");
let event_loop =
EventLoop::<WaylandState>::try_new().expect("failed to create event_loop!");
EventLoop::<WaylandState>::try_new().expect("Failed to create event_loop!");
let (globals, event_queue) = registry_queue_init::<WaylandState>(&connection)
.expect("failed to init registry queue");
.expect("Failed to init registry queue");
let qh = event_queue.handle();
let loop_handle = event_loop.handle();
WaylandSource::new(connection.clone(), event_queue)
.insert(loop_handle.clone())
.expect("failed to insert wayland source to event loop");
.expect("Failed to insert wayland source to event loop");
let (tx, rx) = calloop::channel::channel::<Tick>();
loop_handle
.insert_source(rx, |_, _, state| {
info!("External event was received!");
if state.plugins_state() == PluginsState::Cleaned {
state.update();
}
})
.expect("Failed to insert external tick channel!");
app.insert_resource(ExternalEventDispatcher::new(tx));
app.insert_non_send_resource(RegistryState::new(&globals));
app.insert_non_send_resource(connection.clone());
app.insert_non_send_resource(globals);
@@ -42,10 +80,12 @@ impl Plugin for WaylandPlugin {
app.add_plugins((
output_handler::OutputHandlerPlugin,
input_handler::InputHandlerPlugin,
surface_handler::SurfaceHandlerPlugin,
input_handler::InputHandlerPlugin,
layer_shell::LayerShellPlugin,
session_lock::SessionLockPlugin,
input_region::InputRegionPlugin,
foreign_toplevel_manager::ForeignToplevelManagerPlugin,
));
app.set_runner(|app| runner(app, event_loop));
}
@@ -56,14 +96,19 @@ pub fn runner(mut app: App, mut event_loop: EventLoop<'_, WaylandState>) -> AppE
app.finish();
app.cleanup();
}
let mut state = WaylandState(app);
loop {
// TODO: Error handling
let frame_start = Instant::now();
let _ = event_loop.dispatch(Duration::from_millis(5000), &mut state);
if state.plugins_state() == PluginsState::Cleaned {
state.update();
}
let _ = event_loop.dispatch(None, &mut state);
let _ = event_loop.dispatch(Duration::from_millis(0), &mut state);
// TODO: Poll until delta time is greater than target frame time.
if Instant::now() - frame_start < Duration::from_millis(16) {
std::thread::sleep(Duration::from_millis(16) - (frame_start - Instant::now()));
}
let _ = event_loop.dispatch(Duration::from_millis(0), &mut state);
}
}
+156
View File
@@ -0,0 +1,156 @@
use bevy::{platform::collections::HashMap, prelude::*};
use smithay_client_toolkit::{
delegate_session_lock,
output::OutputState,
reexports::client::{globals::GlobalList, protocol::wl_output::WlOutput, QueueHandle},
session_lock::{SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface},
};
use crate::{
surface_handler::{create_windows, SurfaceConfigured, WaylandSurfaces},
WaylandState,
};
#[derive(Default, Deref, DerefMut)]
struct SessionLockWindows(HashMap<Entity, SessionLockWindowInternal>);
struct SessionLockWindowInternal {
_session_lock_surface: SessionLockSurface,
}
#[derive(Component)]
pub struct SessionLockWindow;
#[derive(Component)]
struct SessionLockUnconfiguredWindow {
output: WlOutput,
}
impl SessionLockUnconfiguredWindow {
pub fn new(output: WlOutput) -> Self {
Self { output }
}
}
#[derive(Clone, Copy, Event)]
pub enum SessionLockEvent {
Lock,
Unlock,
}
pub struct SessionLockPlugin;
impl Plugin for SessionLockPlugin {
fn build(&self, app: &mut App) {
let globals = app.world().non_send_resource::<GlobalList>();
let queue_handle = app.world().non_send_resource::<QueueHandle<WaylandState>>();
let session_lock_state = SessionLockState::new(globals, queue_handle);
app.insert_non_send_resource(session_lock_state);
app.insert_non_send_resource(SessionLockWindows::default());
app.insert_non_send_resource(SessionLockWrapper::default());
app.add_event::<SessionLockEvent>();
app.add_systems(
PreUpdate,
(
session_lock_event_handler.before(create_windows),
configure_lock_surfaces.after(create_windows),
),
);
}
}
#[derive(Deref, DerefMut, Default)]
struct SessionLockWrapper(Option<SessionLock>);
fn session_lock_event_handler(
mut commands: Commands,
mut session_lock_event_reader: EventReader<SessionLockEvent>,
session_lock_state: NonSend<SessionLockState>,
mut session_lock_wrapper: NonSendMut<SessionLockWrapper>,
queue_handle: NonSend<QueueHandle<WaylandState>>,
output_state: NonSend<OutputState>,
) {
for session_lock_event in session_lock_event_reader.read() {
match session_lock_event {
SessionLockEvent::Lock => {
if session_lock_wrapper.is_some() {
error!("Lock was called even if it was already aquired");
return;
}
let session_lock = session_lock_state
.lock(&queue_handle)
.expect("Unable to aquire session lock");
let _ = session_lock_wrapper.insert(session_lock);
for output in output_state.outputs() {
commands.spawn((
Window::default(),
SessionLockUnconfiguredWindow::new(output),
));
}
}
SessionLockEvent::Unlock => {
if let Some(session_lock) = &**session_lock_wrapper {
session_lock.unlock();
}
}
}
}
}
fn configure_lock_surfaces(
mut commands: Commands,
mut session_lock_windows: NonSendMut<SessionLockWindows>,
session_lock_wrapper: NonSend<SessionLockWrapper>,
wayland_surfaces: NonSend<WaylandSurfaces>,
qh: NonSend<QueueHandle<WaylandState>>,
unconfigured_windows: Query<(Entity, &SessionLockUnconfiguredWindow)>,
) {
if let Some(session_lock) = &**session_lock_wrapper {
for (entity, unconfigured_window) in &unconfigured_windows {
let window_wrapper = wayland_surfaces.get_window_wrapper(entity);
let surface = window_wrapper
.expect("tried to assign role before creating surface!")
.wl_surface();
let _session_lock_surface =
session_lock.create_lock_surface(surface.clone(), &unconfigured_window.output, &qh);
let session_lock_window = SessionLockWindowInternal {
_session_lock_surface,
};
session_lock_windows.insert(entity, session_lock_window);
commands
.entity(entity)
.insert(SurfaceConfigured)
.insert(SessionLockWindow)
.remove::<SessionLockUnconfiguredWindow>();
}
}
}
impl SessionLockHandler for WaylandState {
fn locked(
&mut self,
_conn: &smithay_client_toolkit::reexports::client::Connection,
_qh: &smithay_client_toolkit::reexports::client::QueueHandle<Self>,
_session_lock: smithay_client_toolkit::session_lock::SessionLock,
) {
}
fn finished(
&mut self,
_conn: &smithay_client_toolkit::reexports::client::Connection,
_qh: &smithay_client_toolkit::reexports::client::QueueHandle<Self>,
_session_lock: smithay_client_toolkit::session_lock::SessionLock,
) {
}
fn configure(
&mut self,
_conn: &smithay_client_toolkit::reexports::client::Connection,
_qh: &smithay_client_toolkit::reexports::client::QueueHandle<Self>,
_surface: smithay_client_toolkit::session_lock::SessionLockSurface,
_configure: smithay_client_toolkit::session_lock::SessionLockSurfaceConfigure,
_serial: u32,
) {
}
}
delegate_session_lock!(WaylandState);