681 lines
19 KiB
Rust
681 lines
19 KiB
Rust
use {
|
|
crate::{
|
|
control_center::{
|
|
cc_clients::{ClientPane, ClientsPane},
|
|
cc_color_management::ColorManagementPane,
|
|
cc_compositor::CompositorPane,
|
|
cc_gpus::GpusPane,
|
|
cc_idle::IdlePane,
|
|
cc_input::InputPane,
|
|
cc_look_and_feel::LookAndFeelPane,
|
|
cc_outputs::OutputsPane,
|
|
cc_window::{WindowPane, WindowSearchPane},
|
|
cc_xwayland::XwaylandPane,
|
|
},
|
|
egui_adapter::egui_platform::{
|
|
EggError, EggWindow, EggWindowOwner,
|
|
icons::{ICON_CLOSE, ICON_DRAG_INDICATOR, ICON_INFO},
|
|
},
|
|
macros::Bitflag,
|
|
state::State,
|
|
utils::{
|
|
asyncevent::AsyncEvent, copyhashmap::CopyHashMap,
|
|
event_listener::LazyEventSourceListener, numcell::NumCell, static_text::StaticText,
|
|
},
|
|
},
|
|
egui::{
|
|
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, CursorIcon, DragValue, Frame,
|
|
Grid, InnerResponse, Label, Layout, Response, Rgba, RichText, ScrollArea, Sense, SidePanel,
|
|
Stroke, TextBuffer, TextEdit, Ui, UiBuilder, Visuals, Widget, WidgetText, emath::Numeric,
|
|
vec2,
|
|
},
|
|
egui_tiles::{ResizeState, TabState, Tile, TileId, Tiles, Tree},
|
|
linearize::{Linearize, LinearizeExt},
|
|
std::{
|
|
cell::RefCell,
|
|
hash::Hash,
|
|
mem,
|
|
ops::{Deref, DerefMut, RangeInclusive},
|
|
rc::Rc,
|
|
},
|
|
thiserror::Error,
|
|
};
|
|
|
|
mod cc_clients;
|
|
mod cc_color_management;
|
|
mod cc_compositor;
|
|
mod cc_criterion;
|
|
mod cc_gpus;
|
|
mod cc_idle;
|
|
mod cc_input;
|
|
mod cc_look_and_feel;
|
|
mod cc_outputs;
|
|
mod cc_sidebar;
|
|
mod cc_window;
|
|
mod cc_xwayland;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ControlCenterError {
|
|
#[error("Could not get the egg context")]
|
|
GetEggContext(#[source] EggError),
|
|
}
|
|
|
|
linear_ids!(ControlCenterIds, ControlCenterId, u64);
|
|
|
|
pub async fn redraw_control_centers(state: Rc<State>) {
|
|
let cc = &state.control_centers;
|
|
loop {
|
|
cc.redraw.triggered().await;
|
|
let interests = cc.change.take();
|
|
for cc in cc.control_centers.lock().values() {
|
|
if cc.inner.interests.interests.get().intersects(interests) {
|
|
cc.inner.window.request_redraw();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct ControlCenters {
|
|
ids: ControlCenterIds,
|
|
change: NumCell<ControlCenterInterest>,
|
|
redraw: AsyncEvent,
|
|
control_centers: CopyHashMap<ControlCenterId, Rc<ControlCenter>>,
|
|
}
|
|
|
|
bitflags! {
|
|
ControlCenterInterest: u32;
|
|
CCI_COMPOSITOR,
|
|
CCI_IDLE,
|
|
CCI_COLOR_MANAGEMENT,
|
|
CCI_XWAYLAND,
|
|
CCI_OUTPUTS,
|
|
CCI_GPUS,
|
|
CCI_INPUT,
|
|
CCI_LOOK_AND_FEEL,
|
|
}
|
|
|
|
pub struct ControlCenter {
|
|
inner: Rc<ControlCenterInner>,
|
|
}
|
|
|
|
linear_ids!(PaneIds, PaneId, u64);
|
|
|
|
struct ControlCenterInner {
|
|
id: ControlCenterId,
|
|
state: Rc<State>,
|
|
tree: RefCell<Settings>,
|
|
window: Rc<EggWindow>,
|
|
pane_ids: PaneIds,
|
|
interests: Rc<Interests>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Interests {
|
|
interests: NumCell<ControlCenterInterest>,
|
|
interests_array: [NumCell<u64>; <ControlCenterInterest as Bitflag>::Type::BITS as usize],
|
|
}
|
|
|
|
struct Settings {
|
|
tree: Tree<Pane>,
|
|
}
|
|
|
|
struct Pane {
|
|
id: PaneId,
|
|
ps: PaneState,
|
|
own_interests: ControlCenterInterest,
|
|
cc_interests: Rc<Interests>,
|
|
ty: PaneType,
|
|
}
|
|
|
|
struct PaneState {
|
|
errors: Vec<String>,
|
|
}
|
|
|
|
enum PaneType {
|
|
Compositor(CompositorPane),
|
|
Idle(IdlePane),
|
|
ColorManagement(ColorManagementPane),
|
|
Xwayland(XwaylandPane),
|
|
Outputs(Box<OutputsPane>),
|
|
GPUs(GpusPane),
|
|
Input(InputPane),
|
|
LookAndFeel(LookAndFeelPane),
|
|
Clients(ClientsPane),
|
|
Client(ClientPane),
|
|
WindowSearch(WindowSearchPane),
|
|
Window(WindowPane),
|
|
}
|
|
|
|
struct CcBehavior<'a> {
|
|
cc: &'a Rc<ControlCenterInner>,
|
|
close: Option<TileId>,
|
|
open: Option<PaneType>,
|
|
}
|
|
|
|
impl ControlCenters {
|
|
pub fn clear(&self) {
|
|
self.control_centers.clear();
|
|
}
|
|
}
|
|
|
|
impl Pane {
|
|
fn title(&self, res: &mut String) {
|
|
match &self.ty {
|
|
PaneType::Compositor(v) => v.title(res),
|
|
PaneType::Idle(v) => v.title(res),
|
|
PaneType::ColorManagement(v) => v.title(res),
|
|
PaneType::Xwayland(v) => v.title(res),
|
|
PaneType::Outputs(v) => v.title(res),
|
|
PaneType::GPUs(v) => v.title(res),
|
|
PaneType::Input(v) => v.title(res),
|
|
PaneType::LookAndFeel(v) => v.title(res),
|
|
PaneType::Clients(v) => v.title(res),
|
|
PaneType::Client(v) => v.title(res),
|
|
PaneType::WindowSearch(v) => v.title(res),
|
|
PaneType::Window(v) => v.title(res),
|
|
}
|
|
}
|
|
|
|
fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) {
|
|
match &mut self.ty {
|
|
PaneType::Compositor(p) => p.show(ui),
|
|
PaneType::Idle(p) => p.show(ui),
|
|
PaneType::ColorManagement(p) => p.show(ui),
|
|
PaneType::Xwayland(p) => p.show(behavior, ui),
|
|
PaneType::Outputs(p) => p.show(&mut self.ps, ui),
|
|
PaneType::GPUs(p) => p.show(ui),
|
|
PaneType::Input(p) => p.show(&mut self.ps, ui),
|
|
PaneType::LookAndFeel(p) => p.show(ui),
|
|
PaneType::Clients(p) => p.show(behavior, ui),
|
|
PaneType::Client(p) => p.show(behavior, ui),
|
|
PaneType::WindowSearch(p) => p.show(behavior, ui),
|
|
PaneType::Window(p) => p.show(behavior, ui),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PaneType {
|
|
fn interest(&self) -> ControlCenterInterest {
|
|
match self {
|
|
PaneType::Compositor(_) => CCI_COMPOSITOR,
|
|
PaneType::Idle(_) => CCI_IDLE,
|
|
PaneType::ColorManagement(_) => CCI_COLOR_MANAGEMENT,
|
|
PaneType::Xwayland(_) => CCI_XWAYLAND,
|
|
PaneType::Outputs(_) => CCI_OUTPUTS,
|
|
PaneType::GPUs(_) => CCI_GPUS,
|
|
PaneType::Input(_) => CCI_INPUT,
|
|
PaneType::LookAndFeel(_) => CCI_LOOK_AND_FEEL,
|
|
PaneType::Clients(_) => ControlCenterInterest::none(),
|
|
PaneType::Client(_) => ControlCenterInterest::none(),
|
|
PaneType::WindowSearch(_) => ControlCenterInterest::none(),
|
|
PaneType::Window(_) => ControlCenterInterest::none(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl egui_tiles::Behavior<Pane> for CcBehavior<'_> {
|
|
fn pane_ui(&mut self, ui: &mut Ui, tile_id: TileId, pane: &mut Pane) -> egui_tiles::UiResponse {
|
|
let mut drag = false;
|
|
Frame::central_panel(ui.style()).show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
drag = ui
|
|
.add(icon_label(ICON_DRAG_INDICATOR).sense(Sense::drag()))
|
|
.total_drag_delta()
|
|
.map(|d| d.length() >= 5.0)
|
|
.unwrap_or(false);
|
|
let mut title = String::new();
|
|
pane.title(&mut title);
|
|
if ui
|
|
.add(icon_label(&title).sense(Sense::click()))
|
|
.middle_clicked()
|
|
{
|
|
self.close = Some(tile_id);
|
|
}
|
|
if ui
|
|
.add(icon_label(ICON_CLOSE).sense(Sense::click()))
|
|
.clicked()
|
|
{
|
|
self.close = Some(tile_id);
|
|
}
|
|
});
|
|
ui.separator();
|
|
show_errors(ui, &mut pane.ps);
|
|
ui.scope_builder(UiBuilder::new().id(("pane", pane.id)), |ui| {
|
|
ScrollArea::vertical().show(ui, |ui| {
|
|
ui.allocate_space(vec2(ui.available_width(), 0.0));
|
|
pane.show(self, ui);
|
|
});
|
|
});
|
|
});
|
|
if drag {
|
|
egui_tiles::UiResponse::DragStarted
|
|
} else {
|
|
egui_tiles::UiResponse::None
|
|
}
|
|
}
|
|
|
|
fn tab_title_for_pane(&mut self, _pane: &Pane) -> WidgetText {
|
|
"".into()
|
|
}
|
|
|
|
fn tab_hover_cursor_icon(&self) -> CursorIcon {
|
|
CursorIcon::Default
|
|
}
|
|
|
|
fn tab_title_for_tile(&mut self, tiles: &Tiles<Pane>, tile_id: TileId) -> WidgetText {
|
|
fn add_title(tiles: &Tiles<Pane>, res: &mut String, first: &mut bool, tile_id: TileId) {
|
|
if !mem::take(first) {
|
|
res.push_str("/");
|
|
}
|
|
let Some(tile) = tiles.get(tile_id) else {
|
|
res.push_str("MISSING TILE");
|
|
return;
|
|
};
|
|
match tile {
|
|
Tile::Pane(p) => p.title(res),
|
|
Tile::Container(c) => {
|
|
let mut first = true;
|
|
for &tile_id in c.children() {
|
|
add_title(tiles, res, &mut first, tile_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut res = String::new();
|
|
let mut first = true;
|
|
add_title(tiles, &mut res, &mut first, tile_id);
|
|
res.into()
|
|
}
|
|
|
|
fn on_tab_button(
|
|
&mut self,
|
|
_tiles: &Tiles<Pane>,
|
|
tile_id: TileId,
|
|
button_response: Response,
|
|
) -> Response {
|
|
if button_response.middle_clicked() {
|
|
self.close = Some(tile_id);
|
|
}
|
|
button_response
|
|
}
|
|
|
|
fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> Stroke {
|
|
match resize_state {
|
|
ResizeState::Idle => style.visuals.widgets.noninteractive.bg_stroke,
|
|
ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke,
|
|
ResizeState::Dragging => style.visuals.widgets.active.fg_stroke,
|
|
}
|
|
}
|
|
|
|
fn tab_bar_color(&self, visuals: &Visuals) -> Color32 {
|
|
(Rgba::from(visuals.panel_fill) * Rgba::from_gray(0.8)).into()
|
|
}
|
|
|
|
fn tab_bg_color(
|
|
&self,
|
|
visuals: &Visuals,
|
|
_tiles: &Tiles<Pane>,
|
|
_tile_id: TileId,
|
|
state: &TabState,
|
|
) -> Color32 {
|
|
match state.active {
|
|
true => visuals.panel_fill,
|
|
false => self.tab_bar_color(visuals),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EggWindowOwner for ControlCenterInner {
|
|
fn close(&self) {
|
|
self.close();
|
|
}
|
|
|
|
fn render(self: Rc<Self>, ctx: &Context) {
|
|
let settings = &mut *self.tree.borrow_mut();
|
|
SidePanel::left("sidebar").show(ctx, |ui| self.show_sidebar(&mut settings.tree, ui));
|
|
CentralPanel::default()
|
|
.frame(
|
|
Frame::central_panel(&ctx.style())
|
|
.outer_margin(0.0)
|
|
.inner_margin(0.0),
|
|
)
|
|
.show(ctx, |ui| {
|
|
let tree = &mut settings.tree;
|
|
let mut behavior = CcBehavior {
|
|
cc: &self,
|
|
close: Default::default(),
|
|
open: Default::default(),
|
|
};
|
|
tree.ui(&mut behavior, ui);
|
|
if let Some(close) = behavior.close {
|
|
tree.set_visible(close, false);
|
|
tree.remove_recursively(close);
|
|
ui.ctx().request_repaint();
|
|
}
|
|
if let Some(ty) = behavior.open {
|
|
self.open(tree, ty);
|
|
ui.ctx().request_repaint();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
pub fn open_control_center(self: &Rc<Self>) -> Result<Rc<ControlCenter>, ControlCenterError> {
|
|
let ctx = self
|
|
.get_egg_context()
|
|
.map_err(ControlCenterError::GetEggContext)?;
|
|
let window = ctx.create_window("Control Center");
|
|
let cc = Rc::new(ControlCenter {
|
|
inner: Rc::new(ControlCenterInner {
|
|
id: self.control_centers.ids.next(),
|
|
window,
|
|
state: self.clone(),
|
|
tree: RefCell::new(Settings {
|
|
tree: Tree::new_tabs("abcd", vec![]),
|
|
}),
|
|
pane_ids: Default::default(),
|
|
interests: Default::default(),
|
|
}),
|
|
});
|
|
cc.inner.window.set_owner(Some(cc.inner.clone()));
|
|
self.control_centers
|
|
.control_centers
|
|
.set(cc.inner.id, cc.clone());
|
|
Ok(cc)
|
|
}
|
|
|
|
pub fn trigger_cci(&self, cci: ControlCenterInterest) {
|
|
self.control_centers.change.or_assign(cci);
|
|
self.control_centers.redraw.trigger();
|
|
}
|
|
}
|
|
|
|
impl ControlCenterInner {
|
|
fn close(&self) {
|
|
self.window.set_owner(None);
|
|
self.tree.borrow_mut().tree = Tree::empty("");
|
|
self.state.control_centers.control_centers.remove(&self.id);
|
|
}
|
|
}
|
|
|
|
impl Drop for ControlCenter {
|
|
fn drop(&mut self) {
|
|
self.inner.close();
|
|
}
|
|
}
|
|
|
|
impl ControlCenterInner {
|
|
fn create_pane(&self, ty: PaneType) -> Pane {
|
|
let pane = Pane {
|
|
id: self.pane_ids.next(),
|
|
ps: PaneState {
|
|
errors: Default::default(),
|
|
},
|
|
own_interests: ty.interest(),
|
|
cc_interests: self.interests.clone(),
|
|
ty,
|
|
};
|
|
let own = pane.own_interests;
|
|
for (idx, v) in pane.cc_interests.interests_array.iter().enumerate() {
|
|
let interest = ControlCenterInterest(1 << idx);
|
|
if own.intersects(interest) && v.fetch_add(1) == 0 {
|
|
pane.cc_interests.interests.or_assign(interest);
|
|
}
|
|
}
|
|
pane
|
|
}
|
|
|
|
fn open(&self, tree: &mut Tree<Pane>, ty: PaneType) {
|
|
let pane = self.create_pane(ty);
|
|
let id = tree.tiles.insert_pane(pane);
|
|
if let Some(root) = tree.root
|
|
&& let Some(tile) = tree.tiles.get_mut(root)
|
|
{
|
|
match tile {
|
|
Tile::Container(c) => {
|
|
c.add_child(id);
|
|
}
|
|
Tile::Pane(_) => {
|
|
let root = tree.tiles.insert_tab_tile(vec![root, id]);
|
|
tree.root = Some(root);
|
|
}
|
|
}
|
|
} else {
|
|
tree.root = Some(id);
|
|
}
|
|
tree.make_active(|t, _| t == id);
|
|
}
|
|
}
|
|
|
|
impl Drop for Pane {
|
|
fn drop(&mut self) {
|
|
let own = self.own_interests;
|
|
for (idx, v) in self.cc_interests.interests_array.iter().enumerate() {
|
|
let interest = ControlCenterInterest(1 << idx);
|
|
if own.intersects(interest) && v.fetch_sub(1) == 1 {
|
|
self.cc_interests.interests.and_assign(!interest);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn icon_label(icon: &str) -> Label {
|
|
Label::new(icon).selectable(false)
|
|
}
|
|
|
|
fn grid_label(ui: &mut Ui, label: &str) {
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
ui.label(label);
|
|
});
|
|
}
|
|
|
|
fn grid_label_ui<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
|
ui.with_layout(Layout::right_to_left(Align::Center), add_contents)
|
|
}
|
|
|
|
fn tip(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) {
|
|
icon_label(ICON_INFO).ui(ui).on_hover_ui(add_contents);
|
|
}
|
|
|
|
fn text_edit(ui: &mut Ui, v: &mut dyn TextBuffer) -> Response {
|
|
TextEdit::singleline(v)
|
|
.clip_text(false)
|
|
.min_size(vec2(200.0, 0.0))
|
|
.ui(ui)
|
|
}
|
|
|
|
fn show_errors(ui: &mut Ui, pane: &mut PaneState) {
|
|
if pane.errors.is_empty() {
|
|
return;
|
|
}
|
|
let mut to_remove = None;
|
|
for (idx, e) in pane.errors.iter().enumerate() {
|
|
ui.horizontal(|ui| {
|
|
Frame::new().inner_margin(5.0).show(ui, |ui| {
|
|
if ui.button(ICON_CLOSE).clicked() {
|
|
to_remove = Some(idx);
|
|
}
|
|
ui.label(
|
|
RichText::new("Error:")
|
|
.strong()
|
|
.color(ui.style().visuals.error_fg_color),
|
|
);
|
|
ui.add(Label::new(e).wrap());
|
|
});
|
|
});
|
|
}
|
|
if let Some(idx) = to_remove {
|
|
pane.errors.remove(idx);
|
|
ui.ctx().request_repaint();
|
|
}
|
|
ui.separator();
|
|
}
|
|
|
|
fn grid<R>(
|
|
ui: &mut Ui,
|
|
id_salt: impl Hash,
|
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
) -> InnerResponse<R> {
|
|
let mut spacing = ui.spacing().item_spacing;
|
|
spacing.x *= 3.0;
|
|
Grid::new(id_salt).spacing(spacing).show(ui, add_contents)
|
|
}
|
|
|
|
fn row<R>(ui: &mut Ui, name: &str, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
|
row_ui(ui, name, |_| (), add_contents)
|
|
}
|
|
|
|
fn row_ui<R, S>(
|
|
ui: &mut Ui,
|
|
name: &str,
|
|
label: impl FnOnce(&mut Ui) -> S,
|
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
) -> R {
|
|
let ui = &mut *ui.row();
|
|
grid_label_ui(ui, |ui| {
|
|
ui.label(name);
|
|
label(ui);
|
|
});
|
|
add_contents(ui)
|
|
}
|
|
|
|
fn bool(ui: &mut Ui, name: &str, old: bool, set: impl FnOnce(bool)) {
|
|
bool_ui(ui, name, |_| (), old, set);
|
|
}
|
|
|
|
fn bool_ui<R>(
|
|
ui: &mut Ui,
|
|
name: &str,
|
|
label: impl FnOnce(&mut Ui) -> R,
|
|
mut v: bool,
|
|
set: impl FnOnce(bool),
|
|
) {
|
|
row_ui(ui, name, label, |ui| {
|
|
if Checkbox::without_text(&mut v).ui(ui).changed() {
|
|
set(v);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn read_only_bool(ui: &mut Ui, name: &str, old: bool) {
|
|
read_only_bool_ui(ui, name, |_| (), old);
|
|
}
|
|
|
|
fn read_only_bool_ui<R>(ui: &mut Ui, name: &str, label: impl FnOnce(&mut Ui) -> R, mut v: bool) {
|
|
row_ui(ui, name, label, |ui| {
|
|
ui.add_enabled_ui(false, |ui| Checkbox::without_text(&mut v).ui(ui));
|
|
});
|
|
}
|
|
|
|
fn combo_box<T>(ui: &mut Ui, name: &str, old: T, set: impl FnOnce(T))
|
|
where
|
|
T: StaticText + Linearize + PartialEq + Copy,
|
|
{
|
|
combo_box_ui(ui, name, |_| (), old, set);
|
|
}
|
|
|
|
fn combo_box_ui<R, T>(
|
|
ui: &mut Ui,
|
|
name: &str,
|
|
label: impl FnOnce(&mut Ui) -> R,
|
|
mut v: T,
|
|
set: impl FnOnce(T),
|
|
) where
|
|
T: StaticText + Linearize + PartialEq + Copy,
|
|
{
|
|
row_ui(ui, name, label, |ui| {
|
|
let old = v;
|
|
ComboBox::from_id_salt(name)
|
|
.selected_text(v.text())
|
|
.show_ui(ui, |ui| {
|
|
for s in T::variants() {
|
|
ui.selectable_value(&mut v, s, s.text());
|
|
}
|
|
});
|
|
if old != v {
|
|
set(v);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn drag_value<N>(
|
|
ui: &mut Ui,
|
|
name: &str,
|
|
old: N,
|
|
range: RangeInclusive<N>,
|
|
speed: f64,
|
|
set: impl FnOnce(N),
|
|
) where
|
|
N: Numeric,
|
|
{
|
|
drag_value_ui(ui, name, |_| (), old, range, speed, set);
|
|
}
|
|
|
|
fn drag_value_ui<R, N>(
|
|
ui: &mut Ui,
|
|
name: &str,
|
|
label: impl FnOnce(&mut Ui) -> R,
|
|
mut v: N,
|
|
range: RangeInclusive<N>,
|
|
speed: f64,
|
|
set: impl FnOnce(N),
|
|
) where
|
|
N: Numeric,
|
|
{
|
|
row_ui(ui, name, label, |ui| {
|
|
if DragValue::new(&mut v)
|
|
.range(range)
|
|
.speed(speed)
|
|
.ui(ui)
|
|
.changed()
|
|
{
|
|
set(v);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn label(ui: &mut Ui, name: &str, text: impl Into<WidgetText>) {
|
|
row(ui, name, |ui| ui.label(text));
|
|
}
|
|
|
|
trait GridExt {
|
|
fn row(&mut self) -> impl DerefMut<Target = Ui>;
|
|
}
|
|
|
|
impl GridExt for Ui {
|
|
fn row(&mut self) -> impl DerefMut<Target = Ui> {
|
|
GridRow { ui: self }
|
|
}
|
|
}
|
|
|
|
struct GridRow<'a> {
|
|
ui: &'a mut Ui,
|
|
}
|
|
|
|
impl Deref for GridRow<'_> {
|
|
type Target = Ui;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.ui
|
|
}
|
|
}
|
|
|
|
impl DerefMut for GridRow<'_> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut *self.ui
|
|
}
|
|
}
|
|
|
|
impl Drop for GridRow<'_> {
|
|
fn drop(&mut self) {
|
|
self.end_row();
|
|
}
|
|
}
|
|
|
|
impl LazyEventSourceListener for ControlCenterInner {
|
|
fn triggered(self: Rc<Self>) {
|
|
self.window.request_redraw();
|
|
}
|
|
}
|