It is not just because of primitives, it will work for all types that implement Copy. Will not work otherwise:
trait Cool: Sized + std::fmt::Debug {
    fn cool(self) {
        println!("cool -> {:?}", self);
    }
}
#[derive(Debug)]
struct NonCopy;
impl Cool for i32 {}
impl Cool for NonCopy {}
fn main(){
    let val = 123;
    val.cool();
    (&val).cool();
    
    let nc = NonCopy{};
    nc.cool();
    (&nc).cool();
}
Fails with a clear error code:
error[E0507]: cannot move out of a shared reference
  --> src/main.rs:20:5
   |
20 |     (&nc).cool();
   |     ^^^^^^------
   |     |     |
   |     |     value moved due to this method call
   |     move occurs because value has type `NonCopy`, which does not implement the `Copy` trait
   |
Playground
What it's happening is that with the Copy types rust creates a copy transparently for you when needed.
Note that it fails even if we comment out the previous line // nc.cool();, which obviously moves the value...