1459 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			1459 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
/*!
 | 
						|
 * Layout-related data.
 | 
						|
 * 
 | 
						|
 * The `View` contains `Row`s and each `Row` contains `Button`s.
 | 
						|
 * They carry data relevant to their positioning only,
 | 
						|
 * except the Button, which also carries some data
 | 
						|
 * about its appearance and function.
 | 
						|
 * 
 | 
						|
 * The layout is determined bottom-up, by measuring `Button` sizes,
 | 
						|
 * deriving `Row` sizes from them, and then centering them within the `View`.
 | 
						|
 * 
 | 
						|
 * That makes the `View` position immutable,
 | 
						|
 * and therefore different than the other positions.
 | 
						|
 * 
 | 
						|
 * Note that it might be a better idea
 | 
						|
 * to make `View` position depend on its contents,
 | 
						|
 * and let the renderer scale and center it within the widget.
 | 
						|
 */
 | 
						|
 | 
						|
use std::cell::RefCell;
 | 
						|
use std::collections::{ HashMap, HashSet };
 | 
						|
use std::ffi::CString;
 | 
						|
use std::fmt;
 | 
						|
use std::rc::Rc;
 | 
						|
use std::vec::Vec;
 | 
						|
 | 
						|
use ::action::Action;
 | 
						|
use ::drawing;
 | 
						|
use ::keyboard::KeyState;
 | 
						|
use ::logging;
 | 
						|
use ::manager;
 | 
						|
use ::submission::{ Submission, SubmitData, Timestamp };
 | 
						|
use ::util::find_max_double;
 | 
						|
 | 
						|
// Traits
 | 
						|
use std::borrow::Borrow;
 | 
						|
use ::logging::Warn;
 | 
						|
 | 
						|
/// Gathers stuff defined in C or called by C
 | 
						|
