Search code examples
cssrustdom-eventscoordinate-systemsyew

onmousemove event results in wrong position


I am trying to move an element in the screen using Yew. When element is in the top left of the screen, I can move it correctly. But when the element starts in other position, it gets to a wrong place when trying to move it. It seems that the unwanted behaviour depends on the distance from the top left of the screen from where the element is.

I tried to reproduce same logic in React achieving the expected results, but do not know why in Yew I have this strange behaviour. A sandbox is available here.

This is the element I want to move. piece.rs

use yew::prelude::*;

struct Coordinates {
    x: i32,
    y: i32,
}

#[function_component]
pub fn Pawn() -> Html {
    let coordinates: UseStateHandle<Coordinates> = use_state(|| Coordinates { x: 0, y: 0 });
    let is_dragging: UseStateHandle<bool> = use_state(|| false);

    let onmousedown: Callback<MouseEvent> = {
        let is_dragging: UseStateHandle<bool> = is_dragging.clone();
        Callback::from(move |event: MouseEvent| -> () {
            event.prevent_default();
            is_dragging.set(true);
        })
    };
    let onmouseup: Callback<MouseEvent> = {
        let is_dragging: UseStateHandle<bool> = is_dragging.clone();
        Callback::from(move |event: MouseEvent| -> () {
            event.prevent_default();
            is_dragging.set(false);
        })
    };
    let onmousemove: Callback<MouseEvent> = {
        let coordinates: UseStateHandle<Coordinates> = coordinates.clone();

        Callback::from(move |event: MouseEvent| -> () {
            event.prevent_default();
            //web_sys::console::log_1(&is_dragging.to_string().into());
            if *is_dragging == true {
                coordinates.set(Coordinates {
                    x: event.client_x(), 
                    y: event.client_y()
                });
            }
        })
    };

    html! {
        <div
        class="pawn"
        onmouseleave={onmouseup.clone()}
        onmousedown={onmousedown}
        onmouseup={onmouseup}
        onmousemove={onmousemove}
        style={format!("left: {}px; top: {}px;",
         coordinates.x, coordinates.y)}>
         <img src="img/pawn.png" width="50px" height="50px" />
        </div>

    }
}

The following code is added for the question context and to make an reproducible example, these are the main.rs and index.html files:

use yew::prelude::*;
mod board;
mod piece;
use piece::Pawn;

#[function_component]
fn App() -> Html {
    let is_flipped: UseStateHandle<bool> = use_state(|| false);
    let onclick: Callback<MouseEvent> = {
        let is_flipped: UseStateHandle<bool> = is_flipped.clone();
        Callback::from(move |_| match *is_flipped {
            true => is_flipped.set(false),
            false => is_flipped.set(true),
        })
    };

    let set_pawn = |square: &String| -> Html {
        if square.ends_with("7") {
            html! {
               <Pawn />
            }
        } else {
            html! {}
        }
    };

    let render_squares = |squares: &Vec<String>| -> Html {
        html! {
            <tr>
                {
                    for squares.iter().map(|square|
                    html!{
                        <td class="square"><p class="nomenclature">{square} </p> {set_pawn(square)}</td>
                    })
                }
            </tr>
        }
    };

    html! {
        <div class={".container"}>
        <table>
            {
            for board::create_board(*is_flipped).iter().map(render_squares).rev()
            }
        </table>

        <aside>
        <button onclick={onclick}>{"Flip"}</button>
        </aside>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Yew App</title>
    <link data-trunk rel="copy-dir" href="img" />
  </head>
  <style>
    html {
      color: white;
      user-select: none;
      background-color: black;
    }
    .container {
      display: flex;
    }
    table {
      aspect-ratio: 1 /1;
      width: 100vw;
      max-width: 100vh;
    }
    .square {
      color: black;
      padding: 0;
      margin: 0;
      background-color: white;
      position: relative;
    }
    tr:nth-child(odd) td:nth-child(even),
    tr:nth-child(even) td:nth-child(odd) {
      background: green;
    }
    .nomenclature {
      font-size: calc(0.25rem + 1.25vmin);
      position: absolute;
    }

    .pawn {
      position: absolute;
      cursor: grab;
      z-index: 100;
    }
    
  </style>
</html>

Finally, this a method needed to create the layout to be shown.

board.rs

type Board = Vec<Vec<String>>;

pub fn create_board(flipped: bool) -> Board {
    let mut board: Board = Vec::new();
    let mut columns: Vec<char> = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
    let mut rows: Vec<char> = vec!['1', '2', '3', '4', '5', '6', '7', '8'];

    if flipped {
        columns = columns.into_iter().rev().collect();
        rows = rows.into_iter().rev().collect();
    }
    for row in rows {
        board.push(Vec::new());
        for column in &columns {
            let mut square = String::new();
            square.push(*column);
            square.push(row);
            let len: usize = board.len();
            board[len - 1].push(square);
        }
    }

    board
}

How can I fix this and be able to move an element correctly in Yew? Why this logic seems to work in React but not in Yew?

EDIT: When I console log the event coordinates, these three are identical.

web_sys::console::log_1(&event.client_x().to_string().into());
web_sys::console::log_1(&event.screen_x().to_string().into());
web_sys::console::log_1(&event.page_x().to_string().into());

EDIT 2: The problem seems to be in position: relative; in the .square style. If I apply this to the react example, it results in same unwanted behaviour as in Yew.


Solution

  • I solved it in two steps:

    First, I removed position: relative in .square css selector, which is the direct parent of .pawn. This was causing the strange behaviour when moving the element.

    Second, because the initial state of coordinates was {x:0, y:0} in the piece.rs component, all the elements where stacking in that position. To solve that I created a closure to prevent setting up that initial position:

     let set_position = || -> String {
            if coordinates.x != 0 && coordinates.y != 0 {
                format!("left: {}px; top: {}px;", coordinates.x, coordinates.y)
            } else {
                "".to_string()
            }
        };
    

    And then passed it to returning html tag:

    html! {
            <div
            class="pawn"
            style={set_position()}>
            onmouseleave={onmouseup.clone()}
            onmousedown={onmousedown}
            onmouseup={onmouseup}
            onmousemove={onmousemove}
             <img src="img/pawn.png" width="50px" height="50px" />
            </div>
    
    

    I realized this when using devtools in the browser and finding that, after unselecting top and left properties in the element, it went to its expected initial position. Also, it was important to notice that in the react example, I did not use position: relative in the direct parent.

    UPDATE:

    Alternatively to that, you can use rust Option type to set first state as None:

    struct Coordinates {
        x: Option<i32>,
        y: Option<i32>,
    }
    ...
    
    let coordinates: UseStateHandle<Coordinates> = use_state(|| Coordinates { x: None, y: None });
    
    ...
    coordinates.set(Coordinates {x: Some(x),y: Some(y)});
    

    And then finally conditionally use it in the div style:

    style={if coordinates.x.is_some() && coordinates.y.is_some() {format!("left: {}px; top: {}px;", coordinates.x.unwrap(), coordinates.y.unwrap())} else {"".to_string()}}>
    

    Or if you liked the previous closure approach:

    let set_position = || -> Option<String> {
        match coordinates.x.is_some() && coordinates.y.is_some() {
            true => Some(format!(
                "left: {}px; top: {}px;",
                coordinates.x.unwrap(),
                coordinates.y.unwrap()
            )),
            false => None,
        }
    };