In the real program y is calculated to change the index pointing into a 64K array , and wraparound is the desired behavior)
There is no consensus of how things should be done, but here's my advice: Provide to the user two functions:
fn add(&mut self, index: u16) -> u16 { // return previous index why not
    // ..
}
fn sub(&mut self, index: u16) -> u16 {
    // ..
}
You could also add a helper function that should not be used lightly:
fn offset(&mut self, offset: i16) -> u16 {
    // ..
}
The purpose is that the user should know whenever use sub or add, the user should manage only unsigned type. That question is opinion oriented so I understand if people don't agree.
Full example:
use std::mem;
#[derive(Debug, PartialEq, PartialOrd)]
struct MyIndex {
    index: u16,
}
impl MyIndex {
    fn new(index: u16) -> Self {
        Self { index }
    }
    fn add(&mut self, index: u16) -> u16 {
        let index = self.index.wrapping_add(index);
        self.replace(index)
    }
    fn sub(&mut self, index: u16) -> u16 {
        let index = self.index.wrapping_sub(index);
        self.replace(index)
    }
    fn offset(&mut self, offset: i16) -> u16 {
        if offset > 0 {
            self.add(offset as u16)
        } else {
            self.sub(offset as u16)
        }
    }
    fn replace(&mut self, index: u16) -> u16 {
        mem::replace(&mut self.index, index)
    }
}
fn main() {
    let mut index = MyIndex::new(42);
    let mut other_index = MyIndex::new(84);
    let (x, first) = if index > other_index {
        (index.index - other_index.index, true)
    }
    else {
        (other_index.index - index.index, false)
    };
    // ...
    if first {
        index.sub(x);
    }
    else {
        other_index.sub(x);
    }
    println!("{:?} {:?}", index, other_index);
    index.sub(21);
    println!("{:?}", index);
    index.offset(-1);
    println!("{:?}", index);
}