pub mod c {
 | 
						|
    use super::*;
 | 
						|
 | 
						|
    use gtk_sys;
 | 
						|
    use std::os::raw::c_void;
 | 
						|
 | 
						|
    use std::ops::{ Add, Sub };
 | 
						|
 | 
						|
    // The following defined in C
 | 
						|
    #[repr(transparent)]
 | 
						|
    #[derive(Copy, Clone)]
 | 
						|
    pub struct EekGtkKeyboard(pub *const gtk_sys::GtkWidget);
 | 
						|
 | 
						|
    extern "C" {
 | 
						|
        #[allow(improper_ctypes)]
 | 
						|
        pub fn eek_gtk_keyboard_emit_feedback(
 | 
						|
            keyboard: EekGtkKeyboard,
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /// Defined in eek-types.h
 | 
						|
    #[repr(C)]
 | 
						|
    #[derive(Clone, Debug, PartialEq)]
 | 
						|
    pub struct Point {
 | 
						|
        pub x: f64,
 | 
						|
        pub y: f64,
 | 
						|
    }
 | 
						|
    
 | 
						|
    impl Add for Point {
 | 
						|
        type Output = Self;
 | 
						|
        fn add(self, other: Self) -> Self {
 | 
						|
            &self + other
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    impl Add<Point> for &Point {
 | 
						|
        type Output = Point;
 | 
						|
        fn add(self, other: Point) -> Point {
 | 
						|
            Point {
 | 
						|
                x: self.x + other.x,
 | 
						|
                y: self.y + other.y,
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    impl Sub<&Point> for Point {
 | 
						|
        type Output = Point;
 | 
						|
        fn sub(self, other: &Point) -> Point {
 | 
						|
            Point {
 | 
						|
                x: self.x - other.x,
 | 
						|
                y: self.y - other.y,
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Defined in eek-types.h
 | 
						|
    #[repr(C)]
 | 
						|
    #[derive(Clone, Debug, PartialEq)]
 | 
						|
    pub struct Bounds {
 | 
						|
        pub x: f64,
 | 
						|
        pub y: f64,
 | 
						|
        pub width: f64,
 | 
						|
        pub height: f64
 | 
						|
    }
 | 
						|
 | 
						|
    impl Bounds {
 | 
						|
        pub fn contains(&self, point: &Point) -> bool {
 | 
						|
            point.x > self.x && point.x < self.x + self.width
 | 
						|
                && point.y > self.y && point.y < self.y + self.height
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Translate and then scale
 | 
						|
    #[repr(C)]
 | 
						|
    pub struct Transformation {
 | 
						|
        pub origin_x: f64,
 | 
						|
        pub origin_y: f64,
 | 
						|
        pub scale: f64,
 | 
						|
    }
 | 
						|
 | 
						|
    impl Transformation {
 | 
						|
        /// Applies the new transformation after this one
 | 
						|
        pub fn chain(self, next: Transformation) -> Transformation {
 | 
						|
            Transformation {
 | 
						|
                origin_x: self.origin_x + self.scale * next.origin_x,
 | 
						|
                origin_y: self.origin_y + self.scale * next.origin_y,
 | 
						|
                scale: self.scale * next.scale,
 | 
						|
            }
 | 
						|
        }
 | 
						|
        fn forward(&self, p: Point) -> Point {
 | 
						|
            Point {
 | 
						|
                x: (p.x - self.origin_x) / self.scale,
 | 
						|
                y: (p.y - self.origin_y) / self.scale,
 | 
						|
            }
 | 
						|
        }
 | 
						|
        fn reverse(&self, p: Point) -> Point {
 | 
						|
            Point {
 | 
						|
                x: p.x * self.scale + self.origin_x,
 | 
						|
                y: p.y * self.scale + self.origin_y,
 | 
						|
            }
 | 
						|
        }
 | 
						|
        pub fn reverse_bounds(&self, b: Bounds) -> Bounds {
 | 
						|
            let start = self.reverse(Point { x: b.x, y: b.y });
 | 
						|
            let end = self.reverse(Point {
 | 
						|
                x: b.x + b.width,
 | 
						|
                y: b.y + b.height,
 | 
						|
            });
 | 
						|
            Bounds {
 | 
						|
                x: start.x,
 | 
						|
                y: start.y,
 | 
						|
                width: end.x - start.x,
 | 
						|
                height: end.y - start.y,
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // This is constructed only in C, no need for warnings
 | 
						|
    #[allow(dead_code)]
 | 
						|
    #[repr(transparent)]
 | 
						|
    pub struct LevelKeyboard(*const c_void);
 | 
						|
 | 
						|
    // The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
 | 
						|
    
 | 
						|
    /// Positions the layout contents within the available space.
 | 
						|
    /// The origin of the transformation is the point inside the margins.
 | 
						|
    #[no_mangle]
 | 
						|
    pub extern "C"
 | 
						|
    fn squeek_layout_calculate_transformation(
 | 
						|
        layout: *const Layout,
 | 
						|
        allocation_width: f64,
 | 
						|
        allocation_height: f64,
 | 
						|
    ) -> Transformation {
 | 
						|
        let layout = unsafe { &*layout };
 | 
						|
        layout.calculate_transformation(Size {
 | 
						|
            width: allocation_width,
 | 
						|
            height: allocation_height,
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    #[no_mangle]
 | 
						|
    pub extern "C"
 | 
						|
    fn squeek_layout_get_kind(layout: *const Layout) -> u32 {
 | 
						|
        let layout = unsafe { &*layout };
 | 
						|
        layout.kind.clone() as u32
 | 
						|
    }
 | 
						|
 | 
						|
    #[no_mangle]
 | 
						|
    pub extern "C"
 | 
						|
    fn squeek_layout_free(layout: *mut Layout) {
 | 
						|
        unsafe { Box::from_raw(layout) };
 | 
						|
    }
 | 
						|
 | 
						|
    /// Entry points for more complex procedures and algorithms which span multiple modules
 | 
						|
    pub mod procedures {
 | 
						|
        use super::*;
 | 
						|
 | 
						|
        /// Release pointer in the specified position
 | 
						|
        #[no_mangle]
 | 
						|
        pub extern "C"
 | 
						|
        fn squeek_layout_release(
 | 
						|
            layout: *mut Layout,
 | 
						|
            submission: *mut Submission,
 | 
						|
            widget_to_layout: Transformation,
 | 
						|
            time: u32,
 | 
						|
            manager: manager::c::Manager,
 | 
						|
            ui_keyboard: EekGtkKeyboard,
 | 
						|
        ) {
 | 
						|
            let time = Timestamp(time);
 | 
						|
            let layout = unsafe { &mut *layout };
 | 
						|
            let submission = unsafe { &mut *submission };
 | 
						|
            let ui_backend = UIBackend {
 | 
						|
                widget_to_layout,
 | 
						|
                keyboard: ui_keyboard,
 | 
						|
            };
 | 
						|
 | 
						|
            // The list must be copied,
 | 
						|
            // because it will be mutated in the loop
 | 
						|
            for key in layout.pressed_keys.clone() {
 | 
						|
                let key: &Rc<RefCell<KeyState>> = key.borrow();
 | 
						|
                seat::handle_release_key(
 | 
						|
                    layout,
 | 
						|
                    submission,
 | 
						|
                    Some(&ui_backend),
 | 
						|
                    time,
 | 
						|
                    Some(manager),
 | 
						|
                    key,
 | 
						|
                );
 | 
						|
            }
 | 
						|
            drawing::queue_redraw(ui_keyboard);
 | 
						|
        }
 | 
						|
 | 
						|
        /// Release all buttons but don't redraw
 | 
						|
        #[no_mangle]
 | 
						|
        pub extern "C"
 | 
						|
        fn squeek_layout_release_all_only(
 | 
						|
            layout: *mut Layout,
 | 
						|
            submission: *mut Submission,
 | 
						|
            time: u32,
 | 
						|
        ) {
 | 
						|
            let layout = unsafe { &mut *layout };
 | 
						|
            let submission = unsafe { &mut *submission };
 | 
						|
            // The list must be copied,
 | 
						|
            // because it will be mutated in the loop
 | 
						|
            for key in layout.pressed_keys.clone() {
 | 
						|
                let key: &Rc<RefCell<KeyState>> = key.borrow();
 | 
						|
                seat::handle_release_key(
 | 
						|
                    layout,
 | 
						|
                    submission,
 | 
						|
                    None, // don't update UI
 | 
						|
                    Timestamp(time),
 | 
						|
                    None, // don't switch layouts
 | 
						|
                    &mut key.clone(),
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        #[no_mangle]
 | 
						|
        pub extern "C"
 | 
						|
        fn squeek_layout_depress(
 | 
						|
            layout: *mut Layout,
 | 
						|
            submission: *mut Submission,
 | 
						|
            x_widget: f64, y_widget: f64,
 | 
						|
            widget_to_layout: Transformation,
 | 
						|
            time: u32,
 | 
						|
            ui_keyboard: EekGtkKeyboard,
 | 
						|
        ) {
 | 
						|
            let layout = unsafe { &mut *layout };
 | 
						|
            let submission = unsafe { &mut *submission };
 | 
						|
            let point = widget_to_layout.forward(
 | 
						|
                Point { x: x_widget, y: y_widget }
 | 
						|
            );
 | 
						|
 | 
						|
            let state = layout.find_button_by_position(point)
 | 
						|
                .map(|place| place.button.state.clone());
 | 
						|
            
 | 
						|
            if let Some(state) = state {
 | 
						|
                seat::handle_press_key(
 | 
						|
                    layout,
 | 
						|
                    submission,
 | 
						|
                    Timestamp(time),
 | 
						|
                    &state,
 | 
						|
                );
 | 
						|
                // maybe TODO: draw on the display buffer here
 | 
						|
                drawing::queue_redraw(ui_keyboard);
 | 
						|
                unsafe {
 | 
						|
                    eek_gtk_keyboard_emit_feedback(ui_keyboard);
 | 
						|
                }
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        // FIXME: this will work funny
 | 
						|
        // when 2 touch points are on buttons and moving one after another
 | 
						|
        // Solution is to have separate pressed lists for each point
 | 
						|
        #[no_mangle]
 | 
						|
        pub extern "C"
 | 
						|
        fn squeek_layout_drag(
 | 
						|
            layout: *mut Layout,
 | 
						|
            submission: *mut Submission,
 | 
						|
            x_widget: f64, y_widget: f64,
 | 
						|
            widget_to_layout: Transformation,
 | 
						|
            time: u32,
 | 
						|
            manager: manager::c::Manager,
 | 
						|
            ui_keyboard: EekGtkKeyboard,
 | 
						|
        ) {
 | 
						|
            let time = Timestamp(time);
 | 
						|
            let layout = unsafe { &mut *layout };
 | 
						|
            let submission = unsafe { &mut *submission };
 | 
						|
            let ui_backend = UIBackend {
 | 
						|
                widget_to_layout,
 | 
						|
                keyboard: ui_keyboard,
 | 
						|
            };
 | 
						|
            let point = ui_backend.widget_to_layout.forward(
 | 
						|
                Point { x: x_widget, y: y_widget }
 | 
						|
            );
 | 
						|
            
 | 
						|
            let pressed = layout.pressed_keys.clone();
 | 
						|
            let button_info = {
 | 
						|
                let place = layout.find_button_by_position(point);
 | 
						|
                place.map(|place| {(
 | 
						|
                    place.button.state.clone(),
 | 
						|
                    place.button.clone(),
 | 
						|
                    place.offset,
 | 
						|
                )})
 | 
						|
            };
 | 
						|
 | 
						|
            if let Some((state, _button, _view_position)) = button_info {
 | 
						|
                let mut found = false;
 | 
						|
                for wrapped_key in pressed {
 | 
						|
                    let key: &Rc<RefCell<KeyState>> = wrapped_key.borrow();
 | 
						|
                    if Rc::ptr_eq(&state, &wrapped_key.0) {
 | 
						|
                        found = true;
 | 
						|
                    } else {
 | 
						|
                        seat::handle_release_key(
 | 
						|
                            layout,
 | 
						|
                            submission,
 | 
						|
                            Some(&ui_backend),
 | 
						|
                            time,
 | 
						|
                            Some(manager),
 | 
						|
                            key,
 | 
						|
                        );
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                if !found {
 | 
						|
                    seat::handle_press_key(
 | 
						|
                        layout,
 | 
						|
                        submission,
 | 
						|
                        time,
 | 
						|
                        &state,
 | 
						|
                    );
 | 
						|
                    // maybe TODO: draw on the display buffer here
 | 
						|
                    unsafe {
 | 
						|
                        eek_gtk_keyboard_emit_feedback(ui_keyboard);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                for wrapped_key in pressed {
 | 
						|
                    let key: &Rc<RefCell<KeyState>> = wrapped_key.borrow();
 | 
						|
                    seat::handle_release_key(
 | 
						|
                        layout,
 | 
						|
                        submission,
 | 
						|
                        Some(&ui_backend),
 | 
						|
                        time,
 | 
						|
                        Some(manager),
 | 
						|
                        key,
 | 
						|
                    );
 | 
						|
                }
 | 
						|
            }
 | 
						|
            drawing::queue_redraw(ui_keyboard);
 | 
						|
        }
 | 
						|
 | 
						|
        #[cfg(test)]
 | 
						|
        mod test {
 | 
						|
            use super::*;
 | 
						|
            
 | 
						|
            fn near(a: f64, b: f64) -> bool {
 | 
						|
                (a - b).abs() < ((a + b) * 0.001f64).abs()
 | 
						|
            }
 | 
						|
            
 | 
						|
            #[test]
 | 
						|
            fn transform_back() {
 | 
						|
                let transform = Transformation {
 | 
						|
                    origin_x: 10f64,
 | 
						|
                    origin_y: 11f64,
 | 
						|
                    scale: 12f64,
 | 
						|
                };
 | 
						|
                let point = Point { x: 1f64, y: 1f64 };
 | 
						|
                let transformed = transform.reverse(transform.forward(point.clone()));
 | 
						|
                assert!(near(point.x, transformed.x));
 | 
						|
                assert!(near(point.y, transformed.y));
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub struct ButtonPlace<'a> {
 | 
						|
    button: &'a Button,
 | 
						|
    offset: c::Point,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug, Clone, PartialEq)]
 | 
						|
pub struct Size {
 | 
						|
    pub width: f64,
 | 
						|
    pub height: f64,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug, Clone, PartialEq)]
 | 
						|
pub enum Label {
 | 
						|
    /// Text used to display the symbol
 | 
						|
    Text(CString),
 | 
						|
    /// Icon name used to render the symbol
 | 
						|
    IconName(CString),
 | 
						|
}
 | 
						|
 | 
						|
/// The graphical representation of a button
 | 
						|
#[derive(Clone, Debug)]
 | 
						|
pub struct Button {
 | 
						|
    /// ID string, e.g. for CSS 
 | 
						|
    pub name: CString,
 | 
						|
    /// Label to display to the user
 | 
						|
    pub label: Label,
 | 
						|
    pub size: Size,
 | 
						|
    /// The name of the visual class applied
 | 
						|
    pub outline_name: CString,
 | 
						|
    /// current state, shared with other buttons
 | 
						|
    pub state: Rc<RefCell<KeyState>>,
 | 
						|
}
 | 
						|
 | 
						|
impl Button {
 | 
						|
    pub fn get_bounds(&self) -> c::Bounds {
 | 
						|
        c::Bounds {
 | 
						|
            x: 0.0, y: 0.0,
 | 
						|
            width: self.size.width, height: self.size.height,
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// The graphical representation of a row of buttons
 | 
						|
#[derive(Clone, Debug)]
 | 
						|
pub struct Row {
 | 
						|
    /// Buttons together with their offset from the left relative to the row.
 | 
						|
    /// ie. the first button always start at 0.
 | 
						|
    buttons: Vec<(f64, Box<Button>)>,
 | 
						|
 | 
						|
    /// Total size of the row
 | 
						|
    size: Size,
 | 
						|
}
 | 
						|
 | 
						|
impl Row {
 | 
						|
    pub fn new(buttons: Vec<(f64, Box<Button>)>) -> Row {
 | 
						|
        // Make sure buttons are sorted by offset.
 | 
						|
        debug_assert!({
 | 
						|
            let mut sorted = buttons.clone();
 | 
						|
            sorted.sort_by(|(f1, _), (f2, _)| f1.partial_cmp(f2).unwrap());
 | 
						|
 | 
						|
            sorted.iter().map(|(f, _)| *f).collect::<Vec<_>>()
 | 
						|
                == buttons.iter().map(|(f, _)| *f).collect::<Vec<_>>()
 | 
						|
        });
 | 
						|
 | 
						|
        let width = buttons.iter().next_back()
 | 
						|
            .map(|(x_offset, button)| button.size.width + x_offset)
 | 
						|
            .unwrap_or(0.0);
 | 
						|
 | 
						|
        let height = find_max_double(
 | 
						|
            buttons.iter(),
 | 
						|
            |(_offset, button)| button.size.height,
 | 
						|
        );
 | 
						|
 | 
						|
        Row { buttons, size: Size { width, height } }
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn get_size(&self) -> Size {
 | 
						|
        self.size.clone()
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn get_buttons(&self) -> &Vec<(f64, Box<Button>)> {
 | 
						|
        &self.buttons
 | 
						|
    }
 | 
						|
 | 
						|
    /// Finds the first button that covers the specified point
 | 
						|
    /// relative to row's position's origin
 | 
						|
    fn find_button_by_position(&self, x: f64) -> &(f64, Box<Button>)
 | 
						|
    {
 | 
						|
        // Buttons are sorted so we can use a binary search to find the clicked
 | 
						|
        // button. Note this doesn't check whether the point is actually within
 | 
						|
        // a button. This is on purpose as we want a click past the left edge of
 | 
						|
        // the left-most button to register as a click.
 | 
						|
        let result = self.buttons.binary_search_by(
 | 
						|
            |&(f, _)| f.partial_cmp(&x).unwrap()
 | 
						|
        );
 | 
						|
 | 
						|
        let index = result.unwrap_or_else(|r| r);
 | 
						|
        let index = if index > 0 { index - 1 } else { 0 };
 | 
						|
 | 
						|
        &self.buttons[index]
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Clone, Debug)]
 | 
						|
pub struct Spacing {
 | 
						|
    pub row: f64,
 | 
						|
    pub button: f64,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Clone)]
 | 
						|
pub struct View {
 | 
						|
    /// Rows together with their offsets from the top left
 | 
						|
    rows: Vec<(c::Point, Row)>,
 | 
						|
 | 
						|
    /// Total size of the view
 | 
						|
    size: Size,
 | 
						|
}
 | 
						|
 | 
						|
impl View {
 | 
						|
    pub fn new(rows: Vec<(f64, Row)>) -> View {
 | 
						|
        // Make sure rows are sorted by offset.
 | 
						|
        debug_assert!({
 | 
						|
            let mut sorted = rows.clone();
 | 
						|
            sorted.sort_by(|(f1, _), (f2, _)| f1.partial_cmp(f2).unwrap());
 | 
						|
 | 
						|
            sorted.iter().map(|(f, _)| *f).collect::<Vec<_>>()
 | 
						|
                == rows.iter().map(|(f, _)| *f).collect::<Vec<_>>()
 | 
						|
        });
 | 
						|
 | 
						|
        // No need to call `get_rows()`,
 | 
						|
        // as the biggest row is the most far reaching in both directions
 | 
						|
        // because they are all centered.
 | 
						|
        let width = find_max_double(rows.iter(), |(_offset, row)| row.size.width);
 | 
						|
 | 
						|
        let height = rows.iter().next_back()
 | 
						|
            .map(|(y_offset, row)| row.size.height + y_offset)
 | 
						|
            .unwrap_or(0.0);
 | 
						|
 | 
						|
        // Center the rows
 | 
						|
        let rows = rows.into_iter().map(|(y_offset, row)| {(
 | 
						|
                c::Point {
 | 
						|
                    x: (width - row.size.width) / 2.0,
 | 
						|
                    y: y_offset,
 | 
						|
                },
 | 
						|
                row,
 | 
						|
            )}).collect::<Vec<_>>();
 | 
						|
 | 
						|
        View { rows, size: Size { width, height } }
 | 
						|
    }
 | 
						|
    /// Finds the first button that covers the specified point
 | 
						|
    /// relative to view's position's origin
 | 
						|
    fn find_button_by_position(&self, point: c::Point)
 | 
						|
        -> Option<ButtonPlace>
 | 
						|
    {
 | 
						|
        // Only test bounds of the view here, letting rows/column search extend
 | 
						|
        // to the edges of these bounds.
 | 
						|
        let bounds = c::Bounds {
 | 
						|
            x: 0.0,
 | 
						|
            y: 0.0,
 | 
						|
            width: self.size.width,
 | 
						|
            height: self.size.height,
 | 
						|
        };
 | 
						|
        if !bounds.contains(&point) {
 | 
						|
            return None;
 | 
						|
        }
 | 
						|
 | 
						|
        // Rows are sorted so we can use a binary search to find the row.
 | 
						|
        let result = self.rows.binary_search_by(
 | 
						|
            |(f, _)| f.y.partial_cmp(&point.y).unwrap()
 | 
						|
        );
 | 
						|
 | 
						|
        let index = result.unwrap_or_else(|r| r);
 | 
						|
        let index = if index > 0 { index - 1 } else { 0 };
 | 
						|
 | 
						|
        let row = &self.rows[index];
 | 
						|
        let button = row.1.find_button_by_position(point.x - row.0.x);
 | 
						|
 | 
						|
        Some(ButtonPlace {
 | 
						|
            button: &button.1,
 | 
						|
            offset: &row.0 + c::Point { x: button.0, y: 0.0 },
 | 
						|
        })
 | 
						|
    }
 | 
						|
    
 | 
						|
    pub fn get_size(&self) -> Size {
 | 
						|
        self.size.clone()
 | 
						|
    }
 | 
						|
    
 | 
						|
    /// Returns positioned rows, with appropriate x offsets (centered)
 | 
						|
    pub fn get_rows(&self) -> &Vec<(c::Point, Row)> {
 | 
						|
        &self.rows
 | 
						|
    }
 | 
						|
 | 
						|
    /// Returns a size which contains all the views
 | 
						|
    /// if they are all centered on the same point.
 | 
						|
    pub fn calculate_super_size(views: Vec<&View>) -> Size {
 | 
						|
        Size {
 | 
						|
            height: find_max_double(
 | 
						|
                views.iter(),
 | 
						|
                |view| view.size.height,
 | 
						|
            ),
 | 
						|
            width: find_max_double(
 | 
						|
                views.iter(),
 | 
						|
                |view| view.size.width,
 | 
						|
            ),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// The physical characteristic of layout for the purpose of styling
 | 
						|
#[derive(Clone, Copy, PartialEq, Debug)]
 | 
						|
pub enum ArrangementKind {
 | 
						|
    Base = 0,
 | 
						|
    Wide = 1,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug, PartialEq)]
 | 
						|
pub struct Margins {
 | 
						|
    pub top: f64,
 | 
						|
    pub bottom: f64,
 | 
						|
    pub left: f64,
 | 
						|
    pub right: f64,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Clone, Debug, PartialEq)]
 | 
						|
pub enum LatchedState {
 | 
						|
    /// Holds view to return to.
 | 
						|
    FromView(String),
 | 
						|
    Not,
 | 
						|
}
 | 
						|
 | 
						|
// TODO: split into sth like
 | 
						|
// Arrangement (views) + details (keymap) + State (keys)
 | 
						|
/// State of the UI, contains the backend as well
 | 
						|
pub struct Layout {
 | 
						|
    pub margins: Margins,
 | 
						|
    pub kind: ArrangementKind,
 | 
						|
    pub current_view: String,
 | 
						|
 | 
						|
    // If current view is latched,
 | 
						|
    // clicking any button that emits an action (erase, submit, set modifier)
 | 
						|
    // will cause lock buttons to unlatch.
 | 
						|
    view_latched: LatchedState,
 | 
						|
 | 
						|
    // Views own the actual buttons which have state
 | 
						|
    // Maybe they should own UI only,
 | 
						|
    // and keys should be owned by a dedicated non-UI-State?
 | 
						|
    /// Point is the offset within the layout
 | 
						|
    pub views: HashMap<String, (c::Point, View)>,
 | 
						|
 | 
						|
    // Non-UI stuff
 | 
						|
    /// xkb keymaps applicable to the contained keys. Unchangeable
 | 
						|
    pub keymaps: Vec<CString>,
 | 
						|
    // Changeable state
 | 
						|
    // a Vec would be enough, but who cares, this will be small & fast enough
 | 
						|
    // TODO: turn those into per-input point *_buttons to track dragging.
 | 
						|
    // The renderer doesn't need the list of pressed keys any more,
 | 
						|
    // because it needs to iterate
 | 
						|
    // through all buttons of the current view anyway.
 | 
						|
    // When the list tracks actual location,
 | 
						|
    // it becomes possible to place popovers and other UI accurately.
 | 
						|
    pub pressed_keys: HashSet<::util::Pointer<RefCell<KeyState>>>,
 | 
						|
}
 | 
						|
 | 
						|
/// A builder structure for picking up layout data from storage
 | 
						|
pub struct LayoutData {
 | 
						|
    /// Point is the offset within layout
 | 
						|
    pub views: HashMap<String, (c::Point, View)>,
 | 
						|
    pub keymaps: Vec<CString>,
 | 
						|
    pub margins: Margins,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug)]
 | 
						|
struct NoSuchView;
 | 
						|
 | 
						|
impl fmt::Display for NoSuchView {
 | 
						|
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
						|
        write!(f, "No such view")
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Unfortunately, changes are not atomic due to mutability :(
 | 
						|
// An error will not be recoverable
 | 
						|
// The usage of &mut on Rc<RefCell<KeyState>> doesn't mean anything special.
 | 
						|
// Cloning could also be used.
 | 
						|
impl Layout {
 | 
						|
    pub fn new(data: LayoutData, kind: ArrangementKind) -> Layout {
 | 
						|
        Layout {
 | 
						|
            kind,
 | 
						|
            current_view: "base".to_owned(),
 | 
						|
            view_latched: LatchedState::Not,
 | 
						|
            views: data.views,
 | 
						|
            keymaps: data.keymaps,
 | 
						|
            pressed_keys: HashSet::new(),
 | 
						|
            margins: data.margins,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn get_current_view_position(&self) -> &(c::Point, View) {
 | 
						|
        &self.views
 | 
						|
            .get(&self.current_view).expect("Selected nonexistent view")
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn get_current_view(&self) -> &View {
 | 
						|
        &self.views.get(&self.current_view).expect("Selected nonexistent view").1
 | 
						|
    }
 | 
						|
 | 
						|
    fn set_view(&mut self, view: String) -> Result<(), NoSuchView> {
 | 
						|
        if self.views.contains_key(&view) {
 | 
						|
            self.current_view = view;
 | 
						|
            Ok(())
 | 
						|
        } else {
 | 
						|
            Err(NoSuchView)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Layout is passed around mutably,
 | 
						|
    // so better keep the field away from direct access.
 | 
						|
    pub fn get_view_latched(&self) -> &LatchedState {
 | 
						|
        &self.view_latched
 | 
						|
    }
 | 
						|
 | 
						|
    /// Calculates size without margins
 | 
						|
    fn calculate_inner_size(&self) -> Size {
 | 
						|
        View::calculate_super_size(
 | 
						|
            self.views.iter().map(|(_, (_offset, v))| v).collect()
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    /// Size including margins
 | 
						|
    fn calculate_size(&self) -> Size {
 | 
						|
        let inner_size = self.calculate_inner_size();
 | 
						|
        Size {
 | 
						|
            width: self.margins.left + inner_size.width + self.margins.right,
 | 
						|
            height: (
 | 
						|
                self.margins.top
 | 
						|
                + inner_size.height
 | 
						|
                + self.margins.bottom
 | 
						|
            ),
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    pub fn calculate_transformation(
 | 
						|
        &self,
 | 
						|
        available: Size,
 | 
						|
    ) -> c::Transformation {
 | 
						|
        let size = self.calculate_size();
 | 
						|
        let h_scale = available.width / size.width;
 | 
						|
        let v_scale = available.height / size.height;
 | 
						|
        let scale = if h_scale < v_scale { h_scale } else { v_scale };
 | 
						|
        let outside_margins = c::Transformation {
 | 
						|
            origin_x: (available.width - (scale * size.width)) / 2.0,
 | 
						|
            origin_y: (available.height - (scale * size.height)) / 2.0,
 | 
						|
            scale: scale,
 | 
						|
        };
 | 
						|
        outside_margins.chain(c::Transformation {
 | 
						|
            origin_x: self.margins.left,
 | 
						|
            origin_y: self.margins.top,
 | 
						|
            scale: 1.0,
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    fn find_button_by_position(&self, point: c::Point) -> Option<ButtonPlace> {
 | 
						|
        let (offset, layout) = self.get_current_view_position();
 | 
						|
        layout.find_button_by_position(point - offset)
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn foreach_visible_button<F>(&self, mut f: F)
 | 
						|
        where F: FnMut(c::Point, &Box<Button>)
 | 
						|
    {
 | 
						|
        let (view_offset, view) = self.get_current_view_position();
 | 
						|
        for (row_offset, row) in view.get_rows() {
 | 
						|
            for (x_offset, button) in &row.buttons {
 | 
						|
                let offset = view_offset
 | 
						|
                    + row_offset.clone()
 | 
						|
                    + c::Point { x: *x_offset, y: 0.0 };
 | 
						|
                f(offset, button);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    fn apply_view_transition(
 | 
						|
        &mut self,
 | 
						|
        action: &Action,
 | 
						|
    ) {
 | 
						|
        let (transition, new_latched) = Layout::process_action_for_view(
 | 
						|
            action,
 | 
						|
            &self.current_view,
 | 
						|
            &self.view_latched,
 | 
						|
        );
 | 
						|
 | 
						|
        match transition {
 | 
						|
            ViewTransition::UnlatchAll => self.unstick_locks(),
 | 
						|
            ViewTransition::ChangeTo(view) => try_set_view(self, view.into()),
 | 
						|
            ViewTransition::NoChange => {},
 | 
						|
        };
 | 
						|
 | 
						|
        self.view_latched = new_latched;
 | 
						|
    }
 | 
						|
 | 
						|
    /// Unlatch all latched keys,
 | 
						|
    /// so that the new view is the one before first press.
 | 
						|
    fn unstick_locks(&mut self) {
 | 
						|
        if let LatchedState::FromView(name) = self.view_latched.clone() {
 | 
						|
            match self.set_view(name.clone()) {
 | 
						|
                Ok(_) => { self.view_latched = LatchedState::Not; }
 | 
						|
                Err(e) => log_print!(
 | 
						|
                    logging::Level::Bug,
 | 
						|
                    "Bad view {}, can't unlatch ({:?})",
 | 
						|
                    name,
 | 
						|
                    e,
 | 
						|
                ),
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Last bool is new latch state.
 | 
						|
    /// It doesn't make sense when the result carries UnlatchAll,
 | 
						|
    /// but let's not be picky.
 | 
						|
    ///
 | 
						|
    /// Although the state is not defined at the keys
 | 
						|
    /// (it's in the relationship between view and action),
 | 
						|
    /// keys go through the following stages when clicked repeatedly: 
 | 
						|
    /// unlocked+unlatched -> locked+latched -> locked+unlatched
 | 
						|
    /// -> unlocked+unlatched
 | 
						|
    fn process_action_for_view<'a>(
 | 
						|
        action: &'a Action,
 | 
						|
        current_view: &str,
 | 
						|
        latched: &LatchedState,
 | 
						|
    ) -> (ViewTransition<'a>, LatchedState) {
 | 
						|
        match action {
 | 
						|
            Action::Submit { text: _, keys: _ }
 | 
						|
                | Action::Erase
 | 
						|
                | Action::ApplyModifier(_)
 | 
						|
            => {
 | 
						|
                let t = match latched {
 | 
						|
                    LatchedState::FromView(_) => ViewTransition::UnlatchAll,
 | 
						|
                    LatchedState::Not => ViewTransition::NoChange,
 | 
						|
                };
 | 
						|
                (t, LatchedState::Not)
 | 
						|
            },
 | 
						|
            Action::SetView(view) => (
 | 
						|
                ViewTransition::ChangeTo(view),
 | 
						|
                LatchedState::Not,
 | 
						|
            ),
 | 
						|
            Action::LockView { lock, unlock, latches, looks_locked_from: _ } => {
 | 
						|
                use self::ViewTransition as VT;
 | 
						|
                let locked = action.is_locked(current_view);
 | 
						|
                match (locked, latched, latches) {
 | 
						|
                    // Was unlocked, now make locked but latched.
 | 
						|
                    (false, LatchedState::Not, true) => (
 | 
						|
                        VT::ChangeTo(lock),
 | 
						|
                        LatchedState::FromView(current_view.into()),
 | 
						|
                    ),
 | 
						|
                    // Layout is latched for reason other than this button.
 | 
						|
                    (false, LatchedState::FromView(view), true) => (
 | 
						|
                        VT::ChangeTo(lock),
 | 
						|
                        LatchedState::FromView(view.clone()),
 | 
						|
                    ),
 | 
						|
                    // Was latched, now only locked.
 | 
						|
                    (true, LatchedState::FromView(_), true)
 | 
						|
                        => (VT::NoChange, LatchedState::Not),
 | 
						|
                    // Was unlocked, can't latch so now make fully locked.
 | 
						|
                    (false, _, false)
 | 
						|
                        => (VT::ChangeTo(lock), LatchedState::Not),
 | 
						|
                    // Was locked, now make unlocked.
 | 
						|
                    (true, _, _)
 | 
						|
                        => (VT::ChangeTo(unlock), LatchedState::Not),
 | 
						|
                }
 | 
						|
            },
 | 
						|
            _ => (ViewTransition::NoChange, latched.clone()),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug, PartialEq)]
 | 
						|
enum ViewTransition<'a> {
 | 
						|
    ChangeTo(&'a str),
 | 
						|
    UnlatchAll,
 | 
						|
    NoChange,
 | 
						|
}
 | 
						|
 | 
						|
fn try_set_view(layout: &mut Layout, view_name: &str) {
 | 
						|
    layout.set_view(view_name.into())
 | 
						|
        .or_print(
 | 
						|
            logging::Problem::Bug,
 | 
						|
            &format!("Bad view {}, ignoring", view_name),
 | 
						|
        );
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
mod procedures {
 | 
						|
    use super::*;
 | 
						|
 | 
						|
    type Place<'v> = (c::Point, &'v Box<Button>);
 | 
						|
 | 
						|
    /// Finds all buttons referring to the key in `state`,
 | 
						|
    /// together with their offsets within the view.
 | 
						|
    pub fn find_key_places<'v, 's>(
 | 
						|
        view: &'v View,
 | 
						|
        state: &'s Rc<RefCell<KeyState>>
 | 
						|
    ) -> Vec<Place<'v>> {
 | 
						|
        view.get_rows().iter().flat_map(|(row_offset, row)| {
 | 
						|
            row.buttons.iter()
 | 
						|
                .filter_map(move |(x_offset, button)| {
 | 
						|
                    if Rc::ptr_eq(&button.state, state) {
 | 
						|
                        Some((
 | 
						|
                            row_offset + c::Point { x: *x_offset, y: 0.0 },
 | 
						|
                            button,
 | 
						|
                        ))
 | 
						|
                    } else {
 | 
						|
                        None
 | 
						|
                    }
 | 
						|
                })
 | 
						|
        }).collect()
 | 
						|
    }
 | 
						|
    
 | 
						|
    #[cfg(test)]
 | 
						|
    mod test {
 | 
						|
        use super::*;
 | 
						|
 | 
						|
        use ::layout::test::*;
 | 
						|
 | 
						|
        /// Checks whether the path points to the same boxed instances.
 | 
						|
        /// The instance constraint will be droppable
 | 
						|
        /// when C stops holding references to the data
 | 
						|
        #[test]
 | 
						|
        fn view_has_button() {
 | 
						|
            fn as_ptr<T>(v: &Box<T>) -> *const T {
 | 
						|
                v.as_ref() as *const T
 | 
						|
            }
 | 
						|
 | 
						|
            let state = make_state();
 | 
						|
            let state_clone = state.clone();
 | 
						|
 | 
						|
            let button = make_button_with_state("1".into(), state);
 | 
						|
            let button_ptr = as_ptr(&button);
 | 
						|
 | 
						|
            let row = Row::new(vec!((0.1, button)));
 | 
						|
 | 
						|
            let view = View::new(vec!((1.2, row)));
 | 
						|
 | 
						|
            assert_eq!(
 | 
						|
                find_key_places(&view, &state_clone.clone()).into_iter()
 | 
						|
                    .map(|(place, button)| { (place, as_ptr(button)) })
 | 
						|
                    .collect::<Vec<_>>(),
 | 
						|
                vec!(
 | 
						|
                    (c::Point { x: 0.1, y: 1.2 }, button_ptr)
 | 
						|
                )
 | 
						|
            );
 | 
						|
 | 
						|
            let view = View::new(vec![]);
 | 
						|
            assert_eq!(
 | 
						|
                find_key_places(&view, &state_clone.clone()).is_empty(),
 | 
						|
                true
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub struct UIBackend {
 | 
						|
    widget_to_layout: c::Transformation,
 | 
						|
    keyboard: c::EekGtkKeyboard,
 | 
						|
}
 | 
						|
 | 
						|
/// Top level procedures, dispatching to everything
 | 
						|
mod seat {
 | 
						|
    use super::*;
 | 
						|
 | 
						|
    pub fn handle_press_key(
 | 
						|
        layout: &mut Layout,
 | 
						|
        submission: &mut Submission,
 | 
						|
        time: Timestamp,
 | 
						|
        rckey: &Rc<RefCell<KeyState>>,
 | 
						|
    ) {
 | 
						|
        if !layout.pressed_keys.insert(::util::Pointer(rckey.clone())) {
 | 
						|
            log_print!(
 | 
						|
                logging::Level::Bug,
 | 
						|
                "Key {:?} was already pressed", rckey,
 | 
						|
            );
 | 
						|
        }
 | 
						|
        let key: KeyState = {
 | 
						|
            RefCell::borrow(rckey).clone()
 | 
						|
        };
 | 
						|
        let action = key.action.clone();
 | 
						|
        match action {
 | 
						|
            Action::Submit {
 | 
						|
                text: Some(text),
 | 
						|
                keys: _,
 | 
						|
            } => submission.handle_press(
 | 
						|
                KeyState::get_id(rckey),
 | 
						|
                SubmitData::Text(&text),
 | 
						|
                &key.keycodes,
 | 
						|
                time,
 | 
						|
            ),
 | 
						|
            Action::Submit {
 | 
						|
                text: None,
 | 
						|
                keys: _,
 | 
						|
            } => submission.handle_press(
 | 
						|
                KeyState::get_id(rckey),
 | 
						|
                SubmitData::Keycodes,
 | 
						|
                &key.keycodes,
 | 
						|
                time,
 | 
						|
            ),
 | 
						|
            Action::Erase => submission.handle_press(
 | 
						|
                KeyState::get_id(rckey),
 | 
						|
                SubmitData::Erase,
 | 
						|
                &key.keycodes,
 | 
						|
                time,
 | 
						|
            ),
 | 
						|
            _ => {},
 | 
						|
        };
 | 
						|
        RefCell::replace(rckey, key.into_pressed());
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn handle_release_key(
 | 
						|
        layout: &mut Layout,
 | 
						|
        submission: &mut Submission,
 | 
						|
        ui: Option<&UIBackend>,
 | 
						|
        time: Timestamp,
 | 
						|
        manager: Option<manager::c::Manager>,
 | 
						|
        rckey: &Rc<RefCell<KeyState>>,
 | 
						|
    ) {
 | 
						|
        let key: KeyState = {
 | 
						|
            RefCell::borrow(rckey).clone()
 | 
						|
        };
 | 
						|
        let action = key.action.clone();
 | 
						|
 | 
						|
        layout.apply_view_transition(&action);
 | 
						|
 | 
						|
        // update
 | 
						|
        let key = key.into_released();
 | 
						|
 | 
						|
        // process non-view switching
 | 
						|
        match action {
 | 
						|
            Action::Submit { text: _, keys: _ }
 | 
						|
                | Action::Erase
 | 
						|
            => {
 | 
						|
                submission.handle_release(KeyState::get_id(rckey), time);
 | 
						|
            },
 | 
						|
            Action::ApplyModifier(modifier) => {
 | 
						|
                // FIXME: key id is unneeded with stateless locks
 | 
						|
                let key_id = KeyState::get_id(rckey);
 | 
						|
                let gets_locked = !submission.is_modifier_active(modifier);
 | 
						|
                match gets_locked {
 | 
						|
                    true => submission.handle_add_modifier(
 | 
						|
                        key_id,
 | 
						|
                        modifier, time,
 | 
						|
                    ),
 | 
						|
                    false => submission.handle_drop_modifier(key_id, time),
 | 
						|
                }
 | 
						|
            }
 | 
						|
            // only show when UI is present
 | 
						|
            Action::ShowPreferences => if let Some(ui) = &ui {
 | 
						|
                // only show when layout manager is available
 | 
						|
                if let Some(manager) = manager {
 | 
						|
                    let view = layout.get_current_view();
 | 
						|
                    let places = ::layout::procedures::find_key_places(
 | 
						|
                        view, &rckey,
 | 
						|
                    );
 | 
						|
                    // Getting first item will cause mispositioning
 | 
						|
                    // with more than one button with the same key
 | 
						|
                    // on the keyboard.
 | 
						|
                    if let Some((position, button)) = places.get(0) {
 | 
						|
                        let bounds = c::Bounds {
 | 
						|
                            x: position.x,
 | 
						|
                            y: position.y,
 | 
						|
                            width: button.size.width,
 | 
						|
                            height: button.size.height,
 | 
						|
                        };
 | 
						|
                        ::popover::show(
 | 
						|
                            ui.keyboard,
 | 
						|
                            ui.widget_to_layout.reverse_bounds(bounds),
 | 
						|
                            manager,
 | 
						|
                        );
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
            // Other keys are handled in view switcher before.
 | 
						|
            _ => {}
 | 
						|
        };
 | 
						|
 | 
						|
        let pointer = ::util::Pointer(rckey.clone());
 | 
						|
        // Apply state changes
 | 
						|
        layout.pressed_keys.remove(&pointer);
 | 
						|
        // Commit activated button state changes
 | 
						|
        RefCell::replace(rckey, key);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[cfg(test)]
 | 
						|
mod test {
 | 
						|
    use super::*;
 | 
						|
 | 
						|
    use std::ffi::CString;
 | 
						|
    use ::keyboard::PressType;
 | 
						|
 | 
						|
    pub fn make_state_with_action(action: Action)
 | 
						|
        -> Rc<RefCell<::keyboard::KeyState>>
 | 
						|
    {
 | 
						|
        Rc::new(RefCell::new(::keyboard::KeyState {
 | 
						|
            pressed: PressType::Released,
 | 
						|
            keycodes: Vec::new(),
 | 
						|
            action,
 | 
						|
        }))
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn make_state() -> Rc<RefCell<::keyboard::KeyState>> {
 | 
						|
        make_state_with_action(Action::SetView("default".into()))
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn make_button_with_state(
 | 
						|
        name: String,
 | 
						|
        state: Rc<RefCell<::keyboard::KeyState>>,
 | 
						|
    ) -> Box<Button> {
 | 
						|
        Box::new(Button {
 | 
						|
            name: CString::new(name.clone()).unwrap(),
 | 
						|
            size: Size { width: 0f64, height: 0f64 },
 | 
						|
            outline_name: CString::new("test").unwrap(),
 | 
						|
            label: Label::Text(CString::new(name).unwrap()),
 | 
						|
            state: state,
 | 
						|
        })
 | 
						|
    }
 | 
						|
    
 | 
						|
    #[test]
 | 
						|
    fn latch_lock_unlock() {
 | 
						|
        let action = Action::LockView {
 | 
						|
            lock: "lock".into(),
 | 
						|
            unlock: "unlock".into(),
 | 
						|
            latches: true,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            Layout::process_action_for_view(&action, "unlock", &LatchedState::Not),
 | 
						|
            (ViewTransition::ChangeTo("lock"), LatchedState::FromView("unlock".into())),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            Layout::process_action_for_view(&action, "lock", &LatchedState::FromView("unlock".into())),
 | 
						|
            (ViewTransition::NoChange, LatchedState::Not),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            Layout::process_action_for_view(&action, "lock", &LatchedState::Not),
 | 
						|
            (ViewTransition::ChangeTo("unlock"), LatchedState::Not),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            Layout::process_action_for_view(&Action::Erase, "lock", &LatchedState::FromView("base".into())),
 | 
						|
            (ViewTransition::UnlatchAll, LatchedState::Not),
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn latch_pop_layout() {
 | 
						|
        let switch = Action::LockView {
 | 
						|
            lock: "locked".into(),
 | 
						|
            unlock: "base".into(),
 | 
						|
            latches: true,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
        
 | 
						|
        let submit = Action::Erase;
 | 
						|
 | 
						|
        let view = View::new(vec![(
 | 
						|
            0.0,
 | 
						|
            Row::new(vec![
 | 
						|
                (
 | 
						|
                    0.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "switch".into(),
 | 
						|
                        make_state_with_action(switch.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
                (
 | 
						|
                    1.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "submit".into(),
 | 
						|
                        make_state_with_action(submit.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
            ]),
 | 
						|
        )]);
 | 
						|
 | 
						|
        let mut layout = Layout {
 | 
						|
            current_view: "base".into(),
 | 
						|
            view_latched: LatchedState::Not,
 | 
						|
            keymaps: Vec::new(),
 | 
						|
            kind: ArrangementKind::Base,
 | 
						|
            pressed_keys: HashSet::new(),
 | 
						|
            margins: Margins {
 | 
						|
                top: 0.0,
 | 
						|
                left: 0.0,
 | 
						|
                right: 0.0,
 | 
						|
                bottom: 0.0,
 | 
						|
            },
 | 
						|
            views: hashmap! {
 | 
						|
                // Both can use the same structure.
 | 
						|
                // Switching doesn't depend on the view shape
 | 
						|
                // as long as the switching button is present.
 | 
						|
                "base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
 | 
						|
                "locked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
 | 
						|
            },
 | 
						|
        };        
 | 
						|
 | 
						|
        // Basic cycle
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&submit);
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        assert_eq!(&layout.current_view, "base");
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        // Unlatch
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&submit);
 | 
						|
        assert_eq!(&layout.current_view, "base");
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn reverse_unlatch_layout() {
 | 
						|
        let switch = Action::LockView {
 | 
						|
            lock: "locked".into(),
 | 
						|
            unlock: "base".into(),
 | 
						|
            latches: true,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
        
 | 
						|
        let unswitch = Action::LockView {
 | 
						|
            lock: "locked".into(),
 | 
						|
            unlock: "unlocked".into(),
 | 
						|
            latches: false,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
        
 | 
						|
        let submit = Action::Erase;
 | 
						|
 | 
						|
        let view = View::new(vec![(
 | 
						|
            0.0,
 | 
						|
            Row::new(vec![
 | 
						|
                (
 | 
						|
                    0.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "switch".into(),
 | 
						|
                        make_state_with_action(switch.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
                (
 | 
						|
                    1.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "submit".into(),
 | 
						|
                        make_state_with_action(submit.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
            ]),
 | 
						|
        )]);
 | 
						|
 | 
						|
        let mut layout = Layout {
 | 
						|
            current_view: "base".into(),
 | 
						|
            view_latched: LatchedState::Not,
 | 
						|
            keymaps: Vec::new(),
 | 
						|
            kind: ArrangementKind::Base,
 | 
						|
            pressed_keys: HashSet::new(),
 | 
						|
            margins: Margins {
 | 
						|
                top: 0.0,
 | 
						|
                left: 0.0,
 | 
						|
                right: 0.0,
 | 
						|
                bottom: 0.0,
 | 
						|
            },
 | 
						|
            views: hashmap! {
 | 
						|
                // Both can use the same structure.
 | 
						|
                // Switching doesn't depend on the view shape
 | 
						|
                // as long as the switching button is present.
 | 
						|
                "base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
 | 
						|
                "locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
 | 
						|
                "unlocked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
 | 
						|
            },
 | 
						|
        };        
 | 
						|
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&unswitch);
 | 
						|
        assert_eq!(&layout.current_view, "unlocked");
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn latch_twopop_layout() {
 | 
						|
        let switch = Action::LockView {
 | 
						|
            lock: "locked".into(),
 | 
						|
            unlock: "base".into(),
 | 
						|
            latches: true,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
        
 | 
						|
        let switch_again = Action::LockView {
 | 
						|
            lock: "ĄĘ".into(),
 | 
						|
            unlock: "locked".into(),
 | 
						|
            latches: true,
 | 
						|
            looks_locked_from: vec![],
 | 
						|
        };
 | 
						|
        
 | 
						|
        let submit = Action::Erase;
 | 
						|
 | 
						|
        let view = View::new(vec![(
 | 
						|
            0.0,
 | 
						|
            Row::new(vec![
 | 
						|
                (
 | 
						|
                    0.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "switch".into(),
 | 
						|
                        make_state_with_action(switch.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
                (
 | 
						|
                    1.0,
 | 
						|
                    make_button_with_state(
 | 
						|
                        "submit".into(),
 | 
						|
                        make_state_with_action(submit.clone())
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
            ]),
 | 
						|
        )]);
 | 
						|
 | 
						|
        let mut layout = Layout {
 | 
						|
            current_view: "base".into(),
 | 
						|
            view_latched: LatchedState::Not,
 | 
						|
            keymaps: Vec::new(),
 | 
						|
            kind: ArrangementKind::Base,
 | 
						|
            pressed_keys: HashSet::new(),
 | 
						|
            margins: Margins {
 | 
						|
                top: 0.0,
 | 
						|
                left: 0.0,
 | 
						|
                right: 0.0,
 | 
						|
                bottom: 0.0,
 | 
						|
            },
 | 
						|
            views: hashmap! {
 | 
						|
                // All can use the same structure.
 | 
						|
                // Switching doesn't depend on the view shape
 | 
						|
                // as long as the switching button is present.
 | 
						|
                "base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
 | 
						|
                "locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
 | 
						|
                "ĄĘ".into() => (c::Point { x: 0.0, y: 0.0 }, view),
 | 
						|
            },
 | 
						|
        };        
 | 
						|
 | 
						|
        // Latch twice, then Ąto-unlatch across 2 levels
 | 
						|
        layout.apply_view_transition(&switch);
 | 
						|
        println!("{:?}", layout.view_latched);
 | 
						|
        assert_eq!(&layout.current_view, "locked");
 | 
						|
        layout.apply_view_transition(&switch_again);
 | 
						|
        println!("{:?}", layout.view_latched);
 | 
						|
        assert_eq!(&layout.current_view, "ĄĘ");
 | 
						|
        layout.apply_view_transition(&submit);
 | 
						|
        println!("{:?}", layout.view_latched);
 | 
						|
        assert_eq!(&layout.current_view, "base");
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn check_centering() {
 | 
						|
        //    A B
 | 
						|
        // ---bar---
 | 
						|
        let view = View::new(vec![
 | 
						|
            (
 | 
						|
                0.0,
 | 
						|
                Row::new(vec![
 | 
						|
                    (
 | 
						|
                        0.0,
 | 
						|
                        Box::new(Button {
 | 
						|
                            size: Size { width: 5.0, height: 10.0 },
 | 
						|
                            ..*make_button_with_state("A".into(), make_state())
 | 
						|
                        }),
 | 
						|
                    ),
 | 
						|
                    (
 | 
						|
                        5.0,
 | 
						|
                        Box::new(Button {
 | 
						|
                            size: Size { width: 5.0, height: 10.0 },
 | 
						|
                            ..*make_button_with_state("B".into(), make_state())
 | 
						|
                        }),
 | 
						|
                    ),
 | 
						|
                ]),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                10.0,
 | 
						|
                Row::new(vec![
 | 
						|
                    (
 | 
						|
                        0.0,
 | 
						|
                        Box::new(Button {
 | 
						|
                            size: Size { width: 30.0, height: 10.0 },
 | 
						|
                            ..*make_button_with_state("bar".into(), make_state())
 | 
						|
                        }),
 | 
						|
                    ),
 | 
						|
                ]),
 | 
						|
            )
 | 
						|
        ]);
 | 
						|
        assert!(
 | 
						|
            view.find_button_by_position(c::Point { x: 5.0, y: 5.0 })
 | 
						|
                .unwrap().button.name.to_str().unwrap() == "A"
 | 
						|
        );
 | 
						|
        assert!(
 | 
						|
            view.find_button_by_position(c::Point { x: 14.99, y: 5.0 })
 | 
						|
                .unwrap().button.name.to_str().unwrap() == "A"
 | 
						|
        );
 | 
						|
        assert!(
 | 
						|
            view.find_button_by_position(c::Point { x: 15.01, y: 5.0 })
 | 
						|
                .unwrap().button.name.to_str().unwrap() == "B"
 | 
						|
        );
 | 
						|
        assert!(
 | 
						|
            view.find_button_by_position(c::Point { x: 25.0, y: 5.0 })
 | 
						|
                .unwrap().button.name.to_str().unwrap() == "B"
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn check_bottom_margin() {
 | 
						|
        // just one button
 | 
						|
        let view = View::new(vec![
 | 
						|
            (
 | 
						|
                0.0,
 | 
						|
                Row::new(vec![(
 | 
						|
                    0.0,
 | 
						|
                    Box::new(Button {
 | 
						|
                        size: Size { width: 1.0, height: 1.0 },
 | 
						|
                        ..*make_button_with_state("foo".into(), make_state())
 | 
						|
                    }),
 | 
						|
                )]),
 | 
						|
            ),
 | 
						|
        ]);
 | 
						|
        let layout = Layout {
 | 
						|
            current_view: String::new(),
 | 
						|
            view_latched: LatchedState::Not,
 | 
						|
            keymaps: Vec::new(),
 | 
						|
            kind: ArrangementKind::Base,
 | 
						|
            pressed_keys: HashSet::new(),
 | 
						|
            // Lots of bottom margin
 | 
						|
            margins: Margins {
 | 
						|
                top: 0.0,
 | 
						|
                left: 0.0,
 | 
						|
                right: 0.0,
 | 
						|
                bottom: 1.0,
 | 
						|
            },
 | 
						|
            views: hashmap! {
 | 
						|
                String::new() => (c::Point { x: 0.0, y: 0.0 }, view),
 | 
						|
            },
 | 
						|
        };
 | 
						|
        assert_eq!(
 | 
						|
            layout.calculate_inner_size(),
 | 
						|
            Size { width: 1.0, height: 1.0 }
 | 
						|
        );
 | 
						|
        assert_eq!(
 | 
						|
            layout.calculate_size(),
 | 
						|
            Size { width: 1.0, height: 2.0 }
 | 
						|
        );
 | 
						|
        // Don't change those values randomly!
 | 
						|
        // They take advantage of incidental precise float representation
 | 
						|
        // to even be comparable.
 | 
						|
        let transformation = layout.calculate_transformation(
 | 
						|
            Size { width: 2.0, height: 2.0 }
 | 
						|
        );
 | 
						|
        assert_eq!(transformation.scale, 1.0);
 | 
						|
        assert_eq!(transformation.origin_x, 0.5);
 | 
						|
        assert_eq!(transformation.origin_y, 0.0);
 | 
						|
    }
 | 
						|
}
 |