From 6fd934872a3c9f868bed5bd2f5ee33f0cb748912 Mon Sep 17 00:00:00 2001 From: tcmal Date: Tue, 13 Aug 2024 22:27:16 +0100 Subject: Move to one list of clients, shared across monitors --- src/clients/mod.rs | 367 ++++++++++++++++++++++++------------------------ src/clients/monitors.rs | 22 +-- src/clients/tile.rs | 10 +- src/focus.rs | 6 +- src/helpers.rs | 6 +- src/main.rs | 6 +- 6 files changed, 212 insertions(+), 205 deletions(-) diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 29219b6..7ea6f48 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -38,6 +38,9 @@ mod tile; /// The tag a client has, similar to a workspace in most WMs. pub type Tag = u8; +pub type MonitorIdx = usize; +pub type ClientIdx = usize; + impl WM<'_> { /// Perform configure requests if we're happy with them, or they're for an unmanaged window. pub(crate) fn handle_configure_request(&mut self, e: &ConfigureRequestEvent) -> Result<()> { @@ -56,9 +59,6 @@ impl WM<'_> { ); } - // TODO: Allow changing some properties: - // - Border width - // - Size and position if floating c.configure_notify(&self.conn); } else { // Configure it as requested, and sort the rest when we actually map the window @@ -128,71 +128,27 @@ impl WM<'_> { pub struct ClientState { /// The current arranging function. /// This function is expected to ensure that all clients are the correct size, reconfigure them if needed, and map/unmap as needed. - arrange: &'static dyn Fn(&mut MonitorInfo, &Connection<'_>), + arrange: &'static dyn Fn(&Connection<'_>, &mut [Client], &MonitorInfo), + + /// Clients attached to that monitor + pub clients: Vec, - /// A client list for each monitor. + /// Information for each monitor. mons: Vec, - /// Co-ordinates to the currently focused window. - focused: (usize, usize), + /// Index of the currently focused client + focused_client: Option, } impl ClientState { - /// Update the recorded monitors and monitor sizes, retiling if necessary. - pub fn update_geometry(&mut self, conn: &Connection<'_>) -> Result<()> { - let mut dirty = false; - if conn.active_extensions().any(|e| e == Extension::Xinerama) { - let reply = conn.wait_for_reply(conn.send_request(&xinerama::QueryScreens {}))?; - - // Monitor removed, move its clients away - if reply.screen_info().len() > self.monitor_count() { - dirty = true; - self.truncate_screens(reply.screen_info().len()); - } - - // Update screen info & add new client lists if needed - for (i, monitor) in reply.screen_info().iter().enumerate() { - dirty |= self.set_monitor_geometry(i, (*monitor).into()); - } - } else { - // Only one screen - if self.monitor_count() > 1 { - dirty = true; - self.truncate_screens(1); - } - - // TODO: it looks like this won't actually update when the screen size changes? - let setup = conn.get_setup(); - let screen = setup - .roots() - .nth(conn.screen_num()) - .ok_or(Error::NoSuchScreen)?; - - dirty |= self.set_monitor_geometry( - 0, - MonitorGeometry { - x_org: 0, - y_org: 0, - width: screen.width_in_pixels(), - height: screen.height_in_pixels(), - }, - ); - } - - if dirty { - self.rearrange(conn); - } - Ok(()) - } - /// Start managing the given window, adding it to the client list and ensuring its configuration is valid. pub fn manage(&mut self, conn: &Connection<'_>, window: Window) { - let mut mon = self.focused_mon(); + let mut tag = self.focused_mon().focused_tag.create_tag(); let mut floating = false; if let Some(parent) = hints::transient_for(conn, window) { floating = true; - if let Some((parent_mon, _)) = self.find_client_pos(parent) { - mon = parent_mon; + if let Some(c) = self.find_client_mut(parent) { + tag = c.tag; } } @@ -202,19 +158,15 @@ impl ClientState { return; // window stopped existing, so we can't manage it }; - // We're about to invalidate focus position - self.unfocus(conn); - // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved - let tag = self.mons[mon].focused_tag.create_tag(); - self.mons[mon].clients.insert(0, Client::new(window, tag)); + self.clients.push(Client::new(window, tag)); let mon_geom @ MonitorGeometry { width: mon_width, height: mon_height, .. - } = self.mons[mon].screen_info; + } = self.focused_mon().screen_info; - let c = &mut self.mons[mon].clients[0]; + let c = &mut self.clients[0]; #[allow(clippy::cast_sign_loss)] c.set_geom( conn, @@ -253,25 +205,35 @@ impl ClientState { }); c.set_withdrawn(conn, false); - self.refocus(conn, mon, 0); - self.rearrange_monitor(conn, mon); + self.refocus(conn, self.clients.len() - 1); + self.rearrange(conn); } /// Stop managing the given destroyed window pub fn unmanage_destroyed(&mut self, conn: &Connection<'_>, window: Window) { - let Some((mon, i)) = self.find_client_pos(window) else { + let Some(i) = self.find_client_pos(window) else { return; }; - self.mons[mon].clients.remove(i); + let focused_mon = self.focused_mon().screen_info; + + self.clients.remove(i); + + self.focused_client = self.focused_client.filter(|f| *f < self.clients.len()); + if self.focused_client.is_none() { + self.focused_client = self + .clients + .iter() + .position(|c| focused_mon.contains(c.x(), c.y())); + } self.rearrange(conn); } /// Refocus on the client with the given co-ordinates, setting X11 properties as required. /// If the given index is invalid, focus on the root instead. - pub fn refocus(&mut self, conn: &Connection<'_>, mon: usize, i: usize) { + pub fn refocus(&mut self, conn: &Connection<'_>, i: ClientIdx) { self.unfocus(conn); - if let Some(new) = self.set_focused(mon, i) { + if let Some(new) = self.set_focused(i) { new.set_border(conn, conn.colours.border_focused()); new.sync_hints(conn, true); buttons::grab(conn, new.window(), true); @@ -290,6 +252,7 @@ impl ClientState { }); conn.send_event(new.window(), conn.atoms.wm_take_focus); } + // TODO: Update focused tag } else { conn.send_request(&SetInputFocus { revert_to: InputFocus::PointerRoot, @@ -313,92 +276,147 @@ impl ClientState { /// Go to the next or previous window in the current monitor, looping around if needed pub fn change_focus(&mut self, conn: &Connection<'_>, increase: bool) { - if self.focused.0 >= self.mons.len() { + if self.clients.is_empty() { return; } - let mon = self.focused.0; - if self.mons[mon].clients.is_empty() { - return; - } + let curr_focused = self.focused_client.unwrap_or(0); + let look_through = self + .clients + .iter() + .enumerate() + .filter(|(_, c)| self.focused_mon().screen_info.contains(c.x(), c.y())) + .map(|(i, _)| i); + + let new_idx = if increase { + look_through + .cycle() + .skip_while(|i| *i != curr_focused) + .nth(1) + .unwrap_or(0) + } else { + look_through + .rev() + .cycle() + .skip_while(|i| *i != curr_focused) + .nth(1) + .unwrap_or(0) + }; - self.refocus( - conn, - mon, - if increase { - (self.focused.1 + 1) % self.mons[mon].clients.len() - } else if self.focused.1 > 0 { - self.focused.1 - 1 - } else { - self.mons[mon].clients.len() - 1 - }, - ); + self.refocus(conn, new_idx); } /// Toggle whether the client with the given position is floating - pub fn toggle_floating(&mut self, conn: &Connection<'_>, (mon, pos): (usize, usize)) { - let c = &mut self.mons[mon].clients[pos]; + pub fn toggle_floating(&mut self, conn: &Connection<'_>, pos: ClientIdx) { + let c = &mut self.clients[pos]; if c.tiled() { c.set_floating(conn); } else { c.set_tiled(conn); } - self.rearrange_monitor(conn, mon); + self.rearrange_mon(conn, self.client_mon_idx(pos)); } /// Toggle whether the client with the given position is fullscreen - pub fn toggle_fullscreen(&mut self, conn: &Connection<'_>, (mon, pos): (usize, usize)) { - let mon_info = &mut self.mons[mon]; - let c = &mut mon_info.clients[pos]; + pub fn toggle_fullscreen(&mut self, conn: &Connection<'_>, pos: ClientIdx) { + let mon = self.client_mon_idx(pos); + let mon_info = &self.mons[mon]; + let c = &mut self.clients[pos]; if c.tiled() { c.set_fullscreen(conn, &mon_info.screen_info); } else { c.set_tiled(conn); } - self.rearrange_monitor(conn, mon); + self.rearrange_mon(conn, mon); } /// Set the focused tag for the given monitor - pub fn set_mon_tag_focus(&mut self, conn: &Connection<'_>, mon: usize, tag_focus: TagFocus) { + pub fn set_mon_tag_focus( + &mut self, + conn: &Connection<'_>, + mon: MonitorIdx, + tag_focus: TagFocus, + ) { // Hide windows from currently focused tag filter - let curr_focus = self.mons[mon].focused_tag; - self.mons[mon] - .clients + let mon_info = &self.mons[mon]; + let curr_focus = mon_info.focused_tag; + self.clients .iter_mut() .filter(|c| curr_focus.matches(c.tag)) + .filter(|c| mon_info.screen_info.contains(c.x(), c.y())) .for_each(|c| c.ensure_unmapped(conn)); debug!("setting tag focus to {:?} on mon {}", tag_focus, mon); self.mons[mon].focused_tag = tag_focus; self.unfocus(conn); - self.rearrange_monitor(conn, mon); + self.rearrange_mon(conn, mon); } /// Set the tag for the given client - pub fn set_client_tag(&mut self, conn: &Connection<'_>, (mon, pos): (usize, usize), tag: Tag) { - if let Some(c) = self.client_mut(mon, pos) { - if c.tag == tag { - return; + pub fn set_client_tag(&mut self, conn: &Connection<'_>, pos: ClientIdx, tag: Tag) { + let c = self.client_mut(pos); + if c.tag == tag { + return; + } + + debug!("moving client with window {:?} to tag {}", c.window(), tag); + c.tag = tag; + c.ensure_unmapped(conn); + self.unfocus(conn); + self.rearrange_mon(conn, self.client_mon_idx(pos)); + } + + /// Update the recorded monitors and monitor sizes, retiling if necessary. + pub fn update_geometry(&mut self, conn: &Connection<'_>) -> Result<()> { + let mut dirty = false; + if conn.active_extensions().any(|e| e == Extension::Xinerama) { + let reply = conn.wait_for_reply(conn.send_request(&xinerama::QueryScreens {}))?; + + // Monitor removed, move its clients away + if reply.screen_info().len() > self.mons.len() { + dirty = true; + self.truncate_screens(reply.screen_info().len()); + } + + // Update screen info & add new client lists if needed + for (i, monitor) in reply.screen_info().iter().enumerate() { + dirty |= self.set_monitor_geometry(i, (*monitor).into()); } + } else { + // Only one screen + if self.mons.len() > 1 { + dirty = true; + self.truncate_screens(1); + } + + // TODO: it looks like this won't actually update when the screen size changes? + let setup = conn.get_setup(); + let screen = setup + .roots() + .nth(conn.screen_num()) + .ok_or(Error::NoSuchScreen)?; - debug!("moving client with window {:?} to tag {}", c.window(), tag); - c.tag = tag; - c.ensure_unmapped(conn); - self.unfocus(conn); - self.rearrange_monitor(conn, mon); + dirty |= self.set_monitor_geometry( + 0, + MonitorGeometry { + x_org: 0, + y_org: 0, + width: screen.width_in_pixels(), + height: screen.height_in_pixels(), + }, + ); } - } - /// Get the amount of monitors this state is currently aware of - pub fn monitor_count(&self) -> usize { - self.mons.len() + if dirty { + self.rearrange(conn); + } + Ok(()) } - /// Set the new amount of screens, without unmanaging any clients. + /// Set the new amount of screens fn truncate_screens(&mut self, new_size: usize) { - // HACK: working around double borrow stuff match new_size.cmp(&self.mons.len()) { Ordering::Greater => { for _ in 0..new_size - self.mons.len() { @@ -406,11 +424,7 @@ impl ClientState { } } Ordering::Less => { - let mut moved_clients = vec![]; - for old in self.mons.drain(new_size - self.mons.len()..self.mons.len()) { - moved_clients.extend(old.clients.into_iter()); - } - self.mons[0].clients.extend(moved_clients); + self.mons.drain(new_size - self.mons.len()..self.mons.len()); } Ordering::Equal => (), } @@ -418,7 +432,7 @@ impl ClientState { /// Set the given screen's geometry, resizing the monitor list if necessary. /// Returns true if the new info is different from the old one. - fn set_monitor_geometry(&mut self, i: usize, info: MonitorGeometry) -> bool { + fn set_monitor_geometry(&mut self, i: MonitorIdx, info: MonitorGeometry) -> bool { while i >= self.mons.len() { self.mons.push(MonitorInfo::default()); } @@ -430,39 +444,28 @@ impl ClientState { /// Find the [`Client`] corresponding to the given window pub fn find_client_mut(&mut self, window: Window) -> Option<&mut Client> { - let (mon, i) = self.find_client_pos(window)?; - Some(&mut self.mons[mon].clients[i]) + let i = self.find_client_pos(window)?; + Some(&mut self.clients[i]) } - /// Find the position of the client with the given window, returning (monitor, index) - pub fn find_client_pos(&mut self, window: Window) -> Option<(usize, usize)> { - for (pos_mon, mon) in self.mons.iter_mut().enumerate() { - if let Some(pos) = mon.clients.iter().position(|c| c.window() == window) { - return Some((pos_mon, pos)); - } - } + /// Find the position of the client with the given window, returning an index + pub fn find_client_pos(&self, window: Window) -> Option { + self.clients.iter().position(|c| c.window() == window) + } - None + /// Get the index of the currently focused client, if it exists + pub const fn focused_pos(&self) -> Option { + self.focused_client } /// Get a reference to the currently focused client, if it exists. pub fn focused(&self) -> Option<&Client> { - self.client(self.focused.0, self.focused.1) - } - - /// Get the position of the currently focused client, if it exists - pub fn focused_pos(&self) -> Option<(usize, usize)> { - let (mon, pos) = self.focused; - if mon < self.mons.len() && pos < self.mons[mon].clients.len() { - Some((mon, pos)) - } else { - None - } + self.focused_client.map(|i| self.client(i)) } /// Get a mutable reference to the currently focused client, if it exists. pub fn focused_mut(&mut self) -> Option<&mut Client> { - self.client_mut(self.focused.0, self.focused.1) + self.focused_client.map(|i| self.client_mut(i)) } /// Get the position of the currently focused client. This position may be invalid. @@ -470,66 +473,64 @@ impl ClientState { self.focused().is_some_and(|c| c.window() == e) } - /// Set the currently focused client, returning a mutable reference to it if the co-ordinates are valid. - pub fn set_focused(&mut self, mut mon: usize, mut i: usize) -> Option<&mut Client> { - if self.mons.is_empty() { - return None; - } - - if mon >= self.mons.len() { - mon = self.mons.len() - 1; - } - - if self.mons[mon].clients.is_empty() { + /// Set the currently focused client, returning a mutable reference to the client. + pub fn set_focused(&mut self, mut i: ClientIdx) -> Option<&mut Client> { + if self.clients.is_empty() { return None; } - if i >= self.mons[mon].clients.len() { - i = self.mons[mon].clients.len() - 1; + if i >= self.clients.len() { + i = self.clients.len() - 1; } - self.focused = (mon, i); - Some(&mut self.mons[mon].clients[i]) + self.focused_client = Some(i); + Some(&mut self.clients[i]) } - /// Get a mutable reference to the client at the given co-ordinates, if they are valid. - pub fn client(&self, mon: usize, i: usize) -> Option<&Client> { - if mon < self.mons.len() && i < self.mons[mon].clients.len() { - Some(&self.mons[mon].clients[i]) - } else { - None - } + /// Get a reference to the client at the given index + pub fn client(&self, i: ClientIdx) -> &Client { + &self.clients[i] } - /// Get a mutable reference to the client at the given co-ordinates, if they are valid. - pub fn client_mut(&mut self, mon: usize, i: usize) -> Option<&mut Client> { - if mon < self.mons.len() && i < self.mons[mon].clients.len() { - Some(&mut self.mons[mon].clients[i]) - } else { - None - } + /// Get a mutable reference to the client at the given index + pub fn client_mut(&mut self, i: ClientIdx) -> &mut Client { + &mut self.clients[i] } /// Rearrange all clients, reconfiguring them as needed. pub fn rearrange(&mut self, conn: &Connection<'_>) { - for mon in 0..self.monitor_count() { - self.rearrange_monitor(conn, mon); + for mon in 0..self.mons.len() { + self.rearrange_mon(conn, mon); } } /// Rearrange a specific monitor - fn rearrange_monitor(&mut self, conn: &Connection<'_>, mon: usize) { - (self.arrange)(&mut self.mons[mon], conn); + pub fn rearrange_mon(&mut self, conn: &Connection<'_>, mon: MonitorIdx) { + (self.arrange)(conn, &mut self.clients, &self.mons[mon]); + } + + /// Get the index of the currently focused monitor + pub fn focused_mon_idx(&self) -> MonitorIdx { + self.focused_client.map_or(0, |i| self.client_mon_idx(i)) + } + + /// Get the info for the currently focused monitor + pub fn focused_mon(&self) -> &MonitorInfo { + &self.mons[self.focused_mon_idx()] } - /// Get the currently focused monitor - pub const fn focused_mon(&self) -> usize { - self.focused.0 + /// Get the monitor info for the client with the given index + pub fn client_mon_idx(&self, pos: ClientIdx) -> MonitorIdx { + let c = self.client(pos); + self.mons + .iter() + .position(|m| m.screen_info.contains(c.x(), c.y())) + .unwrap_or_else(|| self.mons.len() - 1) } - /// Get the geometry of the monitor with the given index - pub fn mon_geometry(&self, mon: usize) -> MonitorGeometry { - self.mons[mon].screen_info + /// Get the monitor info for the client with the given index + pub fn client_mon(&self, pos: ClientIdx) -> &MonitorInfo { + &self.mons[self.client_mon_idx(pos)] } } @@ -537,8 +538,9 @@ impl Default for ClientState { fn default() -> Self { Self { arrange: &tile::tile, - focused: (0, 0), mons: vec![], + clients: vec![], + focused_client: None, } } } @@ -546,8 +548,9 @@ impl Default for ClientState { impl std::fmt::Debug for ClientState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientState") - .field("focused", &self.focused) + .field("focused_client", &self.focused_client) .field("mons", &self.mons) + .field("clients", &self.clients) .finish_non_exhaustive() } } diff --git a/src/clients/monitors.rs b/src/clients/monitors.rs index 16348ea..c828fbd 100644 --- a/src/clients/monitors.rs +++ b/src/clients/monitors.rs @@ -5,9 +5,6 @@ use super::{Client, Tag}; /// Info stored for each monitor #[derive(Debug)] pub struct MonitorInfo { - /// Clients attached to that monitor - pub clients: Vec, - /// How clients should be filtered by tag pub focused_tag: TagFocus, @@ -43,19 +40,19 @@ impl TagFocus { } impl MonitorInfo { - /// Iterate over all tiled clients, returning a mutable reference to each. - pub fn clients_tiled_mut(&mut self) -> impl Iterator { - self.clients - .iter_mut() - .filter(|c| c.tiled()) + pub fn iter_visible_tiling<'a>( + &'a self, + i: impl IntoIterator, + ) -> impl Iterator { + i.into_iter() .filter(|c| self.focused_tag.matches(c.tag)) + .filter(|c| c.tiled()) } } impl Default for MonitorInfo { fn default() -> Self { Self { - clients: vec![], screen_info: MonitorGeometry { x_org: 0, y_org: 0, @@ -75,6 +72,13 @@ pub struct MonitorGeometry { pub width: u16, pub height: u16, } +impl MonitorGeometry { + #[allow(clippy::cast_possible_wrap)] + pub(crate) fn contains(self, x: i16, y: i16) -> bool { + (self.x_org..self.x_org + self.width as i16).contains(&x) + && (self.y_org..self.y_org + self.height as i16).contains(&y) + } +} impl From for MonitorGeometry { fn from(value: ScreenInfo) -> Self { diff --git a/src/clients/tile.rs b/src/clients/tile.rs index e089c3f..f1f00a7 100644 --- a/src/clients/tile.rs +++ b/src/clients/tile.rs @@ -4,7 +4,7 @@ use std::cmp::min; use crate::conn_info::Connection; -use super::{MonitorGeometry, MonitorInfo}; +use super::{Client, MonitorGeometry, MonitorInfo}; /// A simple tiling function #[allow( @@ -13,12 +13,12 @@ use super::{MonitorGeometry, MonitorInfo}; clippy::cast_possible_truncation, clippy::cast_lossless )] -pub fn tile(mon: &mut MonitorInfo, conn: &Connection<'_>) { - if mon.clients.is_empty() { +pub fn tile(conn: &Connection<'_>, clients: &mut [Client], mon: &MonitorInfo) { + if clients.is_empty() { return; } - let n = mon.clients_tiled_mut().count(); + let n = mon.iter_visible_tiling(&mut *clients).count(); let nmaster = 1; let mfact = 0.6; @@ -38,7 +38,7 @@ pub fn tile(mon: &mut MonitorInfo, conn: &Connection<'_>) { }; let (mut main_y, mut second_y) = (0, 0); - for (i, c) in mon.clients_tiled_mut().enumerate() { + for (i, c) in mon.iter_visible_tiling(clients).enumerate() { if i < nmaster { let h = (mon_height - main_y) / (min(nmaster, n) - i) as u16; c.set_geom( diff --git a/src/focus.rs b/src/focus.rs index 7a4b348..2415a91 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -13,8 +13,8 @@ impl WM<'_> { return; } - if let Some((mon, pos)) = self.clients.find_client_pos(e.event()) { - self.clients.refocus(&self.conn, mon, pos); + if let Some(pos) = self.clients.find_client_pos(e.event()) { + self.clients.refocus(&self.conn, pos); } } @@ -22,7 +22,7 @@ impl WM<'_> { pub fn handle_focus_in(&mut self, e: &FocusInEvent) { if !self.clients.is_focused(e.event()) { if let Some(pos) = self.clients.focused_pos() { - self.clients.refocus(&self.conn, pos.0, pos.1); + self.clients.refocus(&self.conn, pos); } } } diff --git a/src/helpers.rs b/src/helpers.rs index 175eea9..e67aecc 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -73,7 +73,7 @@ pub fn toggle_fullscreen(wm: &mut WM<'_>) { /// Set the focused tag for the currently selected monitor pub fn set_tag_focus(wm: &mut WM<'_>, tag_focus: TagFocus) { wm.clients - .set_mon_tag_focus(&wm.conn, wm.clients.focused_mon(), tag_focus); + .set_mon_tag_focus(&wm.conn, wm.clients.focused_mon_idx(), tag_focus); } /// Set the tag for the currently selected client @@ -86,13 +86,13 @@ pub fn set_tag(wm: &mut WM<'_>, tag: Tag) { /// Move the currently focused window with the mouse pub fn mouse_move(wm: &mut WM<'_>) { if let Err(e) = wm.mouse_move() { - println!("error when moving with mouse: {:?}", e); + println!("error when moving with mouse: {e:?}"); } } /// Resize the currently focused window with the mouse pub fn mouse_resize(wm: &mut WM<'_>) { if let Err(e) = wm.mouse_resize() { - println!("error when resizing with mouse: {:?}", e); + println!("error when resizing with mouse: {e:?}"); } } diff --git a/src/main.rs b/src/main.rs index 5b15b9a..ec2475c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,7 +145,7 @@ impl<'a> WM<'a> { /// Handle some common client requests set out by the EWMH spec fn handle_client_message(&mut self, e: &ClientMessageEvent) { - let Some((mon, pos)) = self.clients.find_client_pos(e.window()) else { + let Some(pos) = self.clients.find_client_pos(e.window()) else { return; }; @@ -164,8 +164,8 @@ impl<'a> WM<'a> { return; } - let mon_geom = self.clients.mon_geometry(mon); - let c = self.clients.client_mut(mon, pos).unwrap(); + let mon_geom = self.clients.client_mon(pos).screen_info; + let c = self.clients.client_mut(pos); let fullscreen = match data[0] { 1 => true, 2 => !c.fullscreen(), -- cgit v1.2.